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({ ...@@ -90,12 +90,14 @@ const config = Object.freeze({
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true', adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true',
}, },
api: { api: {
host: apiHost,
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com', endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com', socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''), basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
}, },
statsApi: { statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST), endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
basePath: '',
}, },
homepage: { homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [], charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
......
async function rewrites() { async function rewrites() {
return [ return [
{ source: '/node-api/:slug*', destination: '/api/:slug*' }, { source: '/node-api/:slug*', destination: '/api/:slug*' },
{ source: '/proxy/:slug*', destination: '/api/proxy' },
].filter(Boolean); ].filter(Boolean);
} }
......
...@@ -307,6 +307,7 @@ frontend: ...@@ -307,6 +307,7 @@ frontend:
- "/" - "/"
prefix: prefix:
# - "/(apps|auth/profile|account)" # - "/(apps|auth/profile|account)"
- "/account"
- "/apps" - "/apps"
- "/_next" - "/_next"
- "/node-api" - "/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( ...@@ -13,10 +13,12 @@ export default function fetchFactory(
apiEndpoint: string = appConfig.api.endpoint, apiEndpoint: string = appConfig.api.endpoint,
) { ) {
return function fetch(path: string, init?: RequestInit): Promise<Response> { return function fetch(path: string, init?: RequestInit): Promise<Response> {
const csrfToken = _req.headers['x-csrf-token']?.toString();
const headers = { const headers = {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
...(csrfToken ? { 'x-csrf-token': csrfToken } : {}),
}; };
const url = new URL(path, apiEndpoint); 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() { ...@@ -55,6 +55,7 @@ function makePolicyMap() {
'connect-src': [ 'connect-src': [
KEY_WORDS.SELF, KEY_WORDS.SELF,
...MAIN_DOMAINS,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason // webpack hmr in safari doesn't recognize localhost as 'self' for some reason
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '', appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
...@@ -62,7 +63,10 @@ function makePolicyMap() { ...@@ -62,7 +63,10 @@ function makePolicyMap() {
// client error monitoring // client error monitoring
'sentry.io', '*.sentry.io', 'sentry.io', '*.sentry.io',
// API
appConfig.api.endpoint,
appConfig.api.socket, appConfig.api.socket,
appConfig.statsApi.endpoint,
// ad // ad
'request-global.czilladx.com', 'request-global.czilladx.com',
......
...@@ -4,27 +4,29 @@ import React from 'react'; ...@@ -4,27 +4,29 @@ import React from 'react';
import type { CsrfData } from 'types/client/account'; import type { CsrfData } from 'types/client/account';
export interface ErrorType<T> { import type { ResourceError } from 'lib/api/resources';
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
interface Params { export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers'];
body?: Record<string, unknown>; body?: Record<string, unknown>;
credentials?: RequestCredentials;
} }
export default function useFetch() { export default function useFetch() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {}; 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 = { const reqParams = {
...params, ...params,
body: params?.method && ![ 'GET', 'HEAD' ].includes(params.method) ? body: params?.method && params?.body && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({ ...params?.body, _csrf_token: token }) : JSON.stringify(params.body) :
undefined, undefined,
headers: {
...params?.headers,
...(token ? { 'x-csrf-token': token } : {}),
},
}; };
return fetch(path, reqParams).then(response => { return fetch(path, reqParams).then(response => {
...@@ -37,7 +39,9 @@ export default function useFetch() { ...@@ -37,7 +39,9 @@ export default function useFetch() {
return response.json().then( return response.json().then(
(jsonError) => Promise.reject({ (jsonError) => Promise.reject({
// DEPRECATED
error: jsonError as Error, error: jsonError as Error,
payload: jsonError as Error,
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
}), }),
......
import { useQuery } from '@tanstack/react-query'; import useApiQuery from 'lib/api/useApiQuery';
import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
interface Error {
error?: {
status?: number;
statusText?: string;
};
}
export default function useFetchProfileInfo() { export default function useFetchProfileInfo() {
const fetch = useFetch(); return useApiQuery('user_info', {
queryOptions: {
return useQuery<unknown, Error, UserInfo>([ QueryKeys.profile ], async() => { refetchOnMount: false,
return fetch('/node-api/account/profile'); enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}, { },
refetchOnMount: false,
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
} }
...@@ -2,8 +2,7 @@ import * as Sentry from '@sentry/react'; ...@@ -2,8 +2,7 @@ import * as Sentry from '@sentry/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/accountQueries'; import { resourceKey } from 'lib/api/resources';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useLoginUrl from 'lib/hooks/useLoginUrl'; import useLoginUrl from 'lib/hooks/useLoginUrl';
...@@ -17,7 +16,7 @@ export interface ErrorType { ...@@ -17,7 +16,7 @@ export interface ErrorType {
export default function useRedirectForInvalidAuthToken() { export default function useRedirectForInvalidAuthToken() {
const queryClient = useQueryClient(); 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 errorStatus = state?.error?.error?.status;
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
......
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
"next": "12.2.5", "next": "12.2.5",
"node-fetch": "^3.2.9", "node-fetch": "^3.2.9",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
"path-to-regexp": "^6.2.1",
"phoenix": "^1.6.15", "phoenix": "^1.6.15",
"pino-http": "^8.2.1", "pino-http": "^8.2.1",
"pino-pretty": "^9.1.1", "pino-pretty": "^9.1.1",
......
...@@ -5,11 +5,11 @@ import type { AppProps } from 'next/app'; ...@@ -5,11 +5,11 @@ import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra'; import { Chakra } from 'lib/Chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import theme from 'theme'; import theme from 'theme';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
...@@ -22,8 +22,8 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -22,8 +22,8 @@ function MyApp({ Component, pageProps }: AppProps) {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, _error) => { retry: (failureCount, _error) => {
const error = _error as ErrorType<{ status: number }>; const error = _error as ResourceError<{ status: number }>;
const status = error?.error?.status; const status = error?.status || error?.error?.status;
if (status && status >= 400 && status < 500) { if (status && status >= 400 && status < 500) {
// don't do retry for client error responses // don't do retry for client error responses
return false; 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 { ...@@ -67,6 +67,12 @@ export interface WatchlistAddress {
address?: AddressParam; address?: AddressParam;
} }
export interface WatchlistTokensResponse {
message: string;
result?: Array<unknown>;
status: string;
}
export interface WatchlistAddressNew { export interface WatchlistAddressNew {
addressName: string; addressName: string;
notificationSettings: NotificationSettings; 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 { 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 { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import type { TWatchlist } from 'types/client/account';
import { QueryKeys as AccountQueryKeys } from 'types/client/accountQueries';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import useFetch from 'lib/hooks/useFetch'; import { resourceKey } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import useLoginUrl from 'lib/hooks/useLoginUrl'; import useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal'; import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
...@@ -27,19 +26,12 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { ...@@ -27,19 +26,12 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); 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 isAuth = Boolean(profileData);
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const watchListQuery = useQuery<unknown, unknown, TWatchlist>( const watchListQuery = useApiQuery('watchlist', { queryOptions: { enabled: isAdded } });
[ AccountQueryKeys.watchlist ],
async() => fetch('/node-api/account/watchlist'),
{
enabled: isAdded,
},
);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (!isAuth) { if (!isAuth) {
......
...@@ -12,11 +12,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; ...@@ -12,11 +12,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; 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 getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = { type Props = {
...@@ -40,7 +40,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -40,7 +40,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
name: data?.name || '', name: data?.name || '',
}, },
}); });
const fetch = useFetch(); const apiFetch = useApiFetch();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
...@@ -48,17 +48,20 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -48,17 +48,20 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const body = { name: data.name }; const body = { name: data.name };
if (!data.token) { 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, { const mutation = useMutation(updateApiKey, {
onSuccess: async(data) => { onSuccess: async(data) => {
const response = data as unknown as ApiKey; 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); const isExisting = prevData && prevData.some((item) => item.api_key === response.api_key);
if (isExisting) { if (isExisting) {
...@@ -76,11 +79,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -76,11 +79,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onClose(); onClose();
}, },
onError: (e: ErrorType<ApiKeyErrors>) => { onError: (error: ResourceErrorAccount<ApiKeyErrors>) => {
if (e?.error?.name) { const errorMap = error.payload?.errors;
setError('name', { type: 'custom', message: getErrorMessage(e.error, 'name') }); if (errorMap?.name) {
} else if (e?.error?.identity_id) { setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
setError('name', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') }); } else if (errorMap?.identity_id) {
setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
......
...@@ -3,9 +3,9 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,9 +3,9 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; 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'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -16,14 +16,17 @@ type Props = { ...@@ -16,14 +16,17 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch(); const apiFetch = useApiFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/node-api/account/api-keys/${ data.api_key }`, { method: 'DELETE' }); return apiFetch('api_keys', {
}, [ data.api_key, fetch ]); pathParams: { id: data.api_key },
fetchParams: { method: 'DELETE' },
});
}, [ data.api_key, apiFetch ]);
const onSuccess = useCallback(async() => { 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); return prevData?.filter((item) => item.api_key !== data.api_key);
}); });
}, [ data, queryClient ]); }, [ data, queryClient ]);
......
...@@ -13,10 +13,10 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -13,10 +13,10 @@ import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward'; import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts'; import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -37,7 +37,7 @@ const BlockDetails = () => { ...@@ -37,7 +37,7 @@ const BlockDetails = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch(); 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 ], [ QueryKeys.block, router.query.id ],
async() => await fetch(`/node-api/blocks/${ router.query.id }`), async() => await fetch(`/node-api/blocks/${ router.query.id }`),
{ {
......
...@@ -12,11 +12,11 @@ import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form'; ...@@ -12,11 +12,11 @@ import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; 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 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 { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
...@@ -46,16 +46,19 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -46,16 +46,19 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch(); const apiFetch = useApiFetch();
const customAbiKey = (data: Inputs & { id?: number }) => { const customAbiKey = (data: Inputs & { id?: number }) => {
const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi }; const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi };
if (!data.id) { 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'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
...@@ -63,7 +66,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -63,7 +66,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const mutation = useMutation(customAbiKey, { const mutation = useMutation(customAbiKey, {
onSuccess: (data) => { onSuccess: (data) => {
const response = data as unknown as CustomAbi; 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); const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) { if (isExisting) {
...@@ -81,13 +84,14 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -81,13 +84,14 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
onClose(); onClose();
}, },
onError: (e: ErrorType<CustomAbiErrors>) => { onError: (error: ResourceErrorAccount<CustomAbiErrors>) => {
if (e?.error?.address_hash || e?.error?.name || e?.error?.abi) { const errorMap = error.payload?.errors;
e?.error?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(e.error, 'address_hash') }); if (errorMap?.address_hash || errorMap?.name || errorMap?.abi) {
e?.error?.name && setError('name', { type: 'custom', message: getErrorMessage(e.error, 'name') }); errorMap?.address_hash && setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
e?.error?.abi && setError('abi', { type: 'custom', message: getErrorMessage(e.error, 'abi') }); errorMap?.name && setError('name', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
} else if (e?.error?.identity_id) { errorMap?.abi && setError('abi', { type: 'custom', message: getErrorMessage(errorMap, 'abi') });
setError('contract_address_hash', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') }); } else if (errorMap?.identity_id) {
setError('contract_address_hash', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
......
...@@ -3,8 +3,9 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,8 +3,9 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account'; 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'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -16,13 +17,17 @@ type Props = { ...@@ -16,13 +17,17 @@ type Props = {
const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiFetch = useApiFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/node-api/account/custom-abis/${ data.id }`, { method: 'DELETE' }); return apiFetch('custom_abi', {
}, [ data ]); pathParams: { id: String(data.id) },
fetchParams: { method: 'DELETE' },
});
}, [ apiFetch, data.id ]);
const onSuccess = useCallback(async() => { 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); return prevData?.filter((item) => item.id !== data.id);
}); });
}, [ data, queryClient ]); }, [ data, queryClient ]);
......
import { Box, Button, Stack, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react'; 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 React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
...@@ -26,13 +24,12 @@ const ApiKeysPage: React.FC = () => { ...@@ -26,13 +24,12 @@ const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure(); const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = 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) => { const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data); setApiKeyModalData(data);
......
import { Box, Button, HStack, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, HStack, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account'; import type { CustomAbi } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal'; import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
...@@ -23,14 +21,12 @@ const CustomAbiPage: React.FC = () => { ...@@ -23,14 +21,12 @@ const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isLoading, isError } = useQuery<unknown, unknown, CustomAbis>([ QueryKeys.customAbis ], async() => const { data, isLoading, isError } = useApiQuery('custom_abi');
await fetch('/node-api/account/custom-abis'));
const onEditClick = useCallback((data: CustomAbi) => { const onEditClick = useCallback((data: CustomAbi) => {
setCustomAbiModalData(data); 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 * as Sentry from '@sentry/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -47,8 +47,6 @@ const Login = () => { ...@@ -47,8 +47,6 @@ const Login = () => {
}); });
}, [ toast, token ]); }, [ toast, token ]);
const prodUrl = 'https://blockscout.com/poa/core';
const handleNumIncrement = React.useCallback(() => { const handleNumIncrement = React.useCallback(() => {
for (let index = 0; index < 5; index++) { for (let index = 0; index < 5; index++) {
setNum(5); setNum(5);
...@@ -58,19 +56,15 @@ const Login = () => { ...@@ -58,19 +56,15 @@ const Login = () => {
return ( return (
<Page> <Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="1000px"> <VStack gap={ 4 } alignItems="flex-start" maxW="1000px">
<PageTitle text="Vercel page"/> <PageTitle text="Login page 😂"/>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
{ isFormVisible && ( { isFormVisible && (
<> <>
<Alert status="error" flexDirection="column" alignItems="flex-start"> <Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md"> <AlertTitle fontSize="md">
!!! Temporary solution for authentication !!! !!! Temporary solution for authentication on localhost !!!
</AlertTitle> </AlertTitle>
<AlertDescription mt={ 3 }> <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 <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 authenticated in current environment
</AlertDescription> </AlertDescription>
...@@ -80,6 +74,10 @@ const Login = () => { ...@@ -80,6 +74,10 @@ const Login = () => {
</> </>
) } ) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button> <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> </VStack>
</Page> </Page>
); );
......
...@@ -2,10 +2,11 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react'; ...@@ -2,10 +2,11 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { WatchlistAddress, WatchlistTokensResponse } from 'types/api/account';
import type { TWatchlist, TWatchlistItem } from 'types/client/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 useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
...@@ -20,14 +21,38 @@ import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem'; ...@@ -20,14 +21,38 @@ import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const { data, isLoading, isError } = const apiFetch = useApiFetch();
useQuery<unknown, unknown, TWatchlist>([ QueryKeys.watchlist ], async() => fetch('/node-api/account/watchlist/get-with-tokens')); 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 queryClient = useQueryClient();
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>(); const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
...@@ -44,7 +69,7 @@ const WatchList: React.FC = () => { ...@@ -44,7 +69,7 @@ const WatchList: React.FC = () => {
}, [ addressModalProps ]); }, [ addressModalProps ]);
const onAddOrEditSuccess = useCallback(async() => { const onAddOrEditSuccess = useCallback(async() => {
await queryClient.refetchQueries([ QueryKeys.watchlist ]); await queryClient.refetchQueries([ resourceKey('watchlist') ]);
setAddressModalData(undefined); setAddressModalData(undefined);
addressModalProps.onClose(); addressModalProps.onClose();
}, [ addressModalProps, queryClient ]); }, [ addressModalProps, queryClient ]);
...@@ -60,7 +85,7 @@ const WatchList: React.FC = () => { ...@@ -60,7 +85,7 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const onDeleteSuccess = useCallback(async() => { 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); return prevData?.filter((item) => item.id !== deleteModalData?.id);
}); });
}, [ deleteModalData?.id, queryClient ]); }, [ deleteModalData?.id, queryClient ]);
......
...@@ -9,11 +9,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; ...@@ -9,11 +9,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account'; 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 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 { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
...@@ -32,7 +32,7 @@ type Inputs = { ...@@ -32,7 +32,7 @@ type Inputs = {
} }
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch(); const apiFetch = useApiFetch();
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
...@@ -54,24 +54,28 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -54,24 +54,28 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { 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); setPending(false);
if (e?.error?.address_hash || e?.error?.name) { const errorMap = error.payload?.errors;
e?.error?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(e.error, 'address_hash') }); if (errorMap?.address_hash || errorMap?.name) {
e?.error?.name && setError('tag', { type: 'custom', message: getErrorMessage(e.error, 'name') }); errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
} else if (e?.error?.identity_id) { errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
setError('address', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') }); } else if (errorMap?.identity_id) {
setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries([ QueryKeys.addressTags ]).then(() => { queryClient.refetchQueries([ resourceKey('private_tags_address') ]).then(() => {
onClose(); onClose();
setPending(false); setPending(false);
}); });
......
...@@ -3,9 +3,9 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,9 +3,9 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account'; 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'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -20,19 +20,23 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type }) ...@@ -20,19 +20,23 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const id = data.id; const id = data.id;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch(); const apiFetch = useApiFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/node-api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' }); const resourceName = type === 'address' ? 'private_tags_address' : 'private_tags_tx';
}, [ fetch, type, id ]); return apiFetch(resourceName, {
pathParams: { id: data.id },
fetchParams: { method: 'DELETE' },
});
}, [ type, apiFetch, data.id ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
if (type === 'address') { 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); return prevData?.filter((item: AddressTag) => item.id !== id);
}); });
} else { } 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); return prevData?.filter((item: TransactionTag) => item.id !== id);
}); });
} }
......
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -18,13 +16,11 @@ import AddressTagTable from './AddressTagTable/AddressTagTable'; ...@@ -18,13 +16,11 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => { const PrivateAddressTags = () => {
const { data: addressTagsData, isLoading, isError } = const { data: addressTagsData, isLoading, isError } = useApiQuery('private_tags_address', { queryOptions: { refetchOnMount: false } });
useQuery<unknown, unknown, AddressTags>([ QueryKeys.addressTags ], async() => fetch('/node-api/account/private-tags/address'), { refetchOnMount: false });
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>(); const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>(); const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
......
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account'; import type { TransactionTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -18,16 +16,11 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem ...@@ -18,16 +16,11 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => { const PrivateTransactionTags = () => {
const { data: transactionTagsData, isLoading, isError } = const { data: transactionTagsData, isLoading, isError } = useApiQuery('private_tags_tx', { queryOptions: { refetchOnMount: false } });
useQuery<unknown, unknown, TransactionTags>(
[ QueryKeys.transactionTags ],
async() => fetch('/node-api/account/private-tags/transaction'), { refetchOnMount: false },
);
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>(); const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>(); const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
......
...@@ -9,11 +9,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; ...@@ -9,11 +9,11 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account'; 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 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 { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
import TransactionInput from 'ui/shared/TransactionInput'; import TransactionInput from 'ui/shared/TransactionInput';
...@@ -44,7 +44,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -44,7 +44,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch(); const apiFetch = useApiFetch();
const { mutate } = useMutation((formData: Inputs) => { const { mutate } = useMutation((formData: Inputs) => {
const body = { const body = {
...@@ -54,24 +54,28 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -54,24 +54,28 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { 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); setPending(false);
if (e?.error?.tx_hash || e?.error?.name) { const errorMap = error.payload?.errors;
e?.error?.tx_hash && setError('transaction', { type: 'custom', message: getErrorMessage(e.error, 'tx_hash') }); if (errorMap?.tx_hash || errorMap?.name) {
e?.error?.name && setError('tag', { type: 'custom', message: getErrorMessage(e.error, 'name') }); errorMap?.tx_hash && setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'tx_hash') });
} else if (e?.error?.identity_id) { errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
setError('transaction', { type: 'custom', message: getErrorMessage(e.error, 'identity_id') }); } else if (errorMap?.identity_id) {
setError('transaction', { type: 'custom', message: getErrorMessage(errorMap, 'identity_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
}, },
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries([ QueryKeys.transactionTags ]).then(() => { queryClient.refetchQueries([ resourceKey('private_tags_tx') ]).then(() => {
onClose(); onClose();
setPending(false); setPending(false);
}); });
......
...@@ -4,9 +4,9 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,9 +4,9 @@ import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; 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'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -23,17 +23,20 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -23,17 +23,20 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tags = data.tags.split(';'); const tags = data.tags.split(';');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch(); const apiFetch = useApiFetch();
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
const deleteApiKey = useCallback(() => { const deleteApiKey = useCallback(() => {
const body = { remove_reason: reason }; const body = { remove_reason: reason };
return fetch(`/node-api/account/public-tags/${ data.id }`, { method: 'DELETE', body }); return apiFetch('public_tags', {
}, [ data.id, fetch, reason ]); pathParams: { id: String(data.id) },
fetchParams: { method: 'DELETE', body },
});
}, [ data.id, apiFetch, reason ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
onDeleteSuccess(); onDeleteSuccess();
queryClient.setQueryData([ QueryKeys.publicTags ], (prevData: PublicTags | undefined) => { queryClient.setQueryData([ resourceKey('public_tags') ], (prevData: PublicTags | undefined) => {
return prevData?.filter((item) => item.id !== data.id); return prevData?.filter((item) => item.id !== data.id);
}); });
}, [ queryClient, data, onDeleteSuccess ]); }, [ queryClient, data, onDeleteSuccess ]);
......
import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem'; import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
...@@ -25,10 +23,8 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -25,10 +23,8 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ QueryKeys.publicTags ], async() => const { data, isLoading, isError } = useApiQuery('public_tags');
await fetch('/node-api/account/public-tags'));
const onDeleteModalClose = useCallback(() => { const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined); setDeleteModalData(undefined);
......
...@@ -13,11 +13,11 @@ import type { FieldError, Path, SubmitHandler } from 'react-hook-form'; ...@@ -13,11 +13,11 @@ import type { FieldError, Path, SubmitHandler } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account'; 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 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 { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert'; import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
...@@ -58,7 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100; ...@@ -58,7 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch(); const apiFetch = useApiFetch();
const inputSize = { base: 'md', lg: 'lg' }; const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
...@@ -100,17 +100,20 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -100,17 +100,20 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
}; };
if (!data?.id) { 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, { const mutation = useMutation(updatePublicTag, {
onSuccess: async(data) => { onSuccess: async(data) => {
const response = data as unknown as PublicTag; 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); const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) { if (isExisting) {
...@@ -128,13 +131,14 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -128,13 +131,14 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
changeToDataScreen(true); changeToDataScreen(true);
}, },
onError: (e: ErrorType<PublicTagErrors>) => { onError: (error: ResourceErrorAccount<PublicTagErrors>) => {
if (e.error?.full_name || e.error?.email || e.error?.tags || e.error?.addresses || e.error?.additional_comment) { const errorMap = error.payload?.errors;
e.error?.full_name && setError('fullName', { type: 'custom', message: getErrorMessage(e.error, 'full_name') }); if (errorMap?.full_name || errorMap?.email || errorMap?.tags || errorMap?.addresses || errorMap?.additional_comment) {
e.error?.email && setError('email', { type: 'custom', message: getErrorMessage(e.error, 'email') }); errorMap?.full_name && setError('fullName', { type: 'custom', message: getErrorMessage(errorMap, 'full_name') });
e.error?.tags && setError('tags', { type: 'custom', message: getErrorMessage(e.error, 'tags') }); errorMap?.email && setError('email', { type: 'custom', message: getErrorMessage(errorMap, 'email') });
e.error?.addresses && setError('addresses.0.address', { type: 'custom', message: getErrorMessage(e.error, 'addresses') }); errorMap?.tags && setError('tags', { type: 'custom', message: getErrorMessage(errorMap, 'tags') });
e.error?.additional_comment && setError('comment', { type: 'custom', message: getErrorMessage(e.error, 'additional_comment') }); 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 { } else {
setAlertVisible(true); setAlertVisible(true);
} }
......
...@@ -2,8 +2,6 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,8 +2,6 @@ import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
...@@ -27,7 +25,7 @@ const Page = ({ ...@@ -27,7 +25,7 @@ const Page = ({
}: Props) => { }: Props) => {
const fetch = useFetch(); 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)), enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -33,7 +34,7 @@ test.describe('auth', () => { ...@@ -33,7 +34,7 @@ test.describe('auth', () => {
}); });
extendedTest('+@dark-mode', async({ mount, page }) => { 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, status: 200,
body: JSON.stringify(profileMock.base), body: JSON.stringify(profileMock.base),
})); }));
......
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -29,7 +30,7 @@ test.describe('auth', () => { ...@@ -29,7 +30,7 @@ test.describe('auth', () => {
}); });
extendedTest('base view', async({ mount, page }) => { 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, status: 200,
body: JSON.stringify(profileMock.base), body: JSON.stringify(profileMock.base),
})); }));
......
import { useQuery } from '@tanstack/react-query';
import React from 'react'; 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 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 ChartWidget from '../shared/chart/ChartWidget';
import { STATS_INTERVALS } from './constants'; import { STATS_INTERVALS } from './constants';
...@@ -22,19 +19,18 @@ function formatDate(date: Date) { ...@@ -22,19 +19,18 @@ function formatDate(date: Date) {
} }
const ChartWidgetContainer = ({ id, title, description, interval }: Props) => { const ChartWidgetContainer = ({ id, title, description, interval }: Props) => {
const fetch = useFetch();
const selectedInterval = STATS_INTERVALS[interval]; const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : 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 } = useApiQuery('stats_charts', {
queryParams: {
const { data, isLoading } = useQuery<unknown, unknown, Charts>( name: id,
[ QueryKeys.charts, id, startDate ], from: startDate,
async() => await fetch(url), to: endDate,
); },
});
const items = data?.chart const items = data?.chart
.map((item) => { .map((item) => {
......
import { Grid } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { Stats } from 'types/api/stats'; import useApiQuery from 'lib/api/useApiQuery';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import NumberWidget from './NumberWidget'; import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton'; import NumberWidgetSkeleton from './NumberWidgetSkeleton';
...@@ -13,12 +9,7 @@ import NumberWidgetSkeleton from './NumberWidgetSkeleton'; ...@@ -13,12 +9,7 @@ import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 8; const skeletonsCount = 8;
const NumberWidgetsList = () => { const NumberWidgetsList = () => {
const fetch = useFetch(); const { data, isLoading } = useApiQuery('stats_counters');
const { data, isLoading } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
async() => await fetch(`/node-api/stats/counters`),
);
return ( return (
<Grid <Grid
......
...@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account'; import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/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 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 { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
...@@ -83,7 +83,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -83,7 +83,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
mode: 'onTouched', mode: 'onTouched',
}); });
const fetch = useFetch(); const apiFetch = useApiFetch();
function updateWatchlist(formData: Inputs) { function updateWatchlist(formData: Inputs) {
const body = { const body = {
...@@ -96,11 +96,14 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -96,11 +96,14 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
}; };
if (!isAdd && data) { if (!isAdd && data) {
// edit address // 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 { } else {
// add address // 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 ...@@ -109,13 +112,14 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
await onSuccess(); await onSuccess();
setPending(false); setPending(false);
}, },
onError: (e: ErrorType<WatchlistErrors>) => { onError: (error: ResourceErrorAccount<WatchlistErrors>) => {
setPending(false); setPending(false);
if (e?.error?.address_hash || e?.error?.name) { const errorMap = error.payload?.errors;
e?.error?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(e.error, 'address_hash') }); if (errorMap?.address_hash || errorMap?.name) {
e?.error?.name && setError('tag', { type: 'custom', message: getErrorMessage(e.error, 'name') }); errorMap?.address_hash && setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'address_hash') });
} else if (e?.error?.watchlist_id) { errorMap?.name && setError('tag', { type: 'custom', message: getErrorMessage(errorMap, 'name') });
setError('address', { type: 'custom', message: getErrorMessage(e.error, 'watchlist_id') }); } else if (errorMap?.watchlist_id) {
setError('address', { type: 'custom', message: getErrorMessage(errorMap, 'watchlist_id') });
} else { } else {
setAlertVisible(true); setAlertVisible(true);
} }
......
...@@ -3,7 +3,7 @@ import React, { useCallback } from 'react'; ...@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import type { TWatchlistItem } from 'types/client/account'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -16,11 +16,14 @@ type Props = { ...@@ -16,11 +16,14 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch(); const apiFetch = useApiFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/node-api/account/watchlist/${ data?.id }`, { method: 'DELETE' }); return apiFetch('custom_abi', {
}, [ data?.id, fetch ]); pathParams: { id: data.id },
fetchParams: { method: 'DELETE' },
});
}, [ data?.id, apiFetch ]);
const address = data?.address_hash; const address = data?.address_hash;
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account'; 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 useToast from 'lib/hooks/useToast';
import ListItemMobile from 'ui/shared/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
...@@ -29,7 +29,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -29,7 +29,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const errorToast = useToast(); const errorToast = useToast();
const fetch = useFetch(); const apiFetch = useApiFetch();
const showErrorToast = useCallback(() => { const showErrorToast = useCallback(() => {
errorToast({ errorToast({
...@@ -61,7 +61,10 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -61,7 +61,10 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
setSwitchDisabled(true); setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } }; const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState); 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: () => { onError: () => {
showErrorToast(); showErrorToast();
......
...@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account'; 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 useToast from 'lib/hooks/useToast';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -34,7 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -34,7 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const errorToast = useToast(); const errorToast = useToast();
const fetch = useFetch(); const apiFetch = useApiFetch();
const showErrorToast = useCallback(() => { const showErrorToast = useCallback(() => {
errorToast({ errorToast({
...@@ -66,7 +66,10 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -66,7 +66,10 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
setSwitchDisabled(true); setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } }; const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState); 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: () => { onError: () => {
showErrorToast(); showErrorToast();
......
...@@ -7769,6 +7769,11 @@ path-parse@^1.0.7: ...@@ -7769,6 +7769,11 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 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: path-type@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 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