Commit f8d90439 authored by tom's avatar tom

Merge branch 'api-changes' into pw-tests

parents 9a595730 e0954498
......@@ -60,9 +60,9 @@ const config = Object.freeze({
name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_NAME),
symbol: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL),
decimals: Number(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS)) || DEFAULT_CURRENCY_DECIMALS,
address: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS),
},
assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME),
nativeTokenAddress: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS),
explorers: parseEnvJson<Array<NetworkExplorer>>(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_EXPLORERS)) || [],
verificationType: process.env.NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE || 'mining',
},
......
......@@ -31,8 +31,9 @@ blockscout:
tls:
enabled: true
path:
# - "/poa/sokol(/|$)(.*)"
- "/"
prefix:
# - "/poa/sokol(/|$)(.*)"
- "/"
# probes
livenessProbe:
enabled: true
......@@ -43,14 +44,14 @@ blockscout:
resources:
limits:
memory:
_default: "1Gi"
_default: "4Gi"
cpu:
_default: "2"
_default: "4"
requests:
memory:
_default: "1Gi"
_default: "4Gi"
cpu:
_default: "2"
_default: "4"
# enable service to connect to RDS
rds:
enabled: false
......@@ -152,17 +153,19 @@ postgres:
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
strategy: Recreate
resources:
limits:
memory:
_default: "1Gi"
_default: "2Gi"
cpu:
_default: "1"
_default: "2"
requests:
memory:
_default: "1Gi"
_default: "2Gi"
cpu:
_default: "1"
_default: "2"
environment:
POSTGRES_USER:
......@@ -434,18 +437,20 @@ frontend:
tls:
enabled: true
path:
# - "/(apps|auth/profile|account)"
- "/"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/account"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
exact:
# - "/(apps|auth/profile|account)"
- "/"
prefix:
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/account"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
resources:
limits:
memory:
......@@ -504,7 +509,7 @@ frontend:
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
_default: stable
NEXT_PUBLIC_APP_INSTANCE:
_default: unknown
NEXT_PUBLIC_API_HOST:
......
......@@ -31,8 +31,9 @@ blockscout:
tls:
enabled: true
path:
prefix:
# - "/poa/sokol(/|$)(.*)"
- "/"
- "/"
# probes
livenessProbe:
enabled: true
......@@ -123,6 +124,8 @@ postgres:
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
strategy: Recreate
resources:
limits:
memory:
......@@ -299,17 +302,20 @@ frontend:
tls:
enabled: true
path:
exact:
# - "/(apps|auth/profile|account)"
- "/"
prefix:
# - "/(apps|auth/profile|account)"
- "/"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
resources:
limits:
memory:
......
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m6 5.293 2.475-2.475.707.707L6.707 6l2.475 2.475-.707.707L6 6.707 3.525 9.182l-.707-.707L5.293 6 2.818 3.525l.707-.707L6 5.293Z" fill="currentColor"/>
</svg>
......@@ -5,10 +5,8 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block';
import { PAGINATION_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch';
......@@ -16,7 +14,7 @@ interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
}
......
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
// todo_tom leave only one api endpoint
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
......
......@@ -2,14 +2,21 @@ import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
export type InternalTransaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
error: string | null;
success: boolean;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: string;
index: number;
block: number;
......
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
......@@ -25,6 +26,13 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters :
Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
......
......@@ -10,4 +10,5 @@ export type Stats = {
gas_prices: {average: number; fast: number; slow: number};
static_gas_price: string;
market_cap: string;
network_utilization_percentage: number;
}
import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric } from './tokenInfo';
import type { TokenInfoGeneric, TokenType } from './tokenInfo';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -47,3 +47,7 @@ export interface TokenTransferResponse {
transaction_hash: string;
} | null;
}
export interface TokenTransferFilters {
type: Array<TokenType>;
}
......@@ -5,10 +5,18 @@ import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
raw: string;
decoded: string;
} | DecodedInput;
export interface Transaction {
export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
hash: string;
result: string;
confirmations: number;
......@@ -17,8 +25,6 @@ export interface Transaction {
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam | null;
created_contract: AddressParam;
value: string;
fee: Fee;
gas_price: string;
......
......@@ -120,7 +120,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}, [ errors, formBackgroundColor ]);
return (
<>
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
{ data && (
<Box marginBottom={ 5 }>
<Controller
......@@ -144,14 +144,14 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }>
<Button
size="lg"
onClick={ handleSubmit(onSubmit) }
type="submit"
disabled={ !isValid }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
</Box>
</>
</form>
);
};
......
import {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
Box, Button, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react';
import NextLink from 'next/link';
......@@ -79,7 +79,9 @@ const AppModal = ({
gridTemplateColumns={{ base: 'auto 1fr' }}
paddingRight={{ sm: 12 }}
>
<Box
<Flex
alignItems="center"
justifyContent="center"
w={{ base: '72px', sm: '144px' }}
h={{ base: '72px', sm: '144px' }}
marginRight={{ base: 6, sm: 8 }}
......@@ -89,7 +91,7 @@ const AppModal = ({
src={ logo }
alt={ `${ title } app icon` }
/>
</Box>
</Flex>
<Heading
as="h2"
......
......@@ -104,7 +104,7 @@ const BlocksContent = ({ type }: Props) => {
return (
<>
{ data ?
{ !isLoading ?
totalText :
<Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
}
......
......@@ -139,7 +139,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}, [ errors, formBackgroundColor ]);
return (
<>
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box>
<Controller
name="contract_address_hash"
......@@ -170,14 +170,14 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }>
<Button
size="lg"
onClick={ handleSubmit(onSubmit) }
type="submit"
disabled={ !isValid }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
</Button>
</Box>
</>
</form>
);
};
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
......@@ -26,10 +27,14 @@ const LatestBlocks = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ],
async() => await fetch(`/api/index/blocks`),
async() => await fetch(`/node-api/index/blocks`),
);
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
() => fetch('/node-api/stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => {
......@@ -78,15 +83,20 @@ const LatestBlocks = () => {
content = (
<>
<Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
{ /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
43.8%
</Text>
</Box>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage && (
<Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
{ /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 6 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
......
......@@ -19,7 +19,7 @@ const LatestTransactions = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ],
async() => await fetch(`/api/index/txs`),
async() => await fetch(`/node-api/index/txs`),
);
let content;
......
......@@ -40,7 +40,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const iconColor = useColorModeValue('blue.600', 'blue.300');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
const isMobile = useIsMobile();
......@@ -100,7 +100,6 @@ const LatestBlocksItem = ({ tx }: Props) => {
type="transaction"
fontWeight="700"
truncation="constant"
target="_self"
/>
</Address>
</Flex>
......
......@@ -29,7 +29,7 @@ const LatestTxsNotice = ({ className }: Props) => {
<>
<Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link href={ txsUrl }>Show in list</Link>
<Link href={ txsUrl }>View all</Link>
</>
);
}
......
......@@ -28,7 +28,7 @@ const Stats = () => {
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
async() => await fetch(`/api/index/stats`),
async() => await fetch(`/node-api/stats`),
);
if (isError) {
......
......@@ -34,7 +34,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
id: 'coin_price',
title: `${ appConfig.network.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
icon: <TokenLogo hash={ appConfig.network.nativeTokenAddress || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>,
icon: <TokenLogo hash={ appConfig.network.currency.address || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>,
hint: `${ appConfig.network.currency.symbol } token daily price in USD.`,
api: {
queryName: QueryKeys.chartsMarket,
......
......@@ -93,7 +93,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}, [ errors, formBackgroundColor ]);
return (
<>
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }>
<Controller
name="address"
......@@ -119,14 +119,14 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }>
<Button
size="lg"
onClick={ handleSubmit(onSubmit) }
type="submit"
disabled={ !isValid }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</>
</form>
);
};
......
......@@ -92,7 +92,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}, [ errors, formBackgroundColor ]);
return (
<>
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }>
<Controller
name="transaction"
......@@ -118,14 +118,14 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Box marginTop={ 8 }>
<Button
size="lg"
onClick={ handleSubmit(onSubmit) }
type="submit"
disabled={ !isValid }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</>
</form>
);
};
......
......@@ -5,6 +5,7 @@ import {
GridItem,
Text,
HStack,
chakra,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
......@@ -151,7 +152,12 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
}, [ changeToDataScreen ]);
return (
<Box width={{ base: 'auto', lg: `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` }} maxWidth="844px">
<chakra.form
noValidate
width={{ base: 'auto', lg: `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` }}
maxWidth="844px"
onSubmit={ handleSubmit(onSubmit) }
>
{ isAlertVisible && <Box mb={ 4 }><FormSubmitAlert/></Box> }
<Text size="sm" variant="secondary" paddingBottom={ 5 }>Company info</Text>
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 5 }>
......@@ -230,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<HStack spacing={ 6 }>
<Button
size="lg"
onClick={ handleSubmit(onSubmit) }
type="submit"
disabled={ !isValid }
isLoading={ mutation.isLoading }
>
......@@ -245,7 +251,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
Cancel
</Button>
</HStack>
</Box>
</chakra.form>
);
};
......
......@@ -6,7 +6,7 @@ import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>;
interface Props {
isActive: boolean;
isActive?: boolean;
appliedFiltersNum?: number;
onClick: () => void;
}
......
import { Input, InputGroup, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import { chakra, Icon, IconButton, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
import crossIcon from 'icons/cross.svg';
import searchIcon from 'icons/search.svg';
type Props = {
......@@ -13,6 +14,7 @@ type Props = {
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
const [ filterQuery, setFilterQuery ] = useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
......@@ -21,6 +23,12 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
onChange(value);
}, [ onChange ]);
const handleFilterQueryClear = useCallback(() => {
setFilterQuery('');
onChange('');
inputRef?.current?.focus();
}, [ onChange ]);
return (
<InputGroup
size={ size }
......@@ -33,6 +41,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
</InputLeftElement>
<Input
ref={ inputRef }
size={ size }
value={ filterQuery }
onChange={ handleFilterQueryChange }
......@@ -40,6 +49,19 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
borderWidth="2px"
textOverflow="ellipsis"
/>
<InputRightElement>
<IconButton
colorScheme="gray"
aria-label="Clear the filter input"
title="Clear the filter input"
w={ 6 }
h={ 6 }
icon={ <Icon as={ crossIcon } w={ 4 } h={ 4 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> }
size="sm"
onClick={ handleFilterQueryClear }
/>
</InputRightElement>
</InputGroup>
);
};
......
......@@ -10,6 +10,7 @@ import {
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
interface Props<TData> {
......@@ -38,8 +39,10 @@ export default function FormModal<TData>({
onClose();
}, [ onClose, setAlertVisible ]);
const isMobile = useIsMobile();
return (
<Modal isOpen={ isOpen } onClose={ onModalClose } size={{ base: 'full', lg: 'md' }}>
<Modal isOpen={ isOpen } onClose={ onModalClose } size={ isMobile ? 'full' : 'md' }>
<ModalOverlay/>
<ModalContent>
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
......
import { Hide, Show, Text, Flex, Skeleton } from '@chakra-ui/react';
import { Hide, Show, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -25,29 +28,34 @@ interface Props {
txHash?: string;
}
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true, txHash }: Props) => {
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => {
const [ filters, setFilters ] = React.useState<Array<TokenType>>([]);
const { isError, isLoading, data, pagination } = useQueryWithPages({
apiPath: path,
queryName,
queryIds,
options: { enabled: !isDisabled },
filters: filters.length ? { type: filters } : undefined,
});
const isPaginatorHidden = pagination.page === 1 && !pagination.hasNextPage;
const handleFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
setFilters(nextValue);
}, []);
const isActionBarHidden = filters.length === 0 && !data?.items.length;
const content = (() => {
if (isLoading || isLoadingProp) {
return (
<>
<Hide below="lg">
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="340px"/> }
<SkeletonTable columns={ showTxInfo ?
[ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] :
[ '185px', '25%', '25%', '25%', '25%' ] }
/>
</Hide>
<Show below="lg">
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo } txHash={ txHash }/>
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo }/>
</Show>
</>
);
......@@ -57,15 +65,19 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return <DataFetchAlert/>;
}
if (!data.items?.length) {
if (!data.items?.length && filters.length === 0) {
return <Text as="span">There are no token transfers</Text>;
}
if (!data.items?.length) {
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ isPaginatorHidden ? 0 : 80 }/>
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 }/>
</Hide>
<Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
......@@ -76,14 +88,9 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return (
<>
{ txHash && (data?.items.length || 0 > 0) && (
<Flex mb={ isPaginatorHidden ? 6 : 0 } w="100%">
<Text as="span" fontWeight={ 600 } whiteSpace="pre">Token transfers for by txn hash: </Text>
<HashStringShorten hash={ txHash }/>
</Flex>
) }
{ isPaginatorHidden ? null : (
<ActionBar>
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<TokenTransferFilter defaultFilters={ filters } onFilterChange={ handleFilterChange } appliedFiltersNum={ filters.length }/>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import FilterButton from 'ui/shared/FilterButton';
import { TOKEN_TYPE } from './helpers';
interface Props {
appliedFiltersNum?: number;
defaultFilters: Array<TokenType>;
onFilterChange: (nextValue: Array<TokenType>) => void;
}
const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w="200px">
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
<Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TokenTransfer);
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: boolean; txHash?: string }) => {
const TokenTransferSkeletonMobile = ({ showTxInfo }: { showTxInfo?: boolean }) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="100%"/> }
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="50px"/>
{ showTxInfo && <Skeleton w="24px" ml="auto"/> }
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="24px"/>
<Skeleton w="90px"/>
</Flex>
{ showTxInfo && (
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="50px"/>
{ showTxInfo && <Skeleton w="24px" ml="auto"/> }
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="24px"/>
<Skeleton w="90px"/>
</Flex>
{ showTxInfo && (
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/>
<Skeleton w="90px"/>
<Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/>
<Skeleton w="90px"/>
</Flex>
)) }
</Box>
</>
</Flex>
)) }
</Box>
);
};
......
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
......@@ -24,3 +25,9 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
return 'Token transfer';
}
};
export const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
......@@ -17,7 +17,7 @@ interface Props {
target?: HTMLAttributeAnchorTarget;
}
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target }: Props) => {
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self' }: Props) => {
let url;
if (type === 'transaction') {
url = link('tx', { id: id || hash });
......@@ -53,7 +53,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
<Link
className={ className }
href={ url }
target={ target || '_blank' }
target={ target }
overflow="hidden"
whiteSpace="nowrap"
>
......
......@@ -86,11 +86,19 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, anchorEl, ...props
.selectAll('.ChartTooltip__point')
.attr('transform', (cur, i) => {
const index = bisectDate(data[i].items, xDate, 1);
const d0 = data[i].items[index - 1];
const d1 = data[i].items[index];
const d = xDate.getTime() - d0?.date.getTime() > d1?.date.getTime() - xDate.getTime() ? d1 : d0;
if (d.date === undefined && d.value === undefined) {
const d0 = data[i].items[index - 1] as TimeChartItem | undefined;
const d1 = data[i].items[index] as TimeChartItem | undefined;
const d = (() => {
if (!d0) {
return d1;
}
if (!d1) {
return d0;
}
return xDate.getTime() - d0.date.getTime() > d1.date.getTime() - xDate.getTime() ? d1 : d0;
})();
if (d?.date === undefined && d?.value === undefined) {
// move point out of container
return 'translate(-100,-100)';
}
......
......@@ -51,7 +51,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }>
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
......
......@@ -58,7 +58,7 @@ const TxDetails = () => {
...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const toAddress = data.to && data.to.hash ? data.to : data.created_contract;
const toAddress = data.to ? data.to : data.created_contract;
const addressToTags = [
...toAddress.private_tags || [],
...toAddress.public_tags || [],
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { TransactionRevertReason } from 'types/api/transaction';
import hexToUtf8 from 'lib/hexToUtf8';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
type Props = TransactionRevertReason;
......@@ -25,7 +26,7 @@ const TxRevertReason = (props: Props) => {
<GridItem fontWeight={ 500 }>Raw:</GridItem>
<GridItem>{ props.raw }</GridItem>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ props.decoded }</GridItem>
<GridItem>{ hexToUtf8(props.raw) }</GridItem>
</Grid>
);
}
......
......@@ -17,7 +17,7 @@ type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to && to.hash ? to : createdContract;
const toData = to ? to : createdContract;
return (
<AccountListItemMobile rowGap={ 3 }>
......
......@@ -15,7 +15,7 @@ type Props = InternalTransaction
const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to && to.hash ? to : createdContract;
const toData = to ? to : createdContract;
return (
<Tr alignItems="top">
......
......@@ -17,7 +17,6 @@ import useTxsSort from './useTxsSort';
type Props = {
queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs;
showDescription?: boolean;
stateFilter?: TTxsFilters['filter'];
apiPath: string;
showBlockInfo?: boolean;
......@@ -25,7 +24,6 @@ type Props = {
const TxsContent = ({
queryName,
showDescription,
stateFilter,
apiPath,
showBlockInfo = true,
......@@ -81,7 +79,6 @@ const TxsContent = ({
return (
<>
{ showDescription && <Box mb={{ base: 6, lg: 12 }}>Only the first 10,000 elements are displayed</Box> }
<TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSortByValue } paginationProps={ pagination } showPagination={ !isPaginatorHidden }/>
{ content }
</>
......
......@@ -33,7 +33,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
return (
<>
......@@ -59,7 +59,6 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
type="transaction"
fontWeight="700"
truncation="constant"
target="_self"
/>
</Address>
</Flex>
......
......@@ -43,7 +43,7 @@ const TxsNewItemNotice = ({ children, className }: Props) => {
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }>
<Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link onClick={ handleClick }>Show in list</Link>
<Link onClick={ handleClick }>View in list</Link>
</Alert>
);
})();
......
......@@ -13,7 +13,6 @@ const TxsTab = ({ tab }: Props) => {
return (
<TxsContent
queryName={ QueryKeys.txsValidate }
showDescription
stateFilter="validated"
apiPath="/node-api/transactions"
/>
......
......@@ -44,7 +44,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
</Address>
);
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const addressTo = (
<Address>
......@@ -87,7 +87,6 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
hash={ tx.hash }
type="transaction"
fontWeight="700"
target="_self"
/>
</Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
......
......@@ -151,7 +151,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
), []);
return (
<>
<form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }>
<Controller
name="address"
......@@ -189,14 +189,14 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }>
<Button
size="lg"
onClick={ handleSubmit(onSubmit) }
type="submit"
isLoading={ pending }
disabled={ !isValid }
>
{ data ? 'Save changes' : 'Add address' }
</Button>
</Box>
</>
</form>
);
};
......
......@@ -20,9 +20,9 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<VStack spacing={ 2 } align="stretch" fontWeight={ 500 } color="gray.700">
<AddressSnippet address={ item.address_hash }/>
<Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }>
{ appConfig.network.nativeTokenAddress && (
{ appConfig.network.currency.address && (
<TokenLogo
hash={ appConfig.network.nativeTokenAddress }
hash={ appConfig.network.currency.address }
name={ appConfig.network.name }
boxSize={ 4 }
borderRadius="sm"
......
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