Commit 375a8b46 authored by tom's avatar tom

custom abi resources

parent 010916f1
import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { RESOURCES } from './resources'; import { RESOURCES } from './resources';
export default function buildUrl(resource: keyof typeof RESOURCES, params?: Record<string, string>) { export default function buildUrl(
resource: keyof typeof RESOURCES,
pathParams?: Record<string, string>,
queryParams?: Record<string, string>,
) {
const base = appConfig.host === 'localhost' ? appConfig.baseUrl : appConfig.api.endpoint; const base = appConfig.host === 'localhost' ? appConfig.baseUrl : appConfig.api.endpoint;
const path = appConfig.host === 'localhost' ? const path = appConfig.host === 'localhost' ?
'/proxy' + appConfig.api.basePath + RESOURCES[resource].path : '/proxy' + appConfig.api.basePath + RESOURCES[resource].path :
appConfig.api.basePath + RESOURCES[resource].path; appConfig.api.basePath + RESOURCES[resource].path;
const url = new URL(path, base); const url = new URL(compile(path)(pathParams), base);
params && Object.entries(params).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value); url.searchParams.append(key, value);
}); });
......
import type { UserInfo } from 'types/api/account'; import type { UserInfo, CustomAbis } from 'types/api/account';
import type { CsrfData } from 'types/client/account'; import type { CsrfData } from 'types/client/account';
export const RESOURCES = { export const RESOURCES = {
// account
user_info: { user_info: {
path: '/api/account/v1/user/info', path: '/api/account/v1/user/info',
queryKey: 'user_info',
}, },
csrf: { csrf: {
path: '/api/account/v1/get_csrf', path: '/api/account/v1/get_csrf',
queryKey: 'csrf', },
custom_abi: {
path: '/api/account/v1/user/custom_abis/:id?',
}, },
}; };
...@@ -16,11 +18,13 @@ export const resourceKey = (x: keyof typeof RESOURCES) => x; ...@@ -16,11 +18,13 @@ export const resourceKey = (x: keyof typeof RESOURCES) => x;
export type ResourcePayload<Q extends keyof typeof RESOURCES> = export type ResourcePayload<Q extends keyof typeof RESOURCES> =
Q extends 'user_info' ? UserInfo : Q extends 'user_info' ? UserInfo :
Q extends 'csrf' ? CsrfData : never; Q extends 'csrf' ? CsrfData :
Q extends 'custom_abi' ? CustomAbis :
never;
export interface ResourceError { export interface ResourceError<T = unknown> {
error?: { error?: T;
status?: number; payload?: T;
statusText?: string; status: Response['status'];
}; statusText: Response['statusText'];
} }
import React from 'react';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl';
import type { RESOURCES, ResourcePayload, ResourceError } from './resources';
interface Params {
pathParams?: Record<string, string>;
queryParams?: Record<string, string>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>;
}
export default function useApiFetch() {
const fetch = useFetch();
return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = ResourcePayload<R>, ErrorType = ResourceError>(
resource: R,
{ pathParams, queryParams, fetchParams }: Params = {},
) => {
const url = buildUrl(resource, pathParams, queryParams);
return fetch<SuccessType, ErrorType>(url, { credentials: 'include', ...fetchParams });
}, [ fetch ]);
}
...@@ -4,15 +4,10 @@ import React from 'react'; ...@@ -4,15 +4,10 @@ import React from 'react';
import type { CsrfData } from 'types/client/account'; import type { CsrfData } from 'types/client/account';
import type { ResourceError } from 'lib/api/resources';
import { resourceKey, RESOURCES } from 'lib/api/resources'; import { resourceKey, RESOURCES } from 'lib/api/resources';
export interface ErrorType<T> { export interface Params {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
body?: Record<string, unknown>; body?: Record<string, unknown>;
credentials?: RequestCredentials; credentials?: RequestCredentials;
...@@ -22,7 +17,7 @@ export default function useFetch() { ...@@ -22,7 +17,7 @@ export default function useFetch() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ resourceKey('csrf') ]) || {}; const { token } = queryClient.getQueryData<CsrfData>([ resourceKey('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 && ![ 'GET', 'HEAD' ].includes(params.method) ?
...@@ -41,7 +36,9 @@ export default function useFetch() { ...@@ -41,7 +36,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,
}), }),
......
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
"lodash": "^4.0.0", "lodash": "^4.0.0",
"next": "12.2.5", "next": "12.2.5",
"node-fetch": "^3.2.9", "node-fetch": "^3.2.9",
"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",
......
...@@ -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 { ResourceError } 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', CustomAbi, CustomAbiErrors>('custom_abi', { fetchParams: { method: 'POST', body } });
} }
return fetch<CustomAbi, CustomAbiErrors>(`/node-api/account/custom-abis/${ data.id }`, { method: 'PUT', body }); return apiFetch<'custom_abi', CustomAbi, CustomAbiErrors>('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: ResourceError<{ errors: 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, 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);
......
...@@ -7737,6 +7737,11 @@ path-parse@^1.0.7: ...@@ -7737,6 +7737,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