Commit 4112b354 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into feat/verified-tokens

parents 8963257c 28f21c4a
......@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......
......@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......
......@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......
......@@ -15,9 +15,11 @@ NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_SMALL_LOGO=
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_IS_L2_NETWORK=false
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.aws-k8s.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.aws-k8s.blockscout.com
......@@ -33,7 +33,7 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
_default: ENC[AES256_GCM,data:jr/MLQ1Rq1xjBVAb/fZFbvak23hEcmRkh5nnG4WNCcORR+xEWhEv7ncP87ClpsfxedDN5pOcFrdUkF7u80OkKA==,iv:TIxP5v9K7mcApYmSKRByKNPwYw0djRWHojYfHvSWqZM=,tag:LSLmwapH044CEzOvlce9Og==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
......@@ -144,8 +144,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-03-08T15:55:49Z"
mac: ENC[AES256_GCM,data:nl/CflZ5t09n3swP1vpR2rVjLVIW+H10/FCImDgeWuwt7F+0Whko3/UrPMypdfJeqiuCjWCmuynqjYqqZn2zjKabvc9bdo/RiQCyrdm7XIA+REA9XnKPnghYjkJYNm5kOLct5zif9Rq4wTfPOIpMRKnYvXrpnmiz5tE/lFXbYMw=,iv:mifwaplxQSJb5Q1CPWgR3hT5bTHTodNAoBEDKtdVLHI=,tag:rCAgVJxV52JV8O4CEZuLRg==,type:str]
lastmodified: "2023-04-18T09:26:42Z"
mac: ENC[AES256_GCM,data:4MdP3Fi3SDxyXjhyI7jogYdigziLkp7oh4M6BthtT2FuUu+XAGnONAdccxULJfXtKS6ue27BtV/pKWhdKeOqKIN23BJERqa4cFgYlyd85XAyL400/fEKEO4nCe2c9VIqNRUQls28/WkRjzRjvzQMMTIRIAJvfrJUYRpIabXWqAo=,iv:Z4shgbH8uJNuoJvmyyQjQ+fZsXuSC3nIGYb6GYO/nac=,tag:IDZvpGiZH41IwAgSidF3xQ==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -29,13 +29,13 @@ import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } f
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type { DepositsResponse } from 'types/api/deposits';
import type { DepositsResponse, DepositsItem } from 'types/api/deposits';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { OutputRootsResponse } from 'types/api/outputRoots';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { SearchRedirectResult, SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type {
TokenCounters,
......@@ -54,7 +54,7 @@ import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type ArrayElement from 'types/utils/ArrayElement';
import type { ArrayElement } from 'types/utils';
import appConfig from 'configs/app/config';
......@@ -386,6 +386,9 @@ export const RESOURCES = {
homepage_blocks: {
path: '/api/v2/main-page/blocks',
},
homepage_deposits: {
path: '/api/v2/main-page/optimism-deposits',
},
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
......@@ -539,6 +542,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<DepositsItem> :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Counters :
Q extends 'stats_lines' ? StatsCharts :
......@@ -576,6 +580,7 @@ Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
Q extends 'search_check_redirect' ? SearchRedirectResult :
Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
......
const old = Number.prototype.toLocaleString;
Number.prototype.toLocaleString = function(locale, ...args) {
return old.call(this, 'en', ...args);
};
export {};
......@@ -14,6 +14,7 @@ SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.NewDeposits |
SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance |
......@@ -42,6 +43,7 @@ export namespace SocketMessage {
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type NewDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
......
......@@ -6,12 +6,12 @@ export default function getConfirmationString(durations: Array<number>) {
const [ lower, upper ] = durations.map((time) => time / 1_000);
if (!upper) {
return `Confirmed within ${ lower } secs`;
return `Confirmed within ${ lower.toLocaleString() } secs`;
}
if (lower === 0) {
return `Confirmed within <= ${ upper } secs`;
return `Confirmed within <= ${ upper.toLocaleString() } secs`;
}
return `Confirmed within ${ lower } - ${ upper } secs`;
return `Confirmed within ${ lower.toLocaleString() } - ${ upper.toLocaleString() } secs`;
}
......@@ -36,6 +36,7 @@ export const token: Address = {
name: null,
private_tags: [],
watchlist_names: [],
watchlist_address_id: null,
public_tags: [],
token: tokenInfo,
block_number_balance_updated_at: 8201413,
......@@ -84,6 +85,7 @@ export const contract: Address = {
public_tags: [ privateTag ],
token: null,
watchlist_names: [ watchlistName ],
watchlist_address_id: 42,
};
export const validator: Address = {
......@@ -113,4 +115,5 @@ export const validator: Address = {
public_tags: [],
token: null,
watchlist_names: [],
watchlist_address_id: null,
};
......@@ -8,7 +8,12 @@ export const verified: Partial<SmartContract> = {
constructor_args: 'constructor_args',
creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode',
compiler_settings: 'compiler_settings',
compiler_settings: {
evmVersion: 'london',
remappings: [
'@openzeppelin/=node_modules/@openzeppelin/',
],
},
evm_version: 'default',
is_verified: true,
name: 'WPOA',
......
......@@ -39,5 +39,11 @@ export const withIndexedFields: DecodedInput = {
type: 'uint256',
value: '31567373703130350',
},
{
indexed: true,
name: 'inputArray',
type: 'uint256[2][2]',
value: [ [ '1', '1' ], [ '1', '1' ] ],
},
],
};
......@@ -242,3 +242,11 @@ export const withActionsUniswap: Transaction = {
},
],
};
export const l2tx: Transaction = {
...base,
l1_gas_price: '82702201886',
l1_fee_scalar: '1.0',
l1_gas_used: '17060',
l1_fee: '1584574188135760',
};
......@@ -17,6 +17,8 @@ import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import 'lib/setLocale';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({
......
import type { NextPage } from 'next';
import type { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import appConfig from 'configs/app/config';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
import CsvExport from 'ui/pages/CsvExport';
const CsvExportPage: NextPage = () => {
......@@ -19,4 +22,12 @@ const CsvExportPage: NextPage = () => {
export default CsvExportPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.reCaptcha.siteKey) {
return {
notFound: true,
};
}
return getServerSidePropsBase(args);
};
import type { GetServerSideProps, NextPage } from 'next';
import type { NextPage } from 'next';
import Head from 'next/head';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SearchRedirectResult } from 'types/api/search';
import buildUrlNode from 'lib/api/buildUrlNode';
import fetchFactory from 'lib/api/nodeFetch';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
import * as serverTiming from 'lib/next/serverTiming';
import SearchResults from 'ui/pages/SearchResults';
const SearchResultsPage: NextPage = () => {
......@@ -27,53 +19,4 @@ const SearchResultsPage: NextPage = () => {
export default SearchResultsPage;
export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, resolvedUrl, query }) => {
const start = Date.now();
try {
const q = String(query.q);
const url = buildUrlNode('search_check_redirect', undefined, { q });
const redirectsResponse = await fetchFactory(req)(url, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:next-line timeout property exist for AbortSignal since Node.js 17 - https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
// but @types/node has not updated their types yet, see issue https://github.com/DefinitelyTyped/DefinitelyTyped/issues/60868
signal: AbortSignal.timeout(1_000),
});
const payload = await redirectsResponse.json() as SearchRedirectResult;
if (!payload || typeof payload !== 'object' || !payload.redirect) {
throw Error();
}
const redirectUrl = (() => {
switch (payload.type) {
case 'block': {
return route({ pathname: '/block/[height]', query: { height: q } });
}
case 'address': {
return route({ pathname: '/address/[hash]', query: { hash: payload.parameter || q } });
}
case 'transaction': {
return route({ pathname: '/tx/[hash]', query: { hash: q } });
}
}
})();
if (!redirectUrl) {
throw Error();
}
return {
redirect: {
destination: redirectUrl,
permanent: false,
},
};
} catch (error) {}
const end = Date.now();
serverTiming.appendValue(res, 'query.search.check-redirect', end - start);
return getServerSidePropsBase({ req, res, resolvedUrl, query });
};
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -32,6 +32,7 @@ export interface Address {
private_tags: Array<AddressTag> | null;
public_tags: Array<AddressTag> | null;
token: TokenInfo | null;
watchlist_address_id: number | null;
watchlist_names: Array<WatchlistName> | null;
}
......
......@@ -30,7 +30,10 @@ export interface SmartContract {
file_path: string;
additional_sources: Array<{ file_path: string; source_code: string }>;
external_libraries: Array<SmartContractExternalLibrary> | null;
compiler_settings: unknown;
compiler_settings?: {
evmVersion?: string;
remappings?: Array<string>;
};
verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null;
}
......
......@@ -7,6 +7,6 @@ export interface DecodedInput {
export interface DecodedInputParams {
name: string;
type: string;
value: string;
value: string | Array<unknown> | Record<string, unknown>;
indexed?: boolean;
}
......@@ -42,7 +42,7 @@ export interface TokenInstance {
animation_url: string | null;
external_app_url: string | null;
metadata: Record<string, unknown> | null;
owner: AddressParam;
owner: AddressParam | null;
token: TokenInfo;
}
......
......@@ -9,16 +9,9 @@ export type TransactionRevertReason = {
raw: string;
} | DecodedInput;
export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
export type Transaction = {
to: AddressParam | null;
created_contract: AddressParam | null;
hash: string;
result: string;
confirmations: number;
......@@ -50,6 +43,10 @@ export type Transaction = (
tx_types: Array<TransactionType>;
tx_tag: string | null;
actions: Array<TxAction>;
l1_fee?: string;
l1_fee_scalar?: string;
l1_gas_price?: string;
l1_gas_used?: string;
}
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
export type ArrayElement<ArrayType extends Array<unknown>> =
ArrayType extends Array<(infer ElementType)> ? ElementType : never;
export type ExcludeNull<T> = T extends null ? never : T;
export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>;
type ArrayElement<ArrayType extends Array<unknown>> =
ArrayType extends Array<(infer ElementType)> ? ElementType : never;
export default ArrayElement;
export type ExcludeNull<T> = T extends null ? never : T;
import type { ExcludeNull } from './ExcludeNull';
export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>;
......@@ -4,6 +4,7 @@ import React from 'react';
import type { CsvExportType } from 'types/client/address';
import appConfig from 'configs/app/config';
import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -17,6 +18,10 @@ interface Props {
const AddressCsvExportLink = ({ className, address, type }: Props) => {
const isMobile = useIsMobile();
if (!appConfig.reCaptcha.siteKey) {
return null;
}
return (
<Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal
......
......@@ -56,6 +56,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
implementation_name: null,
implementation_address: null,
token: null,
watchlist_address_id: null,
watchlist_names: null,
creation_tx_hash: null,
block_number_balance_updated_at: null,
......
......@@ -201,6 +201,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
/>
) }
{ Boolean(data.compiler_settings) && (
......
......@@ -20,7 +20,7 @@ interface ResultComponentProps<T extends SmartContractMethod> {
interface Props<T extends SmartContractMethod> {
data: T;
onSubmit: (data: T, args: Array<string | Array<string>>) => Promise<ContractMethodCallResult<T>>;
onSubmit: (data: T, args: Array<string | Array<unknown>>) => Promise<ContractMethodCallResult<T>>;
ResultComponent: (props: ResultComponentProps<T>) => JSX.Element | null;
isWrite?: boolean;
}
......@@ -44,13 +44,23 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s
};
const castFieldValue = (data: Array<SmartContractMethodInput>) => ([ key, value ]: [ string, string ], index: number) => {
if (data[index].type.includes('[]')) {
if (data[index].type.includes('[')) {
return [ key, parseArrayValue(value) ];
}
return [ key, value ];
};
const parseArrayValue = (value: string) => value.replace(/(\[|\])|\s/g, '').split(',');
const parseArrayValue = (value: string) => {
try {
const parsedResult = JSON.parse(value);
if (Array.isArray(parsedResult)) {
return parsedResult;
}
throw new Error('Not an array');
} catch (error) {
return '';
}
};
const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ResultComponent, isWrite }: Props<T>) => {
......
......@@ -64,7 +64,7 @@ const ContractMethodField = ({ control, name, valueType, placeholder, setValue,
/>
<InputRightElement w="auto" right={ 1 }>
{ field.value && <InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick } isDisabled={ isDisabled }/> }
</InputRightElement>
</InputGroup>
</FormControl>
......
......@@ -20,9 +20,10 @@ import { times } from 'lib/html-entities';
interface Props {
onClick: (power: number) => void;
isDisabled?: boolean;
}
const ContractMethodFieldZeroes = ({ onClick }: Props) => {
const ContractMethodFieldZeroes = ({ onClick, isDisabled }: Props) => {
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18);
const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure();
......@@ -60,6 +61,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => {
colorScheme="gray"
display="inline"
onClick={ handleButtonClick }
isDisabled={ isDisabled }
>
{ times }
<chakra.span>10</chakra.span>
......@@ -76,6 +78,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => {
ml={ 1 }
p={ 0 }
onClick={ onToggle }
isDisabled={ isDisabled }
>
<Icon as={ iconEastMini } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } boxSize={ 6 }/>
</Button>
......
......@@ -38,7 +38,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
},
});
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<string>>) => {
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<unknown>>) => {
return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', {
pathParams: { hash: addressHash },
queryParams: {
......
......@@ -16,9 +16,10 @@ interface Props {
isViper: boolean;
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
remappings?: Array<string>;
}
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource }: Props) => {
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings }: Props) => {
const heading = (
<Text fontWeight={ 500 }>
<span>Contract source code</span>
......@@ -56,7 +57,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink }
{ copyToClipboard }
</Flex>
<CodeEditor data={ editorData }/>
<CodeEditor data={ editorData } remappings={ remappings }/>
</section>
);
};
......
......@@ -51,7 +51,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return contract;
})();
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<string>>) => {
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => {
if (!isConnected) {
throw new Error('Wallet is not connected');
}
......
......@@ -2,13 +2,18 @@ import BigNumber from 'bignumber.js';
import config from 'configs/app/config';
export const getNativeCoinValue = (value: string | Array<string>) => {
export const getNativeCoinValue = (value: string | Array<unknown>) => {
const _value = Array.isArray(value) ? value[0] : value;
if (typeof _value !== 'string') {
return '0';
}
return BigNumber(_value).times(10 ** config.network.currency.decimals).toString();
};
export const addZeroesAllowed = (valueType: string) => {
if (valueType.includes('[]')) {
if (valueType.includes('[')) {
return false;
}
......
......@@ -5,7 +5,7 @@ import React from 'react';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import useRedirectIfNotAuth from 'lib/hooks/useRedirectIfNotAuth';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
......@@ -14,23 +14,22 @@ import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
interface Props {
className?: string;
hash: string;
isAdded: boolean;
watchListId: number | null;
}
const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const addModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const redirectIfNotAuth = useRedirectIfNotAuth();
const watchListQuery = useApiQuery('watchlist', { queryOptions: { enabled: isAdded } });
const handleClick = React.useCallback(() => {
if (redirectIfNotAuth()) {
return;
}
isAdded ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, isAdded, redirectIfNotAuth ]);
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, watchListId, redirectIfNotAuth ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
......@@ -49,18 +48,15 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const formData = React.useMemo(() => {
return {
address_hash: hash,
// FIXME temporary solution
// there is no endpoint in api what can return watchlist address info by its hash
// so we look up in the whole watchlist and hope we can find a necessary item
id: watchListQuery.data?.find((address) => address.address?.hash === hash)?.id || '',
id: String(watchListId),
};
}, [ hash, watchListQuery.data ]);
}, [ hash, watchListId ]);
return (
<>
<Tooltip label={ `${ isAdded ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
<Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
<IconButton
isActive={ isAdded }
isActive={ Boolean(watchListId) }
className={ className }
aria-label="edit"
variant="outline"
......@@ -68,7 +64,7 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
pl="6px"
pr="6px"
onClick={ handleClick }
icon={ <Icon as={ isAdded ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
icon={ <Icon as={ watchListId ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
onFocusCapture={ usePreventFocusAfterModalClosing() }
/>
</Tooltip>
......
......@@ -83,7 +83,7 @@ const TokenSelect = ({ onClick }: Props) => {
}
<Tooltip label="Show all tokens">
<Box>
<NextLink href={{ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }} passHref>
<NextLink href={{ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }} passHref legacyBehavior>
<IconButton
aria-label="Show all tokens"
variant="outline"
......
......@@ -55,7 +55,7 @@ const AddressesListItem = ({
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Txn count</Text>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString('en') }</Text>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString() }</Text>
</HStack>
</ListItemMobile>
);
......
......@@ -60,7 +60,7 @@ const AddressesTableItem = ({
</Td>
) }
<Td isNumeric>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString('en') }</Text>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString() }</Text>
</Td>
</Tr>
);
......
......@@ -15,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => {
{ title }
</LinkOverlay>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay>
{ title }
</LinkOverlay>
......
......@@ -27,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => {
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<Button
as="a"
{ ...buttonProps }
......
......@@ -112,7 +112,7 @@ const BlockDetails = ({ query }: Props) => {
title="Size"
hint="Size of the block in bytes"
>
{ data.size.toLocaleString('en') }
{ data.size.toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Timestamp"
......
......@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text>
<Text variant="secondary">{ data.size.toLocaleString() } bytes</Text>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
......
......@@ -53,7 +53,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') }</Td>
<Td fontSize="sm">{ data.size.toLocaleString() }</Td>
<Td fontSize="sm">
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as depositMock from 'mocks/deposits/deposits';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import LatestDeposits from './LatestDeposits';
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_deposits'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(depositMock.data.items),
}));
const component = await mount(
<TestApp>
<LatestDeposits/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem';
import LatestDepositsItemSkeleton from './LatestDepositsItemSkeleton';
const LatestDeposits = () => {
const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_deposits');
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload page.');
}, []);
const handleNewDepositMessage: SocketMessage.NewDeposits['handler'] = React.useCallback((payload) => {
setNum(payload.deposits);
}, [ setNum ]);
const channel = useSocketChannel({
topic: 'optimism_deposits:new_deposits',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'deposits',
handler: handleNewDepositMessage,
});
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(itemsCount)).map((item, index) => <LatestDepositsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
if (data) {
const depositsUrl = route({ pathname: '/deposits' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit"/>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, itemsCount).map((item => <LatestDepositsItem key={ item.l2_tx_hash } item={ item }/>)) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
</Flex>
</>
);
}
return null;
};
export default LatestDeposits;
import {
Box,
Flex,
Grid,
Icon,
Text,
} from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { DepositsItem } from 'types/api/deposits';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
item: DepositsItem;
}
const LatestTxsItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile();
const l1BlockLink = (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 700 }
display="inline-flex"
mr={ 2 }
>
<Icon as={ blockIcon } boxSize="30px" mr={ 1 }/>
{ item.l1_block_number }
</LinkExternal>
);
const l1TxLink = (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%"
display="inline-flex"
alignItems="center"
overflow="hidden"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
);
const l2TxLink = (
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
alignItems="center"
overflow="hidden"
w="100%"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
);
const content = (() => {
if (isMobile) {
return (
<>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink }
<Text variant="secondary">{ timeAgo }</Text>
</Flex>
<Grid gridTemplateColumns="56px auto">
<Text lineHeight="30px">L1 txn</Text>
{ l1TxLink }
<Text lineHeight="30px">L2 txn</Text>
{ l2TxLink }
</Grid>
</>
);
}
return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink }
<Text lineHeight="30px">L1 txn</Text>
{ l1TxLink }
<Text variant="secondary">{ timeAgo }</Text>
<Text lineHeight="30px">L2 txn</Text>
{ l2TxLink }
</Grid>
);
})();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm"
>
{ content }
</Box>
);
};
export default React.memo(LatestTxsItem);
import {
Box,
Flex,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
const LatestTxsItemSkeleton = () => {
const isMobile = useIsMobile();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
{ isMobile && (
<>
<Flex justifyContent="space-between" alignItems="center" mt={ 1 } mb={ 4 }>
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="80px" h="20px"></Skeleton>
</Flex>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
</>
) }
{ !isMobile && (
<>
<Flex w="100%" mb={ 2 } h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex><Flex w="100%" h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex>
</>
) }
</Box>
);
};
export default LatestTxsItemSkeleton;
import { Box, Heading, Flex, Text, Skeleton } from '@chakra-ui/react';
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -18,10 +18,8 @@ const LatestTransactions = () => {
const { num, socketAlert } = useNewTxsSocket();
let content;
if (isLoading) {
content = (
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
......@@ -30,12 +28,12 @@ const LatestTransactions = () => {
}
if (isError) {
content = <Text mt={ 4 }>No data. Please reload page.</Text>;
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
if (data) {
const txsUrl = route({ pathname: '/txs' });
content = (
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
<Box mb={{ base: 3, lg: 4 }}>
......@@ -48,12 +46,7 @@ const LatestTransactions = () => {
);
}
return (
<Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</Box>
);
return null;
};
export default LatestTransactions;
......@@ -97,18 +97,20 @@ const LatestTxsItem = ({ tx }: Props) => {
mx={ 2 }
color="gray.500"
/>
<Address>
<AddressIcon address={ dataTo }/>
<AddressLink
type="address"
hash={ dataTo.hash }
alias={ dataTo.name }
fontWeight="500"
ml={ 2 }
truncation="constant"
fontSize="sm"
/>
</Address>
{ dataTo && (
<Address>
<AddressIcon address={ dataTo }/>
<AddressLink
type="address"
hash={ dataTo.hash }
alias={ dataTo.name }
fontWeight="500"
ml={ 2 }
truncation="constant"
fontSize="sm"
/>
</Address>
) }
</Flex>
<Flex fontSize="sm" justifyContent="end" flexDirection={{ base: 'column', lg: 'row' }}>
<Box mr={{ base: 0, lg: 3 }} mb={{ base: 2, lg: 0 }}>
......
import { Heading, Tab, Tabs, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import LatestDeposits from 'ui/home/LatestDeposits';
import LatestTxs from 'ui/home/LatestTxs';
const TransactionsHome = () => {
if (appConfig.L2.isL2Network) {
return (
<>
<Heading as="h4" size="sm" mb={ 4 }>Transactions</Heading>
<Tabs isLazy lazyBehavior="keepMounted" defaultIndex={ 0 } variant="soft-rounded">
<TabList>
<Tab key="txn">Latest txn</Tab>
<Tab key="deposits">Deposits (L1→L2 txn)</Tab>
</TabList>
<TabPanels mt={ 4 }>
<TabPanel key="txn" p={ 0 }>
<LatestTxs/>
</TabPanel>
<TabPanel key="deposits" p={ 0 }>
<LatestDeposits/>
</TabPanel>
</TabPanels>
</Tabs>
</>
);
}
return (
<>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
<LatestTxs/>
</>
);
};
export default TransactionsHome;
......@@ -12,7 +12,7 @@ import TokenLogo from 'ui/shared/TokenLogo';
const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
id: 'daily_txs',
title: 'Daily transactions',
value: (stats) => Number(stats.transactions_today).toLocaleString('en', { maximumFractionDigits: 2, notation: 'compact' }),
value: (stats) => Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `The total daily number of transactions on the blockchain for the last month.`,
api: {
......@@ -22,7 +22,7 @@ const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc),
name: 'Tx/day',
valueFormatter: (x: number) => x.toLocaleString('en', { maximumFractionDigits: 2, notation: 'compact' }),
valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
} ]),
},
};
......@@ -48,7 +48,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'market_cup',
title: 'Market cap',
value: (stats) => '$' + Number(stats.market_cap).toLocaleString('en', { maximumFractionDigits: 0, notation: 'compact' }),
value: (stats) => '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 0, notation: 'compact' }),
icon: <Icon as={ globeIcon } boxSize={ 6 } bgColor="#6A5DCC" borderRadius="base" color="white"/>,
// eslint-disable-next-line max-len
hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.',
......@@ -59,7 +59,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc),
name: 'Market cap',
valueFormatter: (x: number) => '$' + x.toLocaleString('en', { maximumFractionDigits: 0 }),
valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 }),
} ]),
},
};
......
......@@ -47,7 +47,7 @@ const Deposits = () => {
return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
A total of { countersQuery.data.toLocaleString('en') } deposits found
A total of { countersQuery.data.toLocaleString() } deposits found
</Text>
);
})();
......
......@@ -4,8 +4,8 @@ import React from 'react';
import appConfig from 'configs/app/config';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestTxs from 'ui/home/LatestTxs';
import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner';
import Page from 'ui/shared/Page/Page';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
......@@ -40,7 +40,7 @@ const Home = () => {
pl={ 4 }
>
<ColorModeToggler trackBg="whiteAlpha.500"/>
<ProfileMenuDesktop/>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> }
</Flex>
</Flex>
<LightMode>
......@@ -52,7 +52,9 @@ const Home = () => {
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/>
<LatestTxs/>
<Box flexGrow={ 1 }>
<Transactions/>
</Box>
</Flex>
</Page>
);
......
......@@ -49,7 +49,7 @@ const OutputRoots = () => {
L2 output index
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_output_index } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_output_index } </Text>
(total of { countersQuery.data.toLocaleString('en') } roots)
(total of { countersQuery.data.toLocaleString() } roots)
</Flex>
);
})();
......
......@@ -62,7 +62,7 @@ test('search by address hash +@mobile', async({ mount, page }) => {
test('search by block number +@mobile', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { q: searchMock.block1.block_number },
query: { q: String(searchMock.block1.block_number) },
},
};
await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_number }`, (route) => route.fulfill({
......
import { Box, chakra, Table, Tbody, Tr, Th, Skeleton, Show, Hide } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import type { FormEvent } from 'react';
import React from 'react';
......@@ -17,9 +18,29 @@ import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput';
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => {
const { query, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true);
const router = useRouter();
const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true);
const { data, isError, isLoading, pagination, isPaginationVisible } = query;
React.useEffect(() => {
if (redirectCheckQuery.data?.redirect && redirectCheckQuery.data.parameter) {
switch (redirectCheckQuery.data.type) {
case 'block': {
router.push({ pathname: '/block/[height]', query: { height: redirectCheckQuery.data.parameter } });
return;
}
case 'address': {
router.push({ pathname: '/address/[hash]', query: { hash: redirectCheckQuery.data.parameter } });
return;
}
case 'transaction': {
router.push({ pathname: '/tx/[hash]', query: { hash: redirectCheckQuery.data.parameter } });
return;
}
}
}
}, [ redirectCheckQuery.data, router ]);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
}, [ ]);
......@@ -29,7 +50,7 @@ const SearchResultsPageContent = () => {
return <DataFetchAlert/>;
}
if (isLoading) {
if (isLoading || redirectCheckQuery.isLoading) {
return (
<Box>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
......@@ -70,7 +91,7 @@ const SearchResultsPageContent = () => {
return null;
}
const text = isLoading ? (
const text = isLoading || redirectCheckQuery.isLoading ? (
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/>
) : (
(
......@@ -129,7 +150,10 @@ const SearchResultsPageContent = () => {
return (
<Page renderHeader={ renderHeader }>
<PageTitle text="Search results"/>
{ isLoading || redirectCheckQuery.isLoading ?
<Skeleton h={ 10 } mb={ 6 } w="100%" maxW="222px"/> :
<PageTitle text="Search results"/>
}
{ bar }
{ content }
</Page>
......
......@@ -50,7 +50,7 @@ const TxnBatches = () => {
Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
(total of { countersQuery.data.toLocaleString('en') } batches)
(total of { countersQuery.data.toLocaleString() } batches)
</Flex>
);
})();
......
......@@ -47,7 +47,7 @@ const Withdrawals = () => {
return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
A total of { countersQuery.data.toLocaleString('en') } withdrawals found
A total of { countersQuery.data.toLocaleString() } withdrawals found
</Text>
);
})();
......
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import type { Address } from 'types/api/address';
import type { TokenInfo } from 'types/api/token';
import appConfig from 'configs/app/config';
......@@ -15,7 +15,7 @@ import AddressActionsMenu from 'ui/shared/AddressActions/Menu';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names'>;
address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
}
......@@ -38,7 +38,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
<CopyToClipboard text={ address.hash }/>
{ address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> }
{ !address.is_contract && appConfig.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } isAdded={ Boolean(address.watchlist_names?.length) } ml={ 3 }/>
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) }
<AddressQrCode hash={ address.hash } ml={ 2 }/>
{ appConfig.isAccountSupported && <AddressActionsMenu/> }
......
......@@ -9,7 +9,7 @@ const GasUsedToTargetRatio = ({ value }: Props) => {
return (
<Tooltip label="% of Gas Target">
<Text variant="secondary">
{ (value > 0 ? '+' : '') + value.toLocaleString('en', { maximumFractionDigits: 2 }) }%
{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%
</Text>
</Tooltip>
);
......
......@@ -12,7 +12,7 @@ const LinkInternal = (props: LinkProps, ref: LegacyRef<HTMLAnchorElement>) => {
}
return (
<NextLink href={ props.href as NextLinkProps['href'] } passHref target={ props.target }>
<NextLink href={ props.href as NextLinkProps['href'] } passHref target={ props.target } legacyBehavior>
<Link { ...props } ref={ ref }/>
</NextLink>
);
......
......@@ -7,7 +7,7 @@ interface InjectedProps {
}
interface Props {
type?: 'transaction' | 'token_transfer';
type?: 'transaction' | 'token_transfer' | 'deposit';
children?: (props: InjectedProps) => JSX.Element;
className?: string;
url: string;
......@@ -23,7 +23,19 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
return alert;
}
const name = type === 'token_transfer' ? 'token transfer' : 'transaction';
let name;
switch (type) {
case 'token_transfer':
name = 'token transfer';
break;
case 'deposit':
name = 'deposit';
break;
default:
name = 'transaction';
break;
}
if (!num) {
return `scanning new ${ name }s...`;
......@@ -31,7 +43,7 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
return (
<>
<Link href={ url }>{ num } more { name }{ num > 1 ? 's' : '' }</Link>
<Link href={ url }>{ num.toLocaleString() } more { name }{ num > 1 ? 's' : '' }</Link>
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
</>
);
......
......@@ -11,7 +11,7 @@ interface Props {
const WIDTH = 50;
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString('en', { maximumFractionDigits: 2 }) + '%';
const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString(undefined, { maximumFractionDigits: 2 }) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.400');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
......
......@@ -41,7 +41,7 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
.x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0]))
.curve(d3.curveCatmullRom);
.curve(d3.curveMonotoneX);
return area(data) || undefined;
}, [ xScale, yScale, data ]);
......
......@@ -62,7 +62,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
const line = d3.line<TimeChartItem>()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value))
.curve(d3.curveCatmullRom);
.curve(d3.curveMonotoneX);
return (
<path
......
......@@ -72,7 +72,7 @@ export default function useTimeChartController({ data, width, height }: Props) {
return format(d as Date);
};
const yTickFormat = () => (d: d3.AxisDomain) => Number(d).toLocaleString('en', { maximumFractionDigits: 3, notation: 'compact' });
const yTickFormat = () => (d: d3.AxisDomain) => Number(d).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' });
return {
xTickFormat,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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