Commit cf84a4b9 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into 422-errors

parents eddcf671 00e7f155
......@@ -90,12 +90,14 @@ const config = Object.freeze({
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true',
},
api: {
host: apiHost,
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
},
statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
basePath: '',
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
......
......@@ -16,6 +16,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
# api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
......
async function rewrites() {
return [
{ source: '/node-api/proxy/:slug*', destination: '/api/proxy' },
{ source: '/node-api/:slug*', destination: '/api/:slug*' },
].filter(Boolean);
}
......
......@@ -307,6 +307,7 @@ frontend:
- "/"
prefix:
# - "/(apps|auth/profile|account)"
- "/account"
- "/apps"
- "/_next"
- "/node-api"
......
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3c0-.184.15-.333.333-.333h4.334v2c0 .368.298.666.666.666h2v1.334h1.334v-.92c0-.441-.176-.865-.489-1.178L9.431 1.822a1.667 1.667 0 0 0-1.179-.489H4.333c-.92 0-1.666.747-1.666 1.667v3.667H4V3Zm0 10v-1H2.667v1c0 .92.746 1.667 1.666 1.667H11c.92 0 1.667-.746 1.667-1.667v-1h-1.334v1a.333.333 0 0 1-.333.333H4.333A.333.333 0 0 1 4 13Zm1.167-5.833H4.75c-.575 0-1.042.466-1.042 1.041v2.084c0 .575.466 1.041 1.042 1.041h.417a1.04 1.04 0 0 0 1.041-1.041v-.209a.418.418 0 0 0-.416-.416.418.418 0 0 0-.417.416v.209a.209.209 0 0 1-.208.208H4.75a.209.209 0 0 1-.208-.208V8.208c0-.114.093-.208.208-.208h.417a.21.21 0 0 1 .208.208v.209a.417.417 0 0 0 .833 0v-.209a1.04 1.04 0 0 0-1.041-1.041Zm2.666 0a1.21 1.21 0 0 0-.599 2.258l.662.377a.375.375 0 0 1-.188.7h-.666a.418.418 0 0 0-.417.417c0 .23.188.417.417.417h.666a1.21 1.21 0 0 0 .6-2.258l-.662-.377a.375.375 0 0 1 .188-.7h.458a.418.418 0 0 0 0-.833h-.459Zm2.125.416v.823c0 .6.144 1.188.417 1.719a3.737 3.737 0 0 0 .417-1.719v-.823a.417.417 0 0 1 .833 0v.823a4.58 4.58 0 0 1-.77 2.542l-.134.2a.415.415 0 0 1-.692 0l-.133-.2a4.58 4.58 0 0 1-.771-2.542v-.823a.417.417 0 0 1 .833 0Z" fill="currentColor"/>
</svg>
import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config';
import type { ApiResource } from './resources';
export default function buildUrl(
resource: ApiResource,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
// FIXME
// 1. I was not able to figure out how to send CORS with credentials from localhost
// unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server
// 2. and there is an issue with API and csrf token
// for some reason API will reply with error "Bad request" to any PUT / POST CORS request
// even though valid csrf-token is passed in header
// we also can pass token in request body but in this case API will replay with "Forbidden" error
// @nikitosing said it will take a lot of time to debug this problem on back-end side, maybe he'll change his mind in future :)
// To sum up, we are using next.js proxy for all instances where app host is not the same as API host (incl. localhost)
// will need to change the condition if there are more micro services that need authentication and DB state changes
const needProxy = appConfig.host !== appConfig.api.host;
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, String(value));
});
return url.toString();
}
......@@ -13,10 +13,12 @@ export default function fetchFactory(
apiEndpoint: string = appConfig.api.endpoint,
) {
return function fetch(path: string, init?: RequestInit): Promise<Response> {
const csrfToken = _req.headers['x-csrf-token']?.toString();
const headers = {
accept: 'application/json',
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
...(csrfToken ? { 'x-csrf-token': csrfToken } : {}),
};
const url = new URL(path, apiEndpoint);
......
import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress } from 'types/api/account';
import type {
Address,
AddressCounters,
AddressTokenBalance,
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse,
AddressCoinBalanceHistoryChart,
AddressBlocksValidatedResponse,
AddressInternalTxsResponse,
AddressTxsFilters,
AddressTokenTransferFilters,
} from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponse } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
export interface ApiResource {
path: string;
endpoint?: string;
basePath?: string;
}
export const RESOURCES = {
// ACCOUNT
user_info: {
path: '/api/account/v1/user/info',
},
custom_abi: {
path: '/api/account/v1/user/custom_abis/:id?',
},
watchlist: {
path: '/api/account/v1/user/watchlist/:id?',
},
public_tags: {
path: '/api/account/v1/user/public_tags/:id?',
},
private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?',
},
private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?',
},
api_keys: {
path: '/api/account/v1/user/api_keys/:id?',
},
// STATS
stats_counters: {
path: '/api/v1/counters',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_charts: {
path: '/api/v1/charts/line',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
// BLOCKS, TXS
blocks: {
path: '/api/v2/blocks',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [ 'type' as const ],
},
block: {
path: '/api/v2/blocks/:id',
},
block_txs: {
path: '/api/v2/blocks/:id/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
txs_validated: {
path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
},
txs_pending: {
path: '/api/v2/transactions',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
},
tx: {
path: '/api/v2/transactions/:id',
},
tx_internal_txs: {
path: '/api/v2/transactions/:id/internal-transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ ],
},
tx_logs: {
path: '/api/v2/transactions/:id/logs',
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ],
},
tx_token_transfers: {
path: '/api/v2/transactions/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ],
},
tx_raw_trace: {
path: '/api/v2/transactions/:id/raw-trace',
},
// ADDRESS
address: {
path: '/api/v2/addresses/:id',
},
address_counters: {
path: '/api/v2/addresses/:id/counters',
},
address_token_balances: {
path: '/api/v2/addresses/:id/token-balances',
},
address_txs: {
path: '/api/v2/addresses/:id/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ],
},
address_internal_txs: {
path: '/api/v2/addresses/:id/internal-transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ],
},
address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated',
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ],
},
address_coin_balance: {
path: '/api/v2/addresses/:id/coin-balance-history',
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ],
},
address_coin_balance_chart: {
path: '/api/v2/addresses/:id/coin-balance-history-by-day',
},
// HOMEPAGE
homepage_stats: {
path: '/api/v2/stats',
},
homepage_chart_txs: {
path: '/api/v2/stats/charts/transactions',
},
homepage_chart_market: {
path: '/api/v2/stats/charts/market',
},
homepage_blocks: {
path: '/api/v2/main-page/blocks',
},
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status',
},
// CONFIG
config_json_rpc: {
path: '/api/v2/config/json-rpc-url',
},
// DEPRECATED
old_api: {
path: '/api',
},
};
export type ResourceName = keyof typeof RESOURCES;
export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] extends {filterFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['filterFields']> :
never;
export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] extends {paginationFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['paginationFields']> :
never;
export const resourceKey = (x: keyof typeof RESOURCES) => x;
export interface ResourceError<T = unknown> {
error?: T;
payload?: T;
status: Response['status'];
statusText: Response['statusText'];
}
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 */
import React from 'react';
import appConfig from 'configs/app/config';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl';
import { RESOURCES } from './resources';
import type { ResourceError, ApiResource } from './resources';
export interface Params {
pathParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>;
}
export default function useApiFetch() {
const fetch = useFetch();
return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = ResourceError>(
resourceName: R,
{ pathParams, queryParams, fetchParams }: Params = {},
) => {
const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resource, pathParams, queryParams);
return fetch<SuccessType, ErrorType>(url, {
credentials: 'include',
...(resource.endpoint && appConfig.host === 'localhost' ? {
headers: {
'x-endpoint': resource.endpoint,
},
} : {}),
...fetchParams,
});
}, [ fetch ]);
}
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
}
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
if (pathParams || queryParams) {
return [ resource, { ...pathParams, ...queryParams } ];
}
return [ resource ];
}
export default function useApiQuery<R extends ResourceName, E = unknown>(
resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E> = {},
) {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError<E>, ResourcePayload<R>>(
getResourceKey(resource, { pathParams, queryParams }),
async() => {
return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams });
}, queryOptions);
}
......@@ -55,6 +55,7 @@ function makePolicyMap() {
'connect-src': [
KEY_WORDS.SELF,
...MAIN_DOMAINS,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
......@@ -62,7 +63,10 @@ function makePolicyMap() {
// client error monitoring
'sentry.io', '*.sentry.io',
// API
appConfig.api.endpoint,
appConfig.api.socket,
appConfig.statsApi.endpoint,
// ad
'request-global.czilladx.com',
......
export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined) {
if (val === undefined) {
return;
}
const valArray = [];
if (typeof val === 'string') {
valArray.push(...val.split(','));
......
......@@ -4,27 +4,32 @@ import React from 'react';
import type { CsrfData } from 'types/client/account';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
import type { ResourceError } from 'lib/api/resources';
interface Params {
export interface Params {
method?: RequestInit['method'];
headers?: RequestInit['headers'];
body?: Record<string, unknown>;
credentials?: RequestCredentials;
}
export default function useFetch() {
const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ErrorType<Error>> => {
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const reqParams = {
...params,
body: params?.method && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({ ...params?.body, _csrf_token: token }) :
body: params?.method && params?.body && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({
...params.body,
_csrf_token: token,
}) :
undefined,
headers: {
...params?.headers,
// ...(token ? { 'x-csrf-token': token } : {}),
},
};
return fetch(path, reqParams).then(response => {
......@@ -37,7 +42,9 @@ export default function useFetch() {
return response.json().then(
(jsonError) => Promise.reject({
// DEPRECATED
error: jsonError as Error,
payload: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
......
import { useQuery } from '@tanstack/react-query';
import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries';
import useApiQuery from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
interface Error {
error?: {
status?: number;
statusText?: string;
};
}
export default function useFetchProfileInfo() {
const fetch = useFetch();
return useQuery<unknown, Error, UserInfo>([ QueryKeys.profile ], async() => {
return fetch('/node-api/account/profile');
}, {
return useApiQuery('user_info', {
queryOptions: {
refetchOnMount: false,
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
},
});
}
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash';
import { useQueryClient } from '@tanstack/react-query';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll';
import { PAGINATION_FIELDS, PAGINATION_FILTERS_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery';
import useFetch from 'lib/hooks/useFetch';
interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
interface Params<Resource extends PaginatedResources> {
resourceName: Resource;
options?: UseApiQueryParams<Resource>['queryOptions'];
pathParams?: UseApiQueryParams<Resource>['pathParams'];
filters?: PaginationFilters<Resource>;
scroll?: { elem: string; offset: number };
}
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({
queryName,
export default function useQueryWithPages<Resource extends PaginatedResources>({
resourceName,
filters,
options,
apiPath,
queryIds,
pathParams,
scroll,
}: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName];
}: Params<Resource>) {
const resource = RESOURCES[resourceName];
const queryClient = useQueryClient();
const router = useRouter();
type NextPageParams = {
[K in keyof PaginatedResponse<Resource>['next_page_params']]: string;
}
const currPageParams = mapValues(pick(router.query, resource.paginationFields), (value) => value?.toString()) as NextPageParams;
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch();
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: currPageParams,
});
const [ hasPagination, setHasPagination ] = React.useState(page > 1);
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const queryParams = { ...filters, ...pageParams[page] };
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
queryKey,
async() => {
const params: Array<string> = [];
Object.entries({ ...filters, ...currPageParams }).forEach(([ key, val ]) => {
if (Array.isArray(val)) {
val.length && params.push(`${ key }=${ val.join(',') }`);
} else if (val) {
params.push(`${ key }=${ val }`);
}
});
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
const queryResult = useApiQuery(resourceName, {
pathParams,
queryParams,
queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
...options,
},
{ staleTime: page === 1 ? 0 : Infinity, ...options },
);
});
const { data } = queryResult;
const onNextPageClick = useCallback(() => {
......@@ -69,9 +65,13 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
return;
}
const nextPageParams = data.next_page_params;
if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]);
}
setPageParams((prev) => ({
...prev,
[page + 1]: mapValues(nextPageParams, (value) => String(value)) as NextPageParams,
}));
setPage(prev => prev + 1);
const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page + 1);
......@@ -80,53 +80,52 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev + 1);
});
}, [ data?.next_page_params, page, pageParams.length, router, scrollToTop ]);
}, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => {
// returning to the first page
// we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) {
nextPageQuery = omit(router.query, paginationFields, 'page');
nextPageQuery = omit(router.query, resource.paginationFields, 'page');
canGoBackwards.current = true;
} else {
const nextPageParams = { ...pageParams[page - 2] };
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
const nextPageParams = pageParams[page - 1];
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val));
nextPageQuery.page = String(page - 1);
}
router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
});
setHasPagination(true);
}, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
}, [ router, page, resource.paginationFields, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ queryName ] });
queryClient.removeQueries({ queryKey: [ resourceName ] });
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ queryName ] });
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
setPage(1);
setPageParams([ ]);
setPageParams({});
canGoBackwards.current = true;
window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from
// so have to remove it but with some delay :)
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
queryClient.removeQueries({ queryKey: [ resourceName ], type: 'inactive' });
}, 100);
});
setHasPagination(true);
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
}, [ queryClient, resourceName, router, resource.paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
const newQuery = omit(router.query, PAGINATION_FIELDS[queryName], 'page', PAGINATION_FILTERS_FIELDS[queryName]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit(router.query, resource.paginationFields, 'page', resource.filterFields);
if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) {
......@@ -143,12 +142,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
{ shallow: true },
).then(() => {
setPage(1);
setPageParams([ ]);
setPageParams({});
scrollToTop();
});
}, [ queryName, router, scrollToTop, setPageParams, setPage ]);
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0;
const hasPaginationParams = Object.keys(currPageParams || {}).length > 0;
const nextPageParams = data?.next_page_params;
const pagination = {
......@@ -165,12 +164,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
React.useEffect(() => {
if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey });
queryClient.cancelQueries({ queryKey: [ resourceName ] });
setPage(1);
}
// hook should run only when queryName has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ queryName ]);
}, [ resourceName ]);
React.useEffect(() => {
window.setTimeout(() => {
......
......@@ -2,8 +2,7 @@ import * as Sentry from '@sentry/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import { QueryKeys } from 'types/client/accountQueries';
import { resourceKey } from 'lib/api/resources';
import * as cookies from 'lib/cookies';
import useLoginUrl from 'lib/hooks/useLoginUrl';
......@@ -17,7 +16,7 @@ export interface ErrorType {
export default function useRedirectForInvalidAuthToken() {
const queryClient = useQueryClient();
const state = queryClient.getQueryState<unknown, ErrorType>([ QueryKeys.profile ]);
const state = queryClient.getQueryState<unknown, ErrorType>([ resourceKey('user_info') ]);
const errorStatus = state?.error?.error?.status;
const loginUrl = useLoginUrl();
......
import { unparse } from 'papaparse';
export default function saveAsCSV(headerRows: Array<string>, dataRows: Array<Array<string>>, filename: string) {
const csv = unparse([
headerRows,
...dataRows,
]);
const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
......@@ -5,11 +5,11 @@ import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import AppError from 'ui/shared/AppError/AppError';
......@@ -22,8 +22,8 @@ function MyApp({ Component, pageProps }: AppProps) {
queries: {
refetchOnWindowFocus: false,
retry: (failureCount, _error) => {
const error = _error as ErrorType<{ status: number }>;
const status = error?.error?.status;
const error = _error as ResourceError<{ status: number }>;
const status = error?.status || error?.error?.status;
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/api_keys/${ req.query.id }`;
};
const apiKeysHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default apiKeysHandler;
import handler from 'lib/api/handler';
const apiKeysHandler = handler(() => '/account/v1/user/api_keys', [ 'GET', 'POST' ]);
export default apiKeysHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/custom_abis/${ req.query.id }`;
};
const customAbiHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default customAbiHandler;
import handler from 'lib/api/handler';
const customAbiHandler = handler(() => '/account/v1/user/custom_abis', [ 'GET', 'POST' ]);
export default customAbiHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/tags/address/${ req.query.id }`;
};
const addressEditHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default addressEditHandler;
import handler from 'lib/api/handler';
const addressHandler = handler(() => '/account/v1/user/tags/address', [ 'GET', 'POST' ]);
export default addressHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/tags/transaction/${ req.query.id }`;
};
const transactionEditHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default transactionEditHandler;
import handler from 'lib/api/handler';
const transactionHandler = handler(() => '/account/v1/user/tags/transaction', [ 'GET', 'POST' ]);
export default transactionHandler;
import handler from 'lib/api/handler';
const profileHandler = handler(() => '/account/v1/user/info', [ 'GET' ]);
export default profileHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/public_tags/${ req.query.id }`;
};
const publicTagsHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default publicTagsHandler;
import handler from 'lib/api/handler';
const publicKeysHandler = handler(() => '/account/v1/user/public_tags', [ 'GET', 'POST' ]);
export default publicKeysHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/watchlist/${ req.query.id }`;
};
const addressEditHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default addressEditHandler;
import type { NextApiRequest, NextApiResponse } from 'next';
import type { WatchlistAddresses } from 'types/api/account';
import type { Tokenlist } from 'types/api/tokenlist';
import type { TWatchlistItem } from 'types/client/account';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => {
httpLogger(_req, res);
const fetch = fetchFactory(_req);
const url = getUrlWithNetwork(_req, '/api/account/v1/user/watchlist');
const watchlistResponse = await fetch(url, { method: 'GET' });
const watchlistData = await watchlistResponse.json() as WatchlistAddresses;
if (watchlistResponse.status !== 200) {
httpLogger.logger.error({ err: { statusText: 'Watchlist token error', status: 500 }, url: _req.url });
res.status(500).end(watchlistData || 'Unknown error');
return;
}
const data = await Promise.all(watchlistData.map(async item => {
const tokens = await fetch(`/api/?module=account&action=tokenlist&address=${ item.address_hash }`);
const tokensData = await tokens.json() as Tokenlist;
return ({ ...item, tokens_count: Array.isArray(tokensData.result) ? tokensData.result.length : 0 });
}));
res.status(200).json(data);
};
export default watchlistWithTokensHandler;
import handler from 'lib/api/handler';
const watchlistHandler = handler(() => '/account/v1/user/watchlist', [ 'GET', 'POST' ]);
export default watchlistHandler;
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;
import _pick from 'lodash/pick';
import _pickBy from 'lodash/pickBy';
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
if (!_req.url) {
res.status(500).json({ error: 'no url provided' });
return;
}
const response = await fetchFactory(_req, _req.headers['x-endpoint']?.toString())(
_req.url.replace(/^\/node-api\/proxy/, ''),
_pickBy(_pick(_req, [ 'body', 'method' ]), Boolean),
);
res.status(response.status).send(response.body);
};
export default handler;
import type { NextApiRequest } from 'next';
import appConfig from 'configs/app/config';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const { name, from, to } = req.query;
return `/v1/charts/line?name=${ name }${ from ? `&from=${ from }&to=${ to }` : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ], appConfig.statsApi.endpoint);
export default requestHandler;
import appConfig from 'configs/app/config';
import handler from 'lib/api/handler';
const getUrl = () => '/v1/counters';
const requestHandler = handler(getUrl, [ 'GET' ], appConfig.statsApi.endpoint);
export default requestHandler;
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);
}
......@@ -19,7 +19,7 @@ const baseStylePopper = defineStyle({
const baseStyleContent = defineStyle((props) => {
const bg = mode('white', 'gray.900')(props);
const shadowColor = mode('gray.200', 'whiteAlpha.300')(props);
const shadowColor = mode('blackAlpha.200', 'whiteAlpha.300')(props);
return {
[$popperBg.variable]: `colors.${ bg }`,
......
......@@ -15,7 +15,7 @@ const variantSimple = definePartsStyle((props) => {
return {
th: {
border: 0,
color: mode('gray.600', 'whiteAlpha.700')(props),
color: mode('blackAlpha.700', 'whiteAlpha.700')(props),
backgroundColor: mode('blackAlpha.100', 'whiteAlpha.200')(props),
...transitionProps,
},
......
......@@ -24,7 +24,7 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
},
_disabled: {
opacity: 1,
backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
backgroundColor: mode('blackAlpha.200', 'whiteAlpha.200')(props),
borderColor: 'transparent',
cursor: 'not-allowed',
_hover: {
......
import type { AddressParam } from './addressParams';
export interface AddressTag {
address_hash: string;
address: AddressParam;
name: string;
id: string;
}
......@@ -64,7 +65,13 @@ export interface WatchlistAddress {
notification_settings: NotificationSettings;
notification_methods: NotificationMethods;
id: string;
address?: AddressParam;
address: AddressParam;
}
export interface WatchlistTokensResponse {
message: string;
result?: Array<unknown>;
status: string;
}
export interface WatchlistAddressNew {
......@@ -83,10 +90,11 @@ export interface PublicTag {
email: string;
company: string;
addresses: Array<string>;
addresses_with_info: Array<AddressParam>;
additional_comment: string;
}
export type PublicTagNew = Omit<PublicTag, 'id'>
export type PublicTagNew = Omit<PublicTag, 'id' | 'addresses_with_info'>
export type PublicTags = Array<PublicTag>;
......@@ -96,6 +104,7 @@ export interface CustomAbi {
name: string;
id: number;
contract_address_hash: string;
contract_address: AddressParam;
abi: Array<AbiItem>;
}
......
......@@ -80,6 +80,11 @@ export interface AddressCoinBalanceHistoryResponse {
};
}
export type AddressCoinBalanceHistoryChart = Array<{
date: string;
value: string;
}>
export interface AddressBlocksValidatedResponse {
items: Array<Block>;
next_page_params: {
......
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]: [],
};
......@@ -21,11 +21,18 @@ export type GasPrices = {
export type Stats = {
counters: {
totalBlocks: string;
averageBlockTime: string;
totalTransactions: string;
completedTransactions: string;
totalAccounts: string;
totalBlocksAllTime: string;
totalTransactions: string;
totalTokens: string;
totalNativeCoinHolders: string;
totalNativeCoinTransfers: string;
};
}
......
export enum QueryKeys {
addressTags = 'address-tags',
apiKeys = 'api-keys',
customAbis = 'custom-abis',
profile = 'profile',
publicTags = 'public-tags',
transactionTags = 'transaction-tags',
watchlist = 'watchlist',
}
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,21 +5,21 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressBlocksValidatedResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketAlert from 'ui/shared/SocketAlert';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedSkeletonMobile from './blocksValidated/AddressBlocksValidatedSkeletonMobile';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
interface Props {
......@@ -31,8 +31,8 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
const queryClient = useQueryClient();
const query = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/blocks-validated`,
queryName: QueryKeys.addressBlocksValidated,
resourceName: 'address_blocks_validated',
pathParams: { id: addressQuery.data?.hash },
options: {
enabled: Boolean(addressQuery.data),
},
......@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
setSocketAlert(false);
queryClient.setQueryData(
[ QueryKeys.addressBlocksValidated, { page: query.pagination.page } ],
getResourceKey('address_blocks_validated', { pathParams: { id: addressQuery.data?.hash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) {
return;
......@@ -57,7 +57,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
items: [ payload.block, ...prevData.items ],
};
});
}, [ query.pagination.page, queryClient ]);
}, [ addressQuery.data?.hash, queryClient ]);
const channel = useSocketChannel({
topic: `blocks:${ addressQuery.data?.hash.toLowerCase() }`,
......@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/>
</Hide>
<Show below="lg">
<AddressBlocksValidatedSkeletonMobile/>
<SkeletonList/>
</Show>
</>
);
......
......@@ -4,8 +4,8 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -23,8 +23,8 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
const queryClient = useQueryClient();
const coinBalanceQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/coin-balance-history`,
queryName: QueryKeys.addressCoinBalanceHistory,
resourceName: 'address_coin_balance',
pathParams: { id: addressQuery.data?.hash },
options: {
enabled: Boolean(addressQuery.data),
},
......@@ -38,7 +38,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
setSocketAlert(false);
queryClient.setQueryData(
[ QueryKeys.addressCoinBalanceHistory, { page: coinBalanceQuery.pagination.page } ],
getResourceKey('address_coin_balance', { pathParams: { id: addressQuery.data?.hash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) {
return;
......@@ -52,7 +52,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
],
};
});
}, [ coinBalanceQuery.pagination.page, queryClient ]);
}, [ addressQuery.data?.hash, queryClient ]);
const channel = useSocketChannel({
topic: `addresses:${ addressQuery.data?.hash.toLowerCase() }`,
......@@ -69,7 +69,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
return (
<>
{ socketAlert && <SocketAlert mb={ 6 }/> }
<AddressCoinBalanceChart/>
<AddressCoinBalanceChart addressQuery={ addressQuery }/>
<AddressCoinBalanceHistory query={ coinBalanceQuery }/>
</>
);
......
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address as TAddress, AddressCounters, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { Address as TAddress } from 'types/api/address';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -35,24 +33,20 @@ interface Props {
const AddressDetails = ({ addressQuery }: Props) => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const countersQuery = useQuery<unknown, unknown, AddressCounters>(
[ QueryKeys.addressCounters, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/counters`),
{
const countersQuery = useApiQuery('address_counters', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
const tokenBalancesQuery = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/token-balances`),
{
});
const tokenBalancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
});
if (addressQuery.isError) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
......@@ -72,7 +66,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
return (
<Box>
<Flex alignItems="center">
<AddressIcon hash={ addressQuery.data.hash }/>
<AddressIcon address={ addressQuery.data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
</Text>
......
......@@ -4,11 +4,12 @@ import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = `/node-api/addresses/${ ADDRESS_HASH }/internal-transactions`;
const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { id: ADDRESS_HASH });
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH },
......@@ -29,7 +30,5 @@ test('base view +@mobile', async({ mount, page }) => {
{ hooksConfig },
);
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot();
});
......@@ -6,7 +6,6 @@ import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -36,9 +35,8 @@ const AddressInternalTxs = () => {
const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/addresses/${ queryId }/internal-transactions`,
queryName: QueryKeys.addressInternalTxs,
queryIds: queryIdArray,
resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
......
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => {
......@@ -12,9 +9,8 @@ const AddressTokenTransfers = () => {
const hash = router.query.id;
return (
<TokenTransfer
path={ `/node-api/addresses/${ hash }/token-transfers` }
queryName={ QueryKeys.addressTokenTransfers }
queryIds={ castArray(router.query.id) }
resourceName="address_token_transfers"
pathParams={{ id: hash?.toString() }}
baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
/>
......
......@@ -4,10 +4,11 @@ import React from 'react';
import { base as txMock } from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs';
const API_URL = '/node-api/addresses/0xd789a607CEac2f0E14867de4EB15b15C9FFB5859/transactions';
const API_URL = buildApiUrl('address_txs', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const hooksConfig = {
router: {
......@@ -29,7 +30,5 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -5,7 +5,6 @@ import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -29,9 +28,8 @@ const AddressTxs = () => {
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ router.query.id }/transactions`,
queryName: QueryKeys.addressTxs,
queryIds: castArray(router.query.id),
resourceName: 'address_txs',
pathParams: { id: castArray(router.query.id)[0] },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
......
......@@ -8,7 +8,7 @@ import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
......@@ -21,7 +21,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
const totalReward = getBlockTotalReward(props);
return (
<AccountListItemMobile rowGap={ 2 }>
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Link href={ blockUrl } fontWeight="700">{ props.height }</Link>
<Text variant="secondary">{ timeAgo }</Text>
......@@ -39,7 +39,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
<Text fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text>
</Flex>
</AccountListItemMobile>
</ListItemMobile>
);
};
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AddressBlocksValidatedSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex justifyContent="space-between" w="100%" h={ 6 }>
<Skeleton w="100px"/>
<Skeleton w="100px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="40px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="70px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="100px"/>
<Skeleton w="120px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default AddressBlocksValidatedSkeletonMobile;
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import React from 'react';
const AddressCoinBalanceChart = () => {
// chart will be added after stats feature is finalized
return <Box p={ 4 } borderColor="gray.200" borderRadius="md" borderWidth="1px">Here will be coin balance chart</Box>;
import type { Address } from 'types/api/address';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import ChartWidget from 'ui/shared/chart/ChartWidget';
interface Props {
addressQuery: UseQueryResult<Address>;
}
const AddressCoinBalanceChart = ({ addressQuery }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { id: addressQuery.data?.hash },
queryOptions: { enabled: Boolean(addressQuery.data?.hash) },
});
const items = React.useMemo(() => data?.map(({ date, value }) => ({
date: new Date(date),
value: BigNumber(value).div(10 ** appConfig.network.currency.decimals).toNumber(),
})), [ data ]);
return (
<ChartWidget
chartHeight="200px"
title="Balances"
items={ items }
isLoading={ isLoading || isError }
/>
);
};
export default AddressCoinBalanceChart;
export default React.memo(AddressCoinBalanceChart);
......@@ -9,11 +9,11 @@ import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressCoinBalanceListItem from './AddressCoinBalanceListItem';
import AddressCoinBalanceSkeletonMobile from './AddressCoinBalanceSkeletonMobile';
import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem';
interface Props {
......@@ -33,7 +33,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<SkeletonTable columns={ [ '25%', '25%', '25%', '25%', '120px' ] }/>
</Hide>
<Show below="lg">
<AddressCoinBalanceSkeletonMobile/>
<SkeletonList/>
</Show>
</>
);
......
......@@ -8,9 +8,9 @@ import appConfig from 'configs/app/config';
import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
......@@ -23,7 +23,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
return (
<AccountListItemMobile rowGap={ 2 }>
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).toFixed(8) } { appConfig.network.currency.symbol }</Text>
<Stat flexGrow="0">
......@@ -51,7 +51,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
<Text fontWeight={ 500 } flexShrink={ 0 }>Age</Text>
<Text variant="secondary">{ timeAgo }</Text>
</Flex>
</AccountListItemMobile>
</ListItemMobile>
);
};
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AddressCoinBalanceSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex justifyContent="space-between" w="100%" h={ 6 }>
<Skeleton w="170px"/>
<Skeleton w="120px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="80px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="150px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="30px"/>
<Skeleton w="60px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default AddressCoinBalanceSkeletonMobile;
......@@ -3,9 +3,9 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import CurrencyValue from 'ui/shared/CurrencyValue';
......@@ -26,7 +26,8 @@ const AddressBalance = ({ data }: Props) => {
}
setLastBlockNumber(blockNumber);
queryClient.setQueryData([ QueryKeys.address, data.hash ], (prevData: Address | undefined) => {
const queryKey = getResourceKey('address', { pathParams: { id: data.hash } });
queryClient.setQueryData(queryKey, (prevData: Address | undefined) => {
if (!prevData) {
return;
}
......
......@@ -8,14 +8,13 @@ const AddressDetailsSkeleton = () => {
<Box>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 }/>
<Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex align="center" columnGap={ 4 } mt={ 8 }>
<Skeleton h={ 6 } w="200px"/>
<Skeleton h={ 6 } w="80px"/>
<Skeleton h={ 6 } w="200px" borderRadius="full"/>
<Skeleton h={ 6 } w="80px" borderRadius="full"/>
</Flex>
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }>
<DetailsSkeletonRow w="30%"/>
......
import { Icon, chakra, Tooltip, IconButton, useDisclosure } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { UserInfo } from 'types/api/account';
import type { TWatchlist } from 'types/client/account';
import { QueryKeys as AccountQueryKeys } from 'types/client/accountQueries';
import { QueryKeys } from 'types/client/queries';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useFetch from 'lib/hooks/useFetch';
import { resourceKey } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
......@@ -27,19 +25,12 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const deleteModalProps = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const fetch = useFetch();
const profileData = queryClient.getQueryData<UserInfo>([ AccountQueryKeys.profile ]);
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl();
const watchListQuery = useQuery<unknown, unknown, TWatchlist>(
[ AccountQueryKeys.watchlist ],
async() => fetch('/node-api/account/watchlist'),
{
enabled: isAdded,
},
);
const watchListQuery = useApiQuery('watchlist', { queryOptions: { enabled: isAdded } });
const handleClick = React.useCallback(() => {
if (!isAuth) {
......@@ -50,7 +41,8 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
await queryClient.refetchQueries({ queryKey: [ QueryKeys.address, router.query.id ] });
const queryKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } });
await queryClient.refetchQueries({ queryKey });
addModalProps.onClose();
}, [ addModalProps, queryClient, router.query.id ]);
......
......@@ -8,11 +8,11 @@ import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......@@ -38,7 +38,7 @@ const TxInternalsListItem = ({
const isIn = Boolean(currentAddress && currentAddress === to?.hash);
return (
<AccountListItemMobile rowGap={ 3 }>
<ListItemMobile rowGap={ 3 }>
<Flex>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
......@@ -53,7 +53,7 @@ const TxInternalsListItem = ({
</HStack>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ from.hash }/>
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
{ (isIn || isOut) ?
......@@ -61,7 +61,7 @@ const TxInternalsListItem = ({
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ toData.hash }/>
<AddressIcon address={ toData }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash }/>
</Address>
</Box>
......@@ -71,7 +71,7 @@ const TxInternalsListItem = ({
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text>
</HStack>
</AccountListItemMobile>
</ListItemMobile>
);
};
......
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
......
......@@ -61,7 +61,7 @@ const AddressIntTxsTableItem = ({
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/>
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
......@@ -73,7 +73,7 @@ const AddressIntTxsTableItem = ({
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ toData.hash }/>
<AddressIcon address={ toData }/>
<AddressLink hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
......
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element => {
const router = useRouter();
const fetch = useFetch();
const { data } = useQuery(
[ QueryKeys.address, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const { data } = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
});
if (!data) {
return <div/>;
......
......@@ -6,13 +6,14 @@ import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png';
const TOKENS_API_URL = '/node-api/addresses/1/token-balances';
const ADDRESS_API_URL = '/node-api/addresses/1';
const TOKENS_API_URL = buildApiUrl('address_token_balances', { id: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const hooksConfig = {
router: {
query: { id: '1' },
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -18,21 +17,20 @@ import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressQueryData = queryClient.getQueryData<Address>([ QueryKeys.address, router.query.id ]);
const addressResourceKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } });
const { data, isError, isLoading, refetch } = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, addressQueryData?.hash ],
async() => await fetch(`/node-api/addresses/${ addressQueryData?.hash }/token-balances`),
{
enabled: Boolean(addressQueryData),
},
);
const balancesIsFetching = useIsFetching({ queryKey: [ QueryKeys.addressTokenBalances, addressQueryData?.hash ] });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useApiQuery('address_token_balances', {
pathParams: { id: addressQueryData?.hash },
queryOptions: { enabled: Boolean(addressQueryData) },
});
const balancesResourceKey = getResourceKey('address_token_balances', { pathParams: { id: addressQueryData?.hash } });
const balancesIsFetching = useIsFetching({ queryKey: balancesResourceKey });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
......
......@@ -12,11 +12,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { ResourceErrorAccount } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
......@@ -40,7 +40,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
name: data?.name || '',
},
});
const fetch = useFetch();
const apiFetch = useApiFetch();
const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
......@@ -48,17 +48,20 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const body = { name: data.name };
if (!data.token) {
return fetch('/node-api/account/api-keys', { method: 'POST', body });
return apiFetch('api_keys', { fetchParams: { method: 'POST', body } });
}
return fetch(`/node-api/account/api-keys/${ data.token }`, { method: 'PUT', body });
return apiFetch('api_keys', {
pathParams: { id: data.token },
fetchParams: { method: 'PUT', body },
});
};
const mutation = useMutation(updateApiKey, {
onSuccess: async(data) => {
const response = data as unknown as ApiKey;
queryClient.setQueryData([ QueryKeys.apiKeys ], (prevData: ApiKeys | undefined) => {
queryClient.setQueryData([ resourceKey('api_keys') ], (prevData: ApiKeys | undefined) => {
const isExisting = prevData && prevData.some((item) => item.api_key === response.api_key);
if (isExisting) {
......@@ -76,11 +79,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onClose();
},
onError: (e: ErrorType<ApiKeyErrors>) => {
if (e?.error?.name) {
setError('name', { type: 'custom', message: getErrorMessage(e.error, 'name') });
} else if (e?.error?.identity_id) {
setError('name', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') });
onError: (error: ResourceErrorAccount<ApiKeyErrors>) => {
const errorMap = error.payload?.errors;
if (errorMap?.name) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else {
setAlertVisible(true);
}
......
......@@ -2,8 +2,8 @@ import React, { useCallback } from 'react';
import type { ApiKey } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import ApiKeySnippet from 'ui/shared/ApiKeySnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
......@@ -23,10 +23,10 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
return (
<AccountListItemMobile>
<ListItemMobile>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile>
</ListItemMobile>
);
};
......
......@@ -3,9 +3,9 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -16,14 +16,17 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const fetch = useFetch();
const apiFetch = useApiFetch();
const mutationFn = useCallback(() => {
return fetch(`/node-api/account/api-keys/${ data.api_key }`, { method: 'DELETE' });
}, [ data.api_key, fetch ]);
return apiFetch('api_keys', {
pathParams: { id: data.api_key },
fetchParams: { method: 'DELETE' },
});
}, [ data.api_key, apiFetch ]);
const onSuccess = useCallback(async() => {
queryClient.setQueryData([ QueryKeys.apiKeys ], (prevData: ApiKeys | undefined) => {
queryClient.setQueryData([ resourceKey('api_keys') ], (prevData: ApiKeys | undefined) => {
return prevData?.filter((item) => item.api_key !== data.api_key);
});
}, [ data, queryClient ]);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment