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

Merge pull request #300 from blockscout/client-api-calls

call api from client side (part 1)
parents 3ae3b14c fa291707
......@@ -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)) || [],
......
async function rewrites() {
return [
{ source: '/node-api/:slug*', destination: '/api/:slug*' },
{ source: '/proxy/:slug*', destination: '/api/proxy' },
].filter(Boolean);
}
......
......@@ -307,6 +307,7 @@ frontend:
- "/"
prefix:
# - "/(apps|auth/profile|account)"
- "/account"
- "/apps"
- "/_next"
- "/node-api"
......
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>,
queryParams?: Record<string, string | 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 ? '/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, value);
});
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 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,
},
// DEPRECATED
old_api: {
path: '/api',
},
};
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 }>
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>;
queryParams?: Record<string, string | 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 { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress } from 'types/api/account';
import type { Stats, Charts } from 'types/api/stats';
import type { RESOURCES, ResourceError } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch';
interface Params<R extends keyof typeof RESOURCES> extends ApiFetchParams {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
}
export default function useApiQuery<R extends keyof typeof RESOURCES>(
resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R> = {},
) {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError, ResourcePayload<R>>(
pathParams || queryParams ? [ resource, { ...pathParams, ...queryParams } ] : [ resource ],
async() => {
return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams });
}, queryOptions);
}
export type ResourcePayload<Q extends keyof typeof RESOURCES> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'stats_counters' ? Stats :
Q extends 'stats_charts' ? Charts :
never;
......@@ -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',
......
......@@ -4,27 +4,29 @@ 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) :
undefined,
headers: {
...params?.headers,
...(token ? { 'x-csrf-token': token } : {}),
},
};
return fetch(path, reqParams).then(response => {
......@@ -37,7 +39,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)),
},
});
}
......@@ -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();
......
......@@ -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 _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(/^\/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;
......@@ -67,6 +67,12 @@ export interface WatchlistAddress {
address?: AddressParam;
}
export interface WatchlistTokensResponse {
message: string;
result?: Array<unknown>;
status: string;
}
export interface WatchlistAddressNew {
addressName: string;
notificationSettings: NotificationSettings;
......
export enum QueryKeys {
addressTags = 'address-tags',
apiKeys = 'api-keys',
customAbis = 'custom-abis',
profile = 'profile',
publicTags = 'public-tags',
transactionTags = 'transaction-tags',
watchlist = 'watchlist',
}
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 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 +26,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) {
......
......@@ -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);
}
......
......@@ -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 ]);
......
......@@ -13,10 +13,10 @@ import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
......@@ -37,7 +37,7 @@ const BlockDetails = () => {
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, Block>(
const { data, isLoading, isError, error } = useQuery<unknown, ResourceError<{ status: number }>, Block>(
[ QueryKeys.block, router.query.id ],
async() => await fetch(`/node-api/blocks/${ router.query.id }`),
{
......
......@@ -12,11 +12,11 @@ import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } 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 { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
......@@ -46,16 +46,19 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
const queryClient = useQueryClient();
const fetch = useFetch();
const apiFetch = useApiFetch();
const customAbiKey = (data: Inputs & { id?: number }) => {
const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi };
if (!data.id) {
return fetch<CustomAbi, CustomAbiErrors>('/node-api/account/custom-abis', { method: 'POST', body });
return apiFetch('custom_abi', { fetchParams: { method: 'POST', body } });
}
return fetch<CustomAbi, CustomAbiErrors>(`/node-api/account/custom-abis/${ data.id }`, { method: 'PUT', body });
return apiFetch('custom_abi', {
pathParams: { id: String(data.id) },
fetchParams: { method: 'PUT', body },
});
};
const formBackgroundColor = useColorModeValue('white', 'gray.900');
......@@ -63,7 +66,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const mutation = useMutation(customAbiKey, {
onSuccess: (data) => {
const response = data as unknown as CustomAbi;
queryClient.setQueryData([ QueryKeys.customAbis ], (prevData: CustomAbis | undefined) => {
queryClient.setQueryData([ resourceKey('custom_abi') ], (prevData: CustomAbis | undefined) => {
const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) {
......@@ -81,13 +84,14 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onClose();
},
onError: (e: ErrorType<CustomAbiErrors>) => {
if (e?.error?.address_hash || e?.error?.name || e?.error?.abi) {
e?.error?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(e.error, 'address_hash') });
e?.error?.name && setError('name', { type: 'custom', message: getErrorMessage(e.error, 'name') });
e?.error?.abi && setError('abi', { type: 'custom', message: getErrorMessage(e.error, 'abi') });
} else if (e?.error?.identity_id) {
setError('contract_address_hash', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') });
onError: (error: ResourceErrorAccount<CustomAbiErrors>) => {
const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) {
errorMap?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
errorMap?.abi && setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') });
} else if (errorMap?.identity_id) {
setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else {
setAlertVisible(true);
}
......
......@@ -3,8 +3,9 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -16,13 +17,17 @@ type Props = {
const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const apiFetch = useApiFetch();
const mutationFn = useCallback(() => {
return fetch(`/node-api/account/custom-abis/${ data.id }`, { method: 'DELETE' });
}, [ data ]);
return apiFetch('custom_abi', {
pathParams: { id: String(data.id) },
fetchParams: { method: 'DELETE' },
});
}, [ apiFetch, data.id ]);
const onSuccess = useCallback(async() => {
queryClient.setQueryData([ QueryKeys.customAbis ], (prevData: CustomAbis | undefined) => {
queryClient.setQueryData([ resourceKey('custom_abi') ], (prevData: CustomAbis | undefined) => {
return prevData?.filter((item) => item.id !== data.id);
});
}, [ data, queryClient ]);
......
import { Box, Button, Stack, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { ApiKey } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { space } from 'lib/html-entities';
......@@ -26,13 +24,12 @@ const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
useRedirectForInvalidAuthToken();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isLoading, isError } = useQuery<unknown, unknown, ApiKeys>([ QueryKeys.apiKeys ], async() => await fetch('/node-api/account/api-keys'));
const { data, isLoading, isError } = useApiQuery('api_keys');
const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data);
......
import { Box, Button, HStack, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { CustomAbi } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
......@@ -23,14 +21,12 @@ const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
useRedirectForInvalidAuthToken();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isLoading, isError } = useQuery<unknown, unknown, CustomAbis>([ QueryKeys.customAbis ], async() =>
await fetch('/node-api/account/custom-abis'));
const { data, isLoading, isError } = useApiQuery('custom_abi');
const onEditClick = useCallback((data: CustomAbi) => {
setCustomAbiModalData(data);
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code, Flex, Box } from '@chakra-ui/react';
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Code, Flex, Box } from '@chakra-ui/react';
import * as Sentry from '@sentry/react';
import type { ChangeEvent } from 'react';
import React from 'react';
......@@ -47,8 +47,6 @@ const Login = () => {
});
}, [ toast, token ]);
const prodUrl = 'https://blockscout.com/poa/core';
const handleNumIncrement = React.useCallback(() => {
for (let index = 0; index < 5; index++) {
setNum(5);
......@@ -58,19 +56,15 @@ const Login = () => {
return (
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="1000px">
<PageTitle text="Vercel page"/>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
<PageTitle text="Login page 😂"/>
{ isFormVisible && (
<>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
!!! Temporary solution for authentication on localhost !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
To Sign in go to production instance first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
......@@ -80,6 +74,10 @@ const Login = () => {
</>
) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
</VStack>
</Page>
);
......
......@@ -2,10 +2,11 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { WatchlistAddress, WatchlistTokensResponse } from 'types/api/account';
import type { TWatchlist, TWatchlistItem } from 'types/client/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 useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
......@@ -20,14 +21,38 @@ import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => {
const { data, isLoading, isError } =
useQuery<unknown, unknown, TWatchlist>([ QueryKeys.watchlist ], async() => fetch('/node-api/account/watchlist/get-with-tokens'));
const apiFetch = useApiFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, TWatchlist>([ resourceKey('watchlist') ], async() => {
try {
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
if (!Array.isArray(watchlistAddresses)) {
throw Error();
}
const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
if (!address?.hash) {
return Promise.resolve(0);
}
return apiFetch<'old_api', WatchlistTokensResponse>('old_api', { queryParams: { address: address.hash, module: 'account', action: 'tokenlist' } })
.then((response) => {
if ('result' in response && Array.isArray(response.result)) {
return response.result.length;
}
return 0;
});
}));
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] }));
} catch (error) {
return error;
}
});
const queryClient = useQueryClient();
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
useRedirectForInvalidAuthToken();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
......@@ -44,7 +69,7 @@ const WatchList: React.FC = () => {
}, [ addressModalProps ]);
const onAddOrEditSuccess = useCallback(async() => {
await queryClient.refetchQueries([ QueryKeys.watchlist ]);
await queryClient.refetchQueries([ resourceKey('watchlist') ]);
setAddressModalData(undefined);
addressModalProps.onClose();
}, [ addressModalProps, queryClient ]);
......@@ -60,7 +85,7 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]);
const onDeleteSuccess = useCallback(async() => {
queryClient.setQueryData([ QueryKeys.watchlist ], (prevData: TWatchlist | undefined) => {
queryClient.setQueryData([ resourceKey('watchlist') ], (prevData: TWatchlist | undefined) => {
return prevData?.filter((item) => item.id !== deleteModalData?.id);
});
}, [ deleteModalData?.id, queryClient ]);
......
......@@ -9,11 +9,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } 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 { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput';
......@@ -32,7 +32,7 @@ type Inputs = {
}
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch();
const apiFetch = useApiFetch();
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched',
......@@ -54,24 +54,28 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const isEdit = data?.id;
if (isEdit) {
return fetch(`/node-api/account/private-tags/address/${ data.id }`, { method: 'PUT', body });
return apiFetch('private_tags_address', {
pathParams: { id: data.id },
fetchParams: { method: 'PUT', body },
});
}
return fetch('/node-api/account/private-tags/address', { method: 'POST', body });
return apiFetch('private_tags_address', { fetchParams: { method: 'POST', body } });
}, {
onError: (e: ErrorType<AddressTagErrors>) => {
onError: (error: ResourceErrorAccount<AddressTagErrors>) => {
setPending(false);
if (e?.error?.address_hash || e?.error?.name) {
e?.error?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(e.error, 'address_hash') });
e?.error?.name && setError('tag', { type: 'custom', message: getErrorMessage(e.error, 'name') });
} else if (e?.error?.identity_id) {
setError('address', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') });
const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name) {
errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) {
setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else {
setAlertVisible(true);
}
},
onSuccess: () => {
queryClient.refetchQueries([ QueryKeys.addressTags ]).then(() => {
queryClient.refetchQueries([ resourceKey('private_tags_address') ]).then(() => {
onClose();
setPending(false);
});
......
......@@ -3,9 +3,9 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } 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 = {
......@@ -20,19 +20,23 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const id = data.id;
const queryClient = useQueryClient();
const fetch = useFetch();
const apiFetch = useApiFetch();
const mutationFn = useCallback(() => {
return fetch(`/node-api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' });
}, [ fetch, type, id ]);
const resourceName = type === 'address' ? 'private_tags_address' : 'private_tags_tx';
return apiFetch(resourceName, {
pathParams: { id: data.id },
fetchParams: { method: 'DELETE' },
});
}, [ type, apiFetch, data.id ]);
const onSuccess = useCallback(async() => {
if (type === 'address') {
queryClient.setQueryData([ QueryKeys.addressTags ], (prevData: AddressTags | undefined) => {
queryClient.setQueryData([ resourceKey('private_tags_address') ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id);
});
} else {
queryClient.setQueryData([ QueryKeys.transactionTags ], (prevData: TransactionTags | undefined) => {
queryClient.setQueryData([ resourceKey('private_tags_tx') ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
});
}
......
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { AddressTag } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -18,13 +16,11 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => {
const { data: addressTagsData, isLoading, isError } =
useQuery<unknown, unknown, AddressTags>([ QueryKeys.addressTags ], async() => fetch('/node-api/account/private-tags/address'), { refetchOnMount: false });
const { data: addressTagsData, isLoading, isError } = useApiQuery('private_tags_address', { queryOptions: { refetchOnMount: false } });
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
......
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { TransactionTag } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -18,16 +16,11 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => {
const { data: transactionTagsData, isLoading, isError } =
useQuery<unknown, unknown, TransactionTags>(
[ QueryKeys.transactionTags ],
async() => fetch('/node-api/account/private-tags/transaction'), { refetchOnMount: false },
);
const { data: transactionTagsData, isLoading, isError } = useApiQuery('private_tags_tx', { queryOptions: { refetchOnMount: false } });
const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
......
......@@ -9,11 +9,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } 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 { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction';
import TagInput from 'ui/shared/TagInput';
import TransactionInput from 'ui/shared/TransactionInput';
......@@ -44,7 +44,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
});
const queryClient = useQueryClient();
const fetch = useFetch();
const apiFetch = useApiFetch();
const { mutate } = useMutation((formData: Inputs) => {
const body = {
......@@ -54,24 +54,28 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const isEdit = data?.id;
if (isEdit) {
return fetch(`/node-api/account/private-tags/transaction/${ data.id }`, { method: 'PUT', body });
return apiFetch('private_tags_tx', {
pathParams: { id: data.id },
fetchParams: { method: 'PUT', body },
});
}
return fetch('/node-api/account/private-tags/transaction', { method: 'POST', body });
return apiFetch('private_tags_tx', { fetchParams: { method: 'POST', body } });
}, {
onError: (e: ErrorType<TransactionTagErrors>) => {
onError: (error: ResourceErrorAccount<TransactionTagErrors>) => {
setPending(false);
if (e?.error?.tx_hash || e?.error?.name) {
e?.error?.tx_hash && setError('transaction', { type: 'custom', message: getErrorMessage(e.error, 'tx_hash') });
e?.error?.name && setError('tag', { type: 'custom', message: getErrorMessage(e.error, 'name') });
} else if (e?.error?.identity_id) {
setError('transaction', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') });
const errorMap = error.payload?.errors;
if (errorMap?.tx_hash || errorMap?.name) {
errorMap?.tx_hash && setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'tx_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.identity_id) {
setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else {
setAlertVisible(true);
}
},
onSuccess: () => {
queryClient.refetchQueries([ QueryKeys.transactionTags ]).then(() => {
queryClient.refetchQueries([ resourceKey('private_tags_tx') ]).then(() => {
onClose();
setPending(false);
});
......
......@@ -4,9 +4,9 @@ import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } 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 = {
......@@ -23,17 +23,20 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tags = data.tags.split(';');
const queryClient = useQueryClient();
const fetch = useFetch();
const apiFetch = useApiFetch();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const deleteApiKey = useCallback(() => {
const body = { remove_reason: reason };
return fetch(`/node-api/account/public-tags/${ data.id }`, { method: 'DELETE', body });
}, [ data.id, fetch, reason ]);
return apiFetch('public_tags', {
pathParams: { id: String(data.id) },
fetchParams: { method: 'DELETE', body },
});
}, [ data.id, apiFetch, reason ]);
const onSuccess = useCallback(async() => {
onDeleteSuccess();
queryClient.setQueryData([ QueryKeys.publicTags ], (prevData: PublicTags | undefined) => {
queryClient.setQueryData([ resourceKey('public_tags') ], (prevData: PublicTags | undefined) => {
return prevData?.filter((item) => item.id !== data.id);
});
}, [ queryClient, data, onDeleteSuccess ]);
......
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import type { PublicTag } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
......@@ -25,10 +23,8 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ QueryKeys.publicTags ], async() =>
await fetch('/node-api/account/public-tags'));
const { data, isLoading, isError } = useApiQuery('public_tags');
const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined);
......
......@@ -13,11 +13,11 @@ import type { FieldError, Path, SubmitHandler } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } 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 { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
......@@ -58,7 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient();
const fetch = useFetch();
const apiFetch = useApiFetch();
const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
......@@ -100,17 +100,20 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
};
if (!data?.id) {
return fetch<PublicTag, PublicTagErrors>('/node-api/account/public-tags', { method: 'POST', body });
return apiFetch('public_tags', { fetchParams: { method: 'POST', body } });
}
return fetch<PublicTag, PublicTagErrors>(`/node-api/account/public-tags/${ data.id }`, { method: 'PUT', body });
return apiFetch('public_tags', {
pathParams: { id: String(data.id) },
fetchParams: { method: 'PUT', body },
});
};
const mutation = useMutation(updatePublicTag, {
onSuccess: async(data) => {
const response = data as unknown as PublicTag;
queryClient.setQueryData([ QueryKeys.publicTags ], (prevData: PublicTags | undefined) => {
queryClient.setQueryData([ resourceKey('public_tags') ], (prevData: PublicTags | undefined) => {
const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) {
......@@ -128,13 +131,14 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
changeToDataScreen(true);
},
onError: (e: ErrorType<PublicTagErrors>) => {
if (e.error?.full_name || e.error?.email || e.error?.tags || e.error?.addresses || e.error?.additional_comment) {
e.error?.full_name && setError('fullName', { type: 'custom', message: getErrorMessage(e.error, 'full_name') });
e.error?.email && setError('email', { type: 'custom', message: getErrorMessage(e.error, 'email') });
e.error?.tags && setError('tags', { type: 'custom', message: getErrorMessage(e.error, 'tags') });
e.error?.addresses && setError('addresses.0.address', { type: 'custom', message: getErrorMessage(e.error, 'addresses') });
e.error?.additional_comment && setError('comment', { type: 'custom', message: getErrorMessage(e.error, 'additional_comment') });
onError: (error: ResourceErrorAccount<PublicTagErrors>) => {
const errorMap = error.payload?.errors;
if (errorMap?.full_name || errorMap?.email || errorMap?.tags || errorMap?.addresses || errorMap?.additional_comment) {
errorMap?.full_name && setError('fullName', { type: 'custom', message: getErrorMessage(errorMap, 'full_name') });
errorMap?.email && setError('email', { type: 'custom', message: getErrorMessage(errorMap, 'email') });
errorMap?.tags && setError('tags', { type: 'custom', message: getErrorMessage(errorMap, 'tags') });
errorMap?.addresses && setError('addresses.0.address', { type: 'custom', message: getErrorMessage(errorMap, 'addresses') });
errorMap?.additional_comment && setError('comment', { type: 'custom', message: getErrorMessage(errorMap, 'additional_comment') });
} else {
setAlertVisible(true);
}
......
......@@ -2,8 +2,6 @@ import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import AppError from 'ui/shared/AppError/AppError';
......@@ -27,7 +25,7 @@ const Page = ({
}: Props) => {
const fetch = useFetch();
useQuery<unknown, unknown, unknown>([ QueryKeys.csrf ], async() => await fetch('/node-api/account/csrf'), {
useQuery([ 'csrf' ], async() => await fetch('/node-api/csrf'), {
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
......@@ -33,7 +34,7 @@ test.describe('auth', () => {
});
extendedTest('+@dark-mode', async({ mount, page }) => {
await page.route('/node-api/account/profile', (route) => route.fulfill({
await page.route('/proxy/poa/core' + RESOURCES.user_info.path, (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
......
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
......@@ -29,7 +30,7 @@ test.describe('auth', () => {
});
extendedTest('base view', async({ mount, page }) => {
await page.route('/node-api/account/profile', (route) => route.fulfill({
await page.route('/proxy/poa/core' + RESOURCES.user_info.path, (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
......
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Charts } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import type { StatsIntervalIds } from 'types/client/stats';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import ChartWidget from '../shared/chart/ChartWidget';
import { STATS_INTERVALS } from './constants';
......@@ -22,19 +19,18 @@ function formatDate(date: Date) {
}
const ChartWidgetContainer = ({ id, title, description, interval }: Props) => {
const fetch = useFetch();
const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const url = `/node-api/stats/charts?name=${ id }${ startDate ? `&from=${ startDate }&to=${ endDate }` : '' }`;
const { data, isLoading } = useQuery<unknown, unknown, Charts>(
[ QueryKeys.charts, id, startDate ],
async() => await fetch(url),
);
const { data, isLoading } = useApiQuery('stats_charts', {
queryParams: {
name: id,
from: startDate,
to: endDate,
},
});
const items = data?.chart
.map((item) => {
......
import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton';
......@@ -13,12 +9,7 @@ import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 8;
const NumberWidgetsList = () => {
const fetch = useFetch();
const { data, isLoading } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
async() => await fetch(`/node-api/stats/counters`),
);
const { data, isLoading } = useApiQuery('stats_counters');
return (
<Grid
......
......@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account';
import type { ResourceErrorAccount } 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 { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import CheckboxInput from 'ui/shared/CheckboxInput';
......@@ -83,7 +83,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
mode: 'onTouched',
});
const fetch = useFetch();
const apiFetch = useApiFetch();
function updateWatchlist(formData: Inputs) {
const body = {
......@@ -96,11 +96,14 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
};
if (!isAdd && data) {
// edit address
return fetch<TWatchlistItem, WatchlistErrors>(`/node-api/account/watchlist/${ data.id }`, { method: 'PUT', body });
return apiFetch('watchlist', {
pathParams: { id: data?.id || '' },
fetchParams: { method: 'PUT', body },
});
} else {
// add address
return fetch<TWatchlistItem, WatchlistErrors>('/node-api/account/watchlist', { method: 'POST', body });
return apiFetch('watchlist', { fetchParams: { method: 'POST', body } });
}
}
......@@ -109,13 +112,14 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
await onSuccess();
setPending(false);
},
onError: (e: ErrorType<WatchlistErrors>) => {
onError: (error: ResourceErrorAccount<WatchlistErrors>) => {
setPending(false);
if (e?.error?.address_hash || e?.error?.name) {
e?.error?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(e.error, 'address_hash') });
e?.error?.name && setError('tag', { type: 'custom', message: getErrorMessage(e.error, 'name') });
} else if (e?.error?.watchlist_id) {
setError('address', { type: 'custom', message: getErrorMessage(e.error, 'watchlist_id') });
const errorMap = error.payload?.errors;
if (errorMap?.address_hash || errorMap?.name) {
errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (errorMap?.watchlist_id) {
setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'watchlist_id') });
} else {
setAlertVisible(true);
}
......
......@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import useFetch from 'lib/hooks/useFetch';
import useApiFetch from 'lib/api/useApiFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import DeleteModal from 'ui/shared/DeleteModal';
......@@ -16,11 +16,14 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data }) => {
const isMobile = useIsMobile();
const fetch = useFetch();
const apiFetch = useApiFetch();
const mutationFn = useCallback(() => {
return fetch(`/node-api/account/watchlist/${ data?.id }`, { method: 'DELETE' });
}, [ data?.id, fetch ]);
return apiFetch('custom_abi', {
pathParams: { id: data.id },
fetchParams: { method: 'DELETE' },
});
}, [ data?.id, apiFetch ]);
const address = data?.address_hash;
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import useFetch from 'lib/hooks/useFetch';
import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
......@@ -29,7 +29,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
const errorToast = useToast();
const fetch = useFetch();
const apiFetch = useApiFetch();
const showErrorToast = useCallback(() => {
errorToast({
......@@ -61,7 +61,10 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
return fetch(`/node-api/account/watchlist/${ item.id }`, { method: 'PUT', body });
return apiFetch('watchlist', {
pathParams: { id: item.id },
fetchParams: { method: 'PUT', body },
});
}, {
onError: () => {
showErrorToast();
......
......@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import useFetch from 'lib/hooks/useFetch';
import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
......@@ -34,7 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
const errorToast = useToast();
const fetch = useFetch();
const apiFetch = useApiFetch();
const showErrorToast = useCallback(() => {
errorToast({
......@@ -66,7 +66,10 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
return fetch(`/node-api/account/watchlist/${ item.id }`, { method: 'PUT', body });
return apiFetch('watchlist', {
pathParams: { id: item.id },
fetchParams: { method: 'PUT', body },
});
}, {
onError: () => {
showErrorToast();
......
......@@ -7769,6 +7769,11 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
......
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