Commit be2ce560 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into refactor-address

parents 7c3797ef 0cfe93cf
API_AUTHORIZATION_TOKEN=xxx
SENTRY_DSN=xxx
NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_ORG=block-scout
......@@ -11,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}]
\ No newline at end of file
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}]
\ No newline at end of file
export const privateTagsTransaction = [
{
transaction: '0x44b51ef7746ff48f74f45699d33557faa96059eb8655fdd7bf14a5f529ea3528',
tag: 'some_tag',
},
{
transaction: '0x44b51ef7746ff48f74f45699d33557faa96059eb8655fdd7bf14a5f529ea9999',
tag: 'some_other_tag',
},
];
export type TPrivateTagsTransaction = Array<TPrivateTagsTransactionItem>
export type TPrivateTagsTransactionItem = {
transaction: string;
tag: string;
}
export const publicTags = [
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
addressName: 'DarkForest',
},
{
address: '0x35317007D203b8a86CA727ad44E473E40450E378',
addressName: 'DarkForest2',
},
],
tags: [
{
name: 'darkforest',
// colorHex: '#4A5568',
// backgroundHex: '#E2E8F0',
},
],
date: 'Jun 10, 2022',
id: '123',
userName: 'Tatyana',
userEmail: 'sample@gmail.com',
companyName: 'Contract name',
companyUrl: 'contractname.com',
comment: 'Please use #ED8936 color for tag...',
},
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
},
],
tags: [
{
name: 'OMNI',
colorHex: '#FFFFFF',
backgroundHex: '#1A202C',
},
{
name: '123456789012345678901237123123',
colorHex: '#FFFFFF',
backgroundHex: '#6B46C1',
},
],
date: 'Jun 5, 2022',
id: '456',
},
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
addressName: 'Contract name',
},
],
tags: [
{
name: 'SANA',
colorHex: '#FFFFFF',
backgroundHex: '#ED8936',
},
],
date: 'Jun 1, 2022',
id: '789',
},
];
export type TPublicTags = Array<TPublicTagItem>
export type TPublicTagItem = {
addresses: Array<TPublicTagAddress>;
tags: Array<TPublicTag>;
// status: typeof STATUS;
date: string;
// id is for react element key, as tag or address may not be unique
id: string;
userName?: string;
userEmail?: string;
companyName?: string;
companyUrl?: string;
comment?: string;
}
export type TPublicTagAddress = {
address: string;
addressName?: string;
}
export type TPublicTag = {
name: string;
colorHex?: string;
backgroundHex?: string;
}
export const data = [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
},
before: {
balance: '0.008350264867549483',
},
diff: '-0.003842645503636562',
},
];
export type TTxState = Array<TTxStateItem>;
export type TTxStateItem = {
address: string;
miner: string;
after: {
balance: string;
nonce?: string;
};
before: {
balance: string;
nonce?: string;
};
diff: string;
storage?: Array<TTxStateItemStorage>;
}
export type TTxStateItemStorage = {
address: string;
before: string;
after: string;
}
export const watchlist = [
{
address: '0x4831c121879d3de0e2b181d9d55e9b0724f5d926',
tokenBalance: 100.1,
tokenBalanceUSD: 101.2,
tokensAmount: 2,
tokensUSD: 202.2,
totalUSD: 123123,
tag: 'some_tag',
notification: true,
},
{
address: '0x8c461F78760988c4135e363a87dd736f8b671ff0',
tokensAmount: 2,
tokensUSD: 2202.2,
totalUSD: 3000.5,
tag: 'some_other_tag',
notification: false,
},
{
address: '0x930F381E649c84579Ef58117E923714964C55D16',
tokenBalance: 200.2,
tokenBalanceUSD: 202.4,
totalUSD: 3000.5,
tag: '12345678901234567890123456789012345',
notification: false,
},
];
export type TWatchlist = Array<TWatchlistItem>
export type TWatchlistItem = {
address: string;
tokenBalance?: number;
tokenBalanceUSD?: number;
tokensAmount?: number;
tokensUSD?: number;
totalUSD?: number;
tag: string;
notification?: boolean;
}
import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch';
import * as cookies from 'lib/cookies';
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
export default function fetch(path: string, init?: RequestInit): Promise<Response> {
export default function fetchFactory(_req: NextApiRequest) {
return function fetch(path: string, init?: RequestInit): Promise<Response> {
const headers = {
accept: 'application/json',
authorization: `Bearer ${ process.env.API_AUTHORIZATION_TOKEN }`,
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
};
const url = `https://blockscout.com${ path }`;
......@@ -15,4 +19,5 @@ export default function fetch(path: string, init?: RequestInit): Promise<Respons
headers,
...init,
});
};
}
import { withSentry } from '@sentry/nextjs';
import type { NextApiRequest, NextApiResponse } from 'next';
import fetch from 'lib/api/fetch';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
......@@ -16,7 +16,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const url = getUrlWithNetwork(_req, `api${ getUrl(_req) }`);
const fetch = fetchFactory(_req);
const response = await fetch(url, {
method: _req.method,
body: isBodyDisallowed ? undefined : _req.body,
......@@ -29,12 +30,13 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
}
let responseError;
const defaultError = { statusText: response.statusText, status: response.status };
try {
const error = await response.json() as { errors: unknown };
responseError = error?.errors || {};
responseError = error?.errors || defaultError;
} catch (error) {
responseError = { statusText: response.statusText, status: response.status };
responseError = defaultError;
}
res.status(500).json(responseError);
......
import * as Sentry from '@sentry/nextjs';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
export default function clientFetch<Success, Error>(path: string, init?: RequestInit): Promise<Success | ErrorType<Error>> {
return fetch(path, init).then(response => {
if (!response.ok) {
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
} else {
return response.json() as Promise<Success>;
}
});
}
......@@ -7,6 +7,7 @@ export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed',
NETWORK_TYPE='network_type',
NETWORK_SUB_TYPE='network_sub_type',
API_TOKEN='_explorer_key',
}
export function get(name?: string | undefined | null) {
......
import * as Sentry from '@sentry/nextjs';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { CsrfData } from 'types/client/account';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
interface Params {
method?: RequestInit['method'];
body?: Record<string, unknown>;
}
export default function useFetch() {
const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ErrorType<Error>> => {
const reqParams = {
...params,
body: params?.method && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({ ...params?.body, _csrf_token: token }) :
undefined,
};
return fetch(path, reqParams).then(response => {
if (!response.ok) {
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
} else {
return response.json() as Promise<Success>;
}
});
}, [ token ]);
}
......@@ -2,10 +2,21 @@ import { useQuery } from '@tanstack/react-query';
import type { UserInfo } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
interface Error {
error?: {
status?: number;
statusText?: string;
};
}
export default function useFetchProfileInfo() {
return useQuery<unknown, unknown, UserInfo>([ 'profile' ], async() => {
const fetch = useFetch();
return useQuery<unknown, Error, UserInfo>([ 'profile' ], async() => {
return fetch('/api/account/profile');
}, { refetchOnMount: false });
}, {
refetchOnMount: false,
});
}
......@@ -6,7 +6,7 @@ export default function useNetwork() {
const router = useRouter();
const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_type === 'string' ? router.query.network_type : undefined,
network_sub_type: typeof router.query.network_sub_type === 'string' ? router.query.network_sub_type : undefined,
});
return selectedNetwork;
}
......@@ -101,6 +101,12 @@ export const ROUTES = {
other: {
pattern: `${ BASE_PATH }/search-results`,
},
// AUTH
auth: {
// no slash required, it is correct
pattern: `${ BASE_PATH }auth/auth0`,
},
};
// !!! for development purpose only !!!
......
const { NextResponse } = require('next/server');
const { NAMES } = require('lib/cookies');
const { link } = require('lib/link/link');
const findNetwork = require('lib/networks/findNetwork').default;
export function middleware(req) {
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
const selectedNetwork = findNetwork(networkParams);
if (selectedNetwork) {
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (!apiToken) {
const authUrl = link('auth', networkParams);
return NextResponse.redirect(authUrl);
}
}
}
export const config = {
matcher: '/:network_type/:network_sub_type/account/:path*',
};
......@@ -22,8 +22,8 @@ const moduleExports = {
return [
{
source: '/',
destination: '/xdai/testnet',
permanent: true,
destination: '/poa/core',
permanent: false,
},
];
},
......
import { Center, VStack, Box } from '@chakra-ui/react';
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import type { NextPage, GetStaticPaths } from 'next';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const Home: NextPage = () => {
const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
return (
<Page>
<Center h="100%" fontSize={{ base: 'sm', lg: 'xl' }}>
<VStack gap={ 4 }>
<Box>
<p>home page for { router.query.network_type } { router.query.network_sub_type } network</p>
</Box>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageHeader text={
`Home Page for ${ selectedNetwork?.name } network`
}/>
{ /* will be deleted when we move to new CI */ }
{ isFormVisible && (
<>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Center>
</Page>
);
};
......
......@@ -4,6 +4,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme';
function MyApp({ Component, pageProps }: AppProps) {
......@@ -11,6 +12,16 @@ function MyApp({ Component, pageProps }: AppProps) {
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: (failureCount, _error) => {
const error = _error as ErrorType<{ status: number }>;
const status = error?.error?.status;
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
},
},
},
}));
......
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
const url = getUrlWithNetwork(_req, `api/account/v1/get_csrf`);
const fetch = fetchFactory(_req);
const response = await fetch(url);
if (response.status === 200) {
const token = response.headers.get('x-bs-account-csrf');
res.status(200).json({ token });
return;
}
res.status(500).json({ statusText: response.statusText, status: response.status });
}
......@@ -4,10 +4,11 @@ import type { WatchlistAddresses } from 'types/api/account';
import type { Tokenlist } from 'types/api/tokenlist';
import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/api/fetch';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => {
const fetch = fetchFactory(_req);
const url = getUrlWithNetwork(_req, 'api/account/v1/user/watchlist');
const watchlistResponse = await fetch(url, { method: 'GET' });
......
......@@ -75,6 +75,10 @@ const variantOutline = defineStyle((props) => {
borderColor: props.isActive ? activeBg : 'blue.400',
color: props.isActive ? activeColor : 'blue.400',
},
_disabled: {
color,
borderColor,
},
},
_disabled: {
opacity: 0.2,
......@@ -83,6 +87,10 @@ const variantOutline = defineStyle((props) => {
bg: activeBg,
borderColor: activeBg,
color: activeColor,
_disabled: {
color,
borderColor,
},
},
};
});
......
......@@ -40,6 +40,19 @@ const sizes = {
py: 6,
},
}),
sm: definePartsStyle({
th: {
px: '10px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '10px',
py: 6,
fontSize: 'sm',
fontWeight: 500,
},
}),
};
const variants = {
......
......@@ -3,3 +3,7 @@ import type { WatchlistAddress } from '../api/account';
export type TWatchlistItem = WatchlistAddress & {tokens_count: number};
export type TWatchlist = Array<TWatchlistItem>;
export interface CsrfData {
token: string;
}
......@@ -13,10 +13,10 @@ import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
type Props = {
data?: ApiKey;
......@@ -39,11 +39,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
name: data?.name || '',
},
});
const fetch = useFetch();
const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const updateApiKey = (data: Inputs) => {
const body = JSON.stringify({ name: data.name });
const body = { name: data.name };
if (!data.token) {
return fetch('/api/account/api-keys', { method: 'POST', body });
......
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -15,10 +15,11 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const fetch = useFetch();
const mutationFn = useCallback(() => {
return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' });
}, [ data ]);
}, [ data.api_key, fetch ]);
const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => {
......
......@@ -39,7 +39,7 @@ const Burger = () => {
>
<DrawerOverlay/>
<DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }>
<DrawerBody p={ 6 } display="flex" flexDirection="column">
<Flex alignItems="center" justifyContent="space-between">
<NetworkLogo onClick={ handleNetworkLogoClick }/>
<NetworkMenuButton
......
......@@ -20,9 +20,10 @@ const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT
interface Props {
isCollapsed?: boolean;
hasAccount?: boolean;
}
const NavFooter = ({ isCollapsed }: Props) => {
const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isMobile = useIsMobile();
const width = (() => {
......@@ -33,6 +34,14 @@ const NavFooter = ({ isCollapsed }: Props) => {
return isCollapsed ? '20px' : '180px';
})();
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return isMobile ? 6 : 20;
})();
return (
<VStack
as="footer"
......@@ -41,7 +50,7 @@ const NavFooter = ({ isCollapsed }: Props) => {
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
width={ width }
paddingTop={ isMobile ? 6 : 8 }
marginTop={ isMobile ? 6 : 20 }
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
color="gray.500"
......
......@@ -4,6 +4,7 @@ import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu';
......@@ -13,16 +14,19 @@ import NavLink from './NavLink';
const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const cookieValue = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const [ isCollapsed, setCollapsedState ] = React.useState(cookieValue === 'true');
const [ isCollapsed, setCollapsedState ] = React.useState(navBarCollapsedCookie === 'true');
React.useEffect(() => {
if (!cookieValue) {
if (!navBarCollapsedCookie) {
setCollapsedState(!isLargeScreen);
}
}, [ isLargeScreen, cookieValue ]);
}, [ isLargeScreen, navBarCollapsedCookie ]);
const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag);
......@@ -66,12 +70,14 @@ const NavigationDesktop = () => {
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack>
</Box>
{ hasAccount && (
<Box as="nav" mt={ 12 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack>
</Box>
<NavFooter isCollapsed={ isCollapsed }/>
) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<ChevronLeftIcon
width={ 6 }
height={ 6 }
......
import { Box, VStack } from '@chakra-ui/react';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/blocks/navigation/NavFooter';
import NavLink from 'ui/blocks/navigation/NavLink';
const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
return (
<>
......@@ -15,12 +21,14 @@ const NavigationMobile = () => {
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack>
</Box>
{ isAuth && (
<Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack>
</Box>
<NavFooter/>
) }
<NavFooter hasAccount={ hasAccount }/>
</>
);
};
......
......@@ -14,10 +14,10 @@ import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
......@@ -46,9 +46,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
const queryClient = useQueryClient();
const fetch = useFetch();
const customAbiKey = (data: Inputs & { id?: number }) => {
const body = JSON.stringify({ 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) {
return fetch<CustomAbi, CustomAbiErrors>('/api/account/custom-abis', { method: 'POST', body });
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { space } from 'lib/html-entities';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
......@@ -25,6 +25,7 @@ const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
......@@ -22,6 +22,7 @@ const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
......
......@@ -16,6 +16,7 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState';
interface Tab {
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state';
......@@ -29,7 +30,7 @@ const TABS: Array<Tab> = [
{ type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> },
{ type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> },
{ type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> },
{ type: 'state', routeName: 'tx_state', name: 'State' },
{ type: 'state', routeName: 'tx_state', name: 'State', component: <TxState/> },
{ type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> },
];
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -24,6 +24,7 @@ const WatchList: React.FC = () => {
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>();
......
......@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput';
......@@ -31,6 +31,7 @@ type Inputs = {
}
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch();
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all',
......@@ -45,10 +46,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const queryClient = useQueryClient();
const { mutate } = useMutation((formData: Inputs) => {
const body = JSON.stringify({
const body = {
name: formData?.tag,
address_hash: formData?.address,
});
};
const isEdit = data?.id;
if (isEdit) {
......
......@@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -18,18 +19,19 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const id = data.id;
const queryClient = useQueryClient();
const fetch = useFetch();
const mutationFn = useCallback(() => {
return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' });
}, [ type, id ]);
}, [ fetch, type, id ]);
const onSuccess = useCallback(async() => {
if (type === 'address') {
queryClient.setQueryData([ type ], (prevData: AddressTags | undefined) => {
queryClient.setQueryData([ 'address-tags' ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id);
});
} else {
queryClient.setQueryData([ type ], (prevData: TransactionTags | undefined) => {
queryClient.setQueryData([ 'transaction-tags' ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
});
}
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -23,6 +23,7 @@ const PrivateAddressTags = () => {
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -23,6 +23,7 @@ const PrivateTransactionTags = () => {
const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const fetch = useFetch();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
......
......@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction';
import TagInput from 'ui/shared/TagInput';
import TransactionInput from 'ui/shared/TransactionInput';
......@@ -43,12 +43,13 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
});
const queryClient = useQueryClient();
const fetch = useFetch();
const { mutate } = useMutation((formData: Inputs) => {
const body = JSON.stringify({
const body = {
name: formData?.tag,
transaction_hash: formData?.transaction,
});
};
const isEdit = data?.id;
if (isEdit) {
......
......@@ -5,7 +5,7 @@ import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -22,12 +22,13 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tags = data.tags.split(';');
const queryClient = useQueryClient();
const fetch = useFetch();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const deleteApiKey = useCallback(() => {
const body = JSON.stringify({ remove_reason: reason });
const body = { remove_reason: reason };
return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body });
}, [ data, reason ]);
}, [ data.id, fetch, reason ]);
const onSuccess = useCallback(async() => {
onDeleteSuccess();
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
......@@ -24,6 +24,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags'));
......
......@@ -13,9 +13,9 @@ import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
......@@ -58,6 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const fetch = useFetch();
const inputSize = isMobile ? 'md' : 'lg';
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
......@@ -87,7 +88,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]);
const updatePublicTag = (formData: Inputs) => {
const payload: PublicTagNew = {
const body: PublicTagNew = {
full_name: formData.fullName || '',
email: formData.email || '',
company: formData.companyName || '',
......@@ -97,7 +98,6 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
tags: formData.tags?.split(';').map((s) => s.trim()).join(';') || '',
additional_comment: formData.comment || '',
};
const body = JSON.stringify(payload);
if (!data?.id) {
return fetch<PublicTag, PublicTagErrors>('/api/account/public-tags', { method: 'POST', body });
......
import { Box, HStack, VStack } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import Header from 'ui/blocks/header/Header';
import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop';
......@@ -14,9 +16,13 @@ interface Props {
const Page = ({ children }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const fetch = useFetch();
const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type;
useQuery<unknown, unknown, unknown>([ 'csrf' ], async() => await fetch('/api/account/csrf'));
React.useEffect(() => {
if (typeof networkType === 'string') {
cookies.set(cookies.NAMES.NETWORK_TYPE, networkType);
......
......@@ -10,7 +10,7 @@ const TxInternals = () => {
<Box>
<Filters/>
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px">
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="20%">Type</Th>
......
import {
Accordion,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
import { data } from 'data/txState';
import TxStateTableItem from './state/TxStateTableItem';
const CURRENCY = 'ETH';
const TxState = () => {
return (
<>
<Text>
A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes
</Text>
<Accordion allowToggle allowMultiple>
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ CURRENCY }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
</Accordion>
</>
);
};
export default TxState;
import {
Grid,
GridItem,
Select,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type { TTxStateItemStorage } from 'data/txState';
const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => {
const gridData = [
{ name: 'Storage Address:', value: storageItem.address },
{ name: 'Before:', value: storageItem.before, select: true },
{ name: 'After:', value: storageItem.after, select: true },
];
const backgroundColor = useColorModeValue('white', 'gray.900');
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return (
<Grid
gridTemplateColumns="auto 1fr"
columnGap={ 3 }
rowGap={ 4 }
px={ 6 }
py={ 4 }
background="blackAlpha.50"
borderRadius="12px"
mb={ 4 }
>
{ gridData.map((item) => (
<>
<GridItem alignSelf="center" fontWeight={ 600 } textAlign="end">{ item.name }</GridItem>
<GridItem>
{ item.select && (
<Select
size="sm"
borderRadius="base"
focusBorderColor="none"
display="inline-block"
w="auto"
mr={ 3 }
background={ backgroundColor }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
{ item.value }
</GridItem>
</>
)) }
</Grid>
);
};
export default TxStateStorageItem;
import {
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Text,
Box,
Tr,
Td,
Flex,
Stat,
StatArrow,
Portal,
Link,
Button,
} from '@chakra-ui/react';
import React, { useRef } from 'react';
import type { TTxStateItem } from 'data/txState';
import { nbsp } from 'lib/html-entities';
import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import TxStateStorageItem from './TxStateStorageItem';
const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => {
const ref = useRef<HTMLTableDataCellElement>(null);
const hasStorageData = Boolean(txStateItem.storage?.length);
return (
<>
<AccordionItem as="tr" isDisabled={ !hasStorageData } fontWeight={ 500 } border={ 0 }>
{ ({ isExpanded }) => (
<>
<Td border={ 0 }>
<AccordionButton
_hover={{ background: 'unset' }}
padding="0"
>
<Button
variant="outline"
borderWidth="1px"
// button can't be inside button (AccordionButton)
as="div"
isActive={ isExpanded }
size="sm"
fontWeight={ 400 }
isDisabled={ !hasStorageData }
colorScheme="gray"
// AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }}
>
{ txStateItem.storage?.length || '0' }
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
</Td>
<Td border={ 0 }>
<Flex height="30px" alignItems="center">
<AddressIcon address={ txStateItem.address }/>
<AddressLinkWithTooltip address={ txStateItem.address } fontWeight="500" truncated withCopy={ false } ml={ 2 }/>
</Flex>
</Td>
<Td border={ 0 } lineHeight="30px"><Link>{ txStateItem.miner }</Link></Td>
<Td border={ 0 } isNumeric lineHeight="30px">
<Box>{ txStateItem.after.balance }</Box>
{ typeof txStateItem.after.nonce !== 'undefined' && (
<Box justifyContent="end" display="inline-flex">Nonce: <Text fontWeight={ 600 }>{ nbsp + txStateItem.after.nonce }</Text></Box>
) }
</Td>
<Td border={ 0 } isNumeric lineHeight="30px">{ txStateItem.before.balance }</Td>
<Td border={ 0 } isNumeric lineHeight="30px">
<Stat>
{ txStateItem.diff }
<StatArrow ml={ 2 } type={ Number(txStateItem.diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Td>
{ hasStorageData && (
<Portal containerRef={ ref }>
<AccordionPanel fontWeight={ 500 }>
{ txStateItem.storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel>
</Portal>
) }
</>
) }
</AccordionItem>
<Tr><Td colSpan={ 6 } ref={ ref } padding={ 0 }></Td></Tr>
</>
);
};
export default TxStateTableItem;
......@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import CheckboxInput from 'ui/shared/CheckboxInput';
......@@ -83,9 +83,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
const queryClient = useQueryClient();
const fetch = useFetch();
function updateWatchlist(formData: Inputs) {
const requestParams = {
const body = {
name: formData?.tag,
address_hash: formData?.address,
notification_settings: formData.notification_settings,
......@@ -95,11 +96,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
};
if (data) {
// edit address
return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body: JSON.stringify(requestParams) });
return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body });
} else {
// add address
return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body: JSON.stringify(requestParams) });
return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body });
}
}
......
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import DeleteModal from 'ui/shared/DeleteModal';
......@@ -17,10 +17,11 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const fetch = useFetch();
const mutationFn = useCallback(() => {
return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' });
}, [ data ]);
}, [ data?.id, fetch ]);
const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'watchlist' ], (prevData: TWatchlist | undefined) => {
......
......@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch';
import useFetch from 'lib/hooks/useFetch';
import useToast from 'lib/hooks/useToast';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
......@@ -34,6 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
const toast = useToast();
const fetch = useFetch();
const showToast = useCallback(() => {
toast({
......@@ -49,9 +50,9 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const { mutate } = useMutation(() => {
setSwitchDisabled(true);
const data = { ...item, notification_methods: { email: !notificationEnabled } };
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) });
return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body });
}, {
onError: () => {
showToast();
......
......@@ -1380,10 +1380,10 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3"
integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw==
"@next/eslint-plugin-next@12.1.6":
version "12.1.6"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e"
integrity sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw==
"@next/eslint-plugin-next@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.0.tgz#302c1f03618d5001ce92ea6826c329268759128e"
integrity sha512-jVdq1qYTNDjUtulnE8/hkPv0pHILV4jMg5La99iaY/FFm20WxVnsAZtbNnMvlPbf8dc010oO304SX9yXbg5PAw==
dependencies:
glob "7.1.7"
......@@ -2709,12 +2709,12 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-next@12.1.6:
version "12.1.6"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.1.6.tgz#55097028982dce49159d8753000be3916ac55254"
integrity sha512-qoiS3g/EPzfCTkGkaPBSX9W0NGE/B1wNO3oWrd76QszVGrdpLggNqcO8+LR6MB0CNqtp9Q8NoeVrxNVbzM9hqA==
eslint-config-next@^12.3.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.0.tgz#d887ab2d143fe1a2b308e9321e932a613e610800"
integrity sha512-guHSkNyKnTBB8HU35COgAMeMV0E026BiYRYvyEVVaTOeFcnU3i1EI8/Da0Rl7H3Sgua5FEvoA0vYd2s8kdIUXg==
dependencies:
"@next/eslint-plugin-next" "12.1.6"
"@next/eslint-plugin-next" "12.3.0"
"@rushstack/eslint-patch" "^1.1.3"
"@typescript-eslint/parser" "^5.21.0"
eslint-import-resolver-node "^0.3.6"
......
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