Commit 5f575d69 authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge branch 'main' into marketplace-improvements

parents e960b26f 11524381
......@@ -20,6 +20,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:playwright/playwright-test',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
plugins: [
'es5',
......@@ -31,6 +32,7 @@ module.exports = {
'eslint-plugin-import-helpers',
'jest',
'eslint-plugin-no-cyrillic-string',
'@tanstack/query',
],
parser: '@typescript-eslint/parser',
parserOptions: {
......@@ -305,7 +307,7 @@ module.exports = {
},
},
{
files: [ '*.config.ts', 'playwright/**', 'deploy/tools/**', 'middleware.ts', 'nextjs/**' ],
files: [ '*.config.ts', '*.config.js', 'playwright/**', 'deploy/tools/**', 'middleware.ts', 'nextjs/**' ],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
......
......@@ -89,6 +89,7 @@ jobs:
);
if (releases[0].tagName !== process.env.TAG) {
core.info(`Current latest tag: ${ releases[0].tagName }`);
core.setFailed(`Release with tag ${ process.env.TAG } is not latest one.`);
return;
}
......
......@@ -32,3 +32,8 @@ jobs:
label_name: 'pre-release'
label_description: Tasks in pre-release right now
secrets: inherit
upload_source_maps:
name: Upload source maps to Sentry
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
......@@ -78,3 +78,9 @@ jobs:
name: Publish Docker image
uses: './.github/workflows/publish-image.yml'
secrets: inherit
upload_source_maps:
name: Upload source maps to Sentry
needs: publish_image
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
name: Upload source maps to Sentry
on:
workflow_call:
workflow_dispatch:
env:
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
build_and_upload:
name: Build app with source maps and upload to Sentry
runs-on: ubuntu-latest
if: ${{ github.ref_type == 'tag' }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Make production build with source maps
run: yarn build
env:
NODE_ENV: production
GENERATE_SOURCEMAPS: true
- name: Inject Sentry debug ID
run: yarn sentry-cli sourcemaps inject ./.next
- name: Upload source maps to Sentry
run: yarn sentry-cli sourcemaps upload --release=${{ github.ref_name }} --validate ./.next
\ No newline at end of file
......@@ -21,6 +21,7 @@ const config: Feature<{
instance: string;
release: string | undefined;
environment: string;
enableTracing: boolean;
}> = (() => {
if (dsn && instance && environment) {
return Object.freeze({
......@@ -30,6 +31,7 @@ const config: Feature<{
instance,
release,
environment,
enableTracing: getEnvValue('NEXT_PUBLIC_SENTRY_ENABLE_TRACING') === 'true',
});
}
......
......@@ -54,7 +54,8 @@ releases:
name: regcred
type: kubernetes.io/dockerconfigjson
- name: bs-stack
chart: blockscout-ci-cd/blockscout-stack
chart: blockscout/blockscout-stack
version: 1.2.*
namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
labels:
app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
......@@ -78,7 +79,8 @@ releases:
name: regcred
type: kubernetes.io/dockerconfigjson
- name: bs-stack
chart: blockscout-ci-cd/blockscout-stack
chart: blockscout/blockscout-stack
version: 1.2.*
namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
labels:
app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
......
......@@ -37,11 +37,20 @@ get_target_filename() {
local name_suffix="${name_prefix%_URL}"
local name_lc="$(echo "$name_suffix" | tr '[:upper:]' '[:lower:]')"
# Remove query parameters from the URL and get the filename
local filename=$(basename "${url%%\?*}")
# Extract the extension from the filename
local extension="${filename##*.}"
# Check if the URL starts with "file://"
if [[ "$url" == file://* ]]; then
# Extract the local file path
local file_path="${url#file://}"
# Get the filename from the local file path
local filename=$(basename "$file_path")
# Extract the extension from the filename
local extension="${filename##*.}"
else
# Remove query parameters from the URL and get the filename
local filename=$(basename "${url%%\?*}")
# Extract the extension from the filename
local extension="${filename##*.}"
fi
# Convert the extension to lowercase
extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
......@@ -59,19 +68,25 @@ download_and_save_asset() {
# Check if the environment variable is set
if [ -z "${!env_var}" ]; then
echo " [.] Environment variable $env_var is not set. Skipping download."
echo " [.] $env_var: Variable is not set. Skipping download."
return 1
fi
# Download the asset using curl
curl -s -o "$destination" "$url"
# Check if the URL starts with "file://"
if [[ "$url" == file://* ]]; then
# Copy the local file to the destination
cp "${url#file://}" "$destination"
else
# Download the asset using curl
curl -s -o "$destination" "$url"
fi
# Check if the download was successful
if [ $? -eq 0 ]; then
echo " [+] Downloaded $env_var to $destination successfully."
echo " [+] $env_var: Successfully saved file from $url to $destination."
return 0
else
echo " [-] Failed to download $env_var from $url."
echo " [-] $env_var: Failed to save file from $url."
return 1
fi
}
......
......@@ -160,6 +160,12 @@ const sentrySchema = yup
then: (schema) => schema.test(urlTest),
otherwise: (schema) => schema.max(-1, 'SENTRY_CSP_REPORT_URI cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
}),
NEXT_PUBLIC_SENTRY_ENABLE_TRACING: yup
.boolean()
.when('NEXT_PUBLIC_SENTRY_DSN', {
is: (value: string) => Boolean(value),
then: (schema) => schema,
}),
NEXT_PUBLIC_APP_INSTANCE: yup
.string()
.when('NEXT_PUBLIC_SENTRY_DSN', {
......
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
NEXT_PUBLIC_APP_ENV=production
NEXT_PUBLIC_APP_INSTANCE=duck
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
......@@ -530,6 +530,7 @@ For blockchains that implementing SUAVE architecture additional fields will be s
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | Required | - | `<your-secret>` |
| SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `<your-secret>` |
| NEXT_PUBLIC_SENTRY_ENABLE_TRACING | `boolean` | Enables tracing and performance monitoring in Sentry.io | - | `false` | `true` |
| NEXT_PUBLIC_APP_ENV | `string` | App env (e.g development, review or production). Passed as `environment` property to Sentry config | - | `production` | `production` |
| NEXT_PUBLIC_APP_INSTANCE | `string` | Name of app instance. Used as custom tag `app_instance` value in the main Sentry scope. If not provided, it will be constructed from `NEXT_PUBLIC_APP_HOST` | - | - | `wonderful_kepler` |
......
......@@ -3,13 +3,13 @@ import type {
UserInfo,
CustomAbis,
PublicTags,
AddressTags,
TransactionTags,
ApiKeys,
WatchlistAddress,
VerifiedAddressResponse,
TokenInfoApplicationConfig,
TokenInfoApplications,
WatchlistResponse,
TransactionTagsResponse,
AddressTagsResponse,
} from 'types/api/account';
import type {
Address,
......@@ -90,20 +90,23 @@ export const RESOURCES = {
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v1/user/watchlist/:id?',
path: '/api/account/v2/user/watchlist/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
public_tags: {
path: '/api/account/v1/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?',
path: '/api/account/v2/user/tags/address/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?',
path: '/api/account/v2/user/tags/transaction/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
api_keys: {
path: '/api/account/v1/user/api_keys/:id?',
......@@ -579,7 +582,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -588,10 +592,10 @@ export type ResourcePayload<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'private_tags_address' ? AddressTagsResponse :
Q extends 'private_tags_tx' ? TransactionTagsResponse :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'watchlist' ? WatchlistResponse :
Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_applications' ? TokenInfoApplications :
......
......@@ -23,12 +23,15 @@ export default function useApiQuery<R extends ResourceName, E = unknown>(
) {
const apiFetch = useApiFetch();
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>(
getResourceKey(resource, { pathParams, queryParams }),
async() => {
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => {
// all errors and error typing is handled by react-query
// so error response will never go to the data
// that's why we are safe here to do type conversion "as Promise<ResourcePayload<R>>"
return apiFetch(resource, { pathParams, queryParams, fetchParams }) as Promise<ResourcePayload<R>>;
}, queryOptions);
},
...queryOptions,
});
}
......@@ -18,7 +18,7 @@ export default function useQueryClientConfig() {
}
return failureCount < 2;
},
useErrorBoundary: (error) => {
throwOnError: (error) => {
const status = getErrorObjStatusCode(error);
// don't catch error for "Too many requests" response
return status === 429;
......
......@@ -10,27 +10,29 @@ import useFetch from 'lib/hooks/useFetch';
export default function useGetCsrfToken() {
const nodeApiFetch = useFetch();
useQuery(getResourceKey('csrf'), async() => {
if (!isNeedProxy()) {
const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
useQuery({
queryKey: getResourceKey('csrf'),
queryFn: async() => {
if (!isNeedProxy()) {
const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
if (!csrfFromHeader) {
Sentry.captureException(new Error('Client fetch failed'), { tags: {
source: 'fetch',
'source.resource': 'csrf',
'status.code': 500,
'status.text': 'Unable to obtain csrf token from header',
} });
return;
}
if (!csrfFromHeader) {
Sentry.captureException(new Error('Client fetch failed'), { tags: {
source: 'fetch',
'source.resource': 'csrf',
'status.code': 500,
'status.text': 'Unable to obtain csrf token from header',
} });
return;
}
return { token: csrfFromHeader };
}
return { token: csrfFromHeader };
}
return nodeApiFetch('/node-api/csrf');
}, {
return nodeApiFetch('/node-api/csrf');
},
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
}
......@@ -8,20 +8,18 @@ const feature = config.features.safe;
export default function useIsSafeAddress(hash: string | undefined): boolean {
const fetch = useFetch();
const { data } = useQuery(
[ 'safe_transaction_api', hash ],
async() => {
const { data } = useQuery({
queryKey: [ 'safe_transaction_api', hash ],
queryFn: async() => {
if (!feature.isEnabled || !hash) {
return Promise.reject();
}
return fetch(`${ feature.apiUrl }/${ hash }`, undefined, { omitSentryErrorLog: true });
},
{
enabled: feature.isEnabled && Boolean(hash),
refetchOnMount: false,
},
);
enabled: feature.isEnabled && Boolean(hash),
refetchOnMount: false,
});
return Boolean(data);
}
......@@ -9,11 +9,22 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
return;
}
const tracesSampleRate: number | undefined = (() => {
if (feature.environment === 'staging') {
return 1;
}
if (feature.environment === 'production' && feature.instance === 'eth') {
return 0.2;
}
})();
return {
environment: feature.environment,
dsn: feature.dsn,
release: feature.release,
enableTracing: false,
enableTracing: feature.enableTracing,
tracesSampleRate,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
......@@ -40,6 +51,10 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
// Relay and WalletConnect errors
'Attempt to connect to relay via',
'WebSocket connection failed for URL: wss://relay.walletconnect.com',
],
denyUrls: [
// Facebook flakiness
......@@ -56,6 +71,12 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
// AD fetch failed errors
/czilladx\.com/i,
/coinzilla\.com/i,
/coinzilla\.io/i,
/slise\.xyz/i,
],
};
})();
......
......@@ -73,4 +73,21 @@ export const FOOTER_LINKS: Array<CustomLinksGroup> = [
],
},
{
title: 'Partners',
links: [
{
text: 'MetaDock',
url: 'https://blocksec.com/metadock',
},
{
text: 'Sourcify',
url: 'https://sourcify.dev/',
},
{
text: 'DRPC',
url: 'https://drpc.org?ref=559183',
},
],
},
];
......@@ -38,6 +38,7 @@ const moduleExports = {
redirects,
headers,
output: 'standalone',
productionBrowserSourceMaps: process.env.GENERATE_SOURCEMAPS === 'true',
};
module.exports = withRoutes(moduleExports);
......@@ -61,7 +61,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
<ReactQueryDevtools/>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
</AppContextProvider>
......
......@@ -32,8 +32,8 @@ export default handler;
export const config = {
api: {
// disable body parser otherwise it is impossible to upload large files (over 1Mb)
// e.g. when verifying a smart contract
bodyParser: false,
bodyParser: {
sizeLimit: '100mb',
},
},
};
......@@ -8,6 +8,14 @@ export interface AddressTag {
export type AddressTags = Array<AddressTag>
export type AddressTagsResponse = {
items: AddressTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export interface ApiKey {
api_key: string;
name: string;
......@@ -48,6 +56,14 @@ export interface TransactionTag {
export type TransactionTags = Array<TransactionTag>
export type TransactionTagsResponse = {
items: TransactionTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export type Transactions = Array<Transaction>
export interface UserInfo {
......@@ -78,6 +94,14 @@ export interface WatchlistAddressNew {
export type WatchlistAddresses = Array<WatchlistAddress>
export type WatchlistResponse = {
items: WatchlistAddresses;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
......
......@@ -11,12 +11,15 @@ export type L2WithdrawalsItem = {
'status': string;
}
export type L2WithdrawalStatus =
'In challenge period' |
'Ready for relay' |
'Relayed' |
'Waiting for state root' |
'Ready to prove';
export const WITHDRAWAL_STATUSES = [
'Waiting for state root',
'Ready to prove',
'In challenge period',
'Ready for relay',
'Relayed',
] as const;
export type L2WithdrawalStatus = typeof WITHDRAWAL_STATUSES[number];
export type L2WithdrawalsResponse = {
items: Array<L2WithdrawalsItem>;
......
......@@ -2,6 +2,7 @@ import type { AddressParam } from './addressParams';
import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { L2WithdrawalStatus } from './l2Withdrawals';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';
......@@ -52,6 +53,9 @@ export type Transaction = {
l1_gas_price?: string;
l1_gas_used?: string;
has_error_in_internal_txs: boolean | null;
// optimism fields
op_withdrawal_status?: L2WithdrawalStatus;
op_l1_transaction_hash?: string;
// SUAVE fields
execution_node?: AddressParam | null;
allowed_peekers?: Array<string>;
......
......@@ -16,7 +16,7 @@ const TAB_LIST_PROPS = {
const AddressContract = ({ tabs }: Props) => {
const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code');
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code' || id.startsWith('read_'));
return (
<RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
);
......
......@@ -10,7 +10,7 @@ interface Props {
}
const AddressCoinBalanceChart = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
const { data, isPending, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { hash: addressHash },
});
......@@ -24,7 +24,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
isError={ isError }
title="Balances"
items={ items }
isLoading={ isLoading }
isLoading={ isPending }
h="300px"
units={ config.chain.currency.symbol }
/>
......
......@@ -6,6 +6,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import type { PaginationParams } from 'ui/shared/pagination/types';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -15,7 +16,7 @@ import AddressCoinBalanceListItem from './AddressCoinBalanceListItem';
import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem';
interface Props {
query: UseQueryResult<AddressCoinBalanceHistoryResponse> & {
query: UseQueryResult<AddressCoinBalanceHistoryResponse, ResourceError<unknown>> & {
pagination: PaginationParams;
};
}
......
import { Alert, Flex } from '@chakra-ui/react';
import React from 'react';
import { useAccount } from 'wagmi';
import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract';
......@@ -16,6 +15,7 @@ import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable';
import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
import useWatchAccount from './useWatchAccount';
interface Props {
addressHash?: string;
......@@ -25,13 +25,13 @@ interface Props {
const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const apiFetch = useApiFetch();
const { address: userAddress } = useAccount();
const account = useWatchAccount();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
from: userAddress,
from: account?.address,
},
queryOptions: {
enabled: Boolean(addressHash),
......@@ -50,11 +50,11 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
args,
method_id: item.method_id,
contract_type: isProxy ? 'proxy' : 'regular',
from: userAddress,
from: account?.address,
},
},
});
}, [ addressHash, apiFetch, isCustomAbi, isProxy, userAddress ]);
}, [ account?.address, addressHash, apiFetch, isCustomAbi, isProxy ]);
const renderItemContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.error) {
......@@ -83,7 +83,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
if (isPending) {
return <ContentLoader/>;
}
......@@ -94,7 +94,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return (
<>
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ account && <ContractConnectWallet/> }
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
</>
......
......@@ -29,7 +29,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
......@@ -99,7 +99,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
if (isPending) {
return <ContentLoader/>;
}
......
import { watchAccount, getAccount } from '@wagmi/core';
import React from 'react';
export function getWalletAccount() {
try {
return getAccount();
} catch (error) {
return null;
}
}
export default function useWatchAccount() {
const [ account, setAccount ] = React.useState(getWalletAccount());
React.useEffect(() => {
if (!account) {
return;
}
return watchAccount(setAccount);
}, [ account ]);
return account;
}
......@@ -53,7 +53,7 @@ describe('function prepareAbi()', () => {
expect(abi).toHaveLength(commonAbi.length);
});
it('if there are two or more methods with the same name, filters out those which inputs are not matched', () => {
it('if there are two or more methods with the same name and inputs length, filters out those which input types are not matched', () => {
const abi = prepareAbi([
...commonAbi,
{
......@@ -75,4 +75,26 @@ describe('function prepareAbi()', () => {
const item = abi.find((item) => 'name' in item ? item.name === method.name : false);
expect(item).toEqual(commonAbi[2]);
});
it('if there are two or more methods with the same name and different inputs length, filters out those which inputs are not matched', () => {
const abi = prepareAbi([
...commonAbi,
{
inputs: [
{ internalType: 'address', name: '_fallbackUser', type: 'address' },
],
name: 'directNativeDeposit',
outputs: [
{ internalType: 'uint256', name: '', type: 'uint256' },
],
stateMutability: 'payable',
type: 'function',
},
], method);
expect(abi).toHaveLength(commonAbi.length);
const item = abi.find((item) => 'name' in item ? item.name === method.name : false);
expect(item).toEqual(commonAbi[2]);
});
});
......@@ -61,6 +61,10 @@ export function prepareAbi(abi: Abi, item: SmartContractWriteMethod): Abi {
return true;
}
if (abiItem.inputs.length !== item.inputs.length) {
return false;
}
return abiItem.inputs.every(({ name, type }) => {
const itemInput = item.inputs.find((input) => input.name === name);
return Boolean(itemInput) && itemInput?.type === type;
......
......@@ -7,11 +7,12 @@ import type { AddressCounters } from 'types/api/address';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
prop: keyof AddressCounters;
query: UseQueryResult<AddressCounters>;
query: UseQueryResult<AddressCounters, ResourceError<unknown>>;
address: string;
onClick: () => void;
isAddressQueryLoading: boolean;
......
......@@ -35,7 +35,7 @@ const TokenSelect = ({ onClick }: Props) => {
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
const { data, isError, isPending, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
......@@ -72,7 +72,7 @@ const TokenSelect = ({ onClick }: Props) => {
handler: handleTokenBalanceMessage,
});
if (isLoading) {
if (isPending) {
return (
<Flex columnGap={ 3 }>
<Skeleton h={ 8 } w="150px" borderRadius="base"/>
......
......@@ -49,12 +49,12 @@ const TokenBalances = () => {
<TokenBalancesItem
name="Net Worth"
value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
/>
<TokenBalancesItem
name={ `${ config.chain.currency.symbol } Balance` }
value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ config.chain.currency.symbol }` }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
/>
<TokenBalancesItem
name="Tokens"
......@@ -62,7 +62,7 @@ const TokenBalances = () => {
`${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
tokensNumText
}
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
/>
</Flex>
);
......
......@@ -49,7 +49,7 @@ export default function useFetchTokens({ hash }: Props) {
}, [ erc1155query.data, erc20query.data, erc721query.data ]);
return {
isLoading: erc20query.isLoading || erc721query.isLoading || erc1155query.isLoading,
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data,
refetch,
......
......@@ -57,7 +57,8 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
};
const mutation = useMutation(updateApiKey, {
const mutation = useMutation({
mutationFn: updateApiKey,
onSuccess: async(data) => {
const response = data as unknown as ApiKey;
......@@ -148,7 +149,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
......
......@@ -21,7 +21,7 @@ const hooksConfig = {
test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
const query = {
data: blockMock.base,
isLoading: false,
isPending: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
......@@ -39,7 +39,7 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
test('genesis block', async({ mount, page }) => {
const query = {
data: blockMock.genesis,
isLoading: false,
isPending: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
......@@ -62,7 +62,7 @@ const customFieldsTest = test.extend({
customFieldsTest('rootstock custom fields', async({ mount, page }) => {
const query = {
data: blockMock.rootstock,
isLoading: false,
isPending: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
......
......@@ -63,7 +63,8 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const mutation = useMutation(customAbiKey, {
const mutation = useMutation({
mutationFn: customAbiKey,
onSuccess: (data) => {
const response = data as unknown as CustomAbi;
queryClient.setQueryData([ resourceKey('custom_abi') ], (prevData: CustomAbis | undefined) => {
......@@ -175,7 +176,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Create custom ABI' }
</Button>
......
......@@ -11,10 +11,10 @@ import ChainIndicatorChart from './ChainIndicatorChart';
type Props = UseQueryResult<TimeChartData>;
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => {
const content = (() => {
if (isLoading) {
if (isPending) {
return <ContentLoader mt="auto"/>;
}
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage';
import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
interface Props {
......@@ -14,7 +15,7 @@ interface Props {
icon: React.ReactNode;
isSelected: boolean;
onClick: (id: ChainIndicatorId) => void;
stats: UseQueryResult<HomeStats>;
stats: UseQueryResult<HomeStats, ResourceError<unknown>>;
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
......@@ -33,7 +34,7 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats
return null;
}
if (stats.isLoading) {
if (stats.isPending) {
return (
<Skeleton
h={ 3 }
......
......@@ -41,7 +41,7 @@ const ChainIndicators = () => {
}
const valueTitle = (() => {
if (statsQueryResult.isLoading) {
if (statsQueryResult.isPending) {
return <Skeleton h="48px" w="215px" mt={ 3 } mb={ 4 }/>;
}
......
......@@ -24,15 +24,14 @@ function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, fav
export default function useMarketplaceApps(filter: string, selectedCategoryId: string = MarketplaceCategory.ALL, favoriteApps: Array<string> = []) {
const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(configUrl, undefined, { resource: 'marketplace-apps' }),
{
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
staleTime: Infinity,
enabled: feature.isEnabled,
});
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>({
queryKey: [ 'marketplace-apps' ],
queryFn: async() => apiFetch(configUrl, undefined, { resource: 'marketplace-apps' }),
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
staleTime: Infinity,
enabled: feature.isEnabled,
});
const displayedApps = React.useMemo(() => {
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
......
......@@ -179,7 +179,7 @@ const AddressPageContent = () => {
const titleSecondRow = (
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<AddressEntity
address={{ ...addressQuery.data, name: '' }}
address={{ ...addressQuery.data, hash, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
......@@ -192,7 +192,7 @@ const AddressPageContent = () => {
{ !isLoading && !addressQuery.data?.is_contract && config.features.account.isEnabled && (
<AddressFavoriteButton hash={ hash } watchListId={ addressQuery.data?.watchlist_address_id }/>
) }
<AddressQrCode address={ addressQuery.data } isLoading={ isLoading }/>
<AddressQrCode address={{ hash }} isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
<NetworkExplorers type="address" pathParam={ hash } ml="auto"/>
</Flex>
......
......@@ -64,7 +64,7 @@ const ContractVerification = () => {
return <DataFetchAlert/>;
}
if (configQuery.isLoading || contractQuery.isLoading || isVerifiedContract) {
if (configQuery.isPending || contractQuery.isPending || isVerifiedContract) {
return <ContentLoader/>;
}
......
......@@ -108,7 +108,7 @@ const CsvExport = () => {
return <DataFetchAlert/>;
}
if (addressQuery.isLoading) {
if (addressQuery.isPending) {
return <ContentLoader/>;
}
......
......@@ -31,9 +31,9 @@ const MarketplaceApp = () => {
const router = useRouter();
const id = getQueryParamString(router.query.id);
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
[ 'marketplace-apps', id ],
async() => {
const { isPending, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-apps', id ],
queryFn: async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
if (!Array.isArray(result)) {
throw result;
......@@ -46,12 +46,10 @@ const MarketplaceApp = () => {
return item;
},
{
enabled: feature.isEnabled,
},
);
enabled: feature.isEnabled,
});
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const [ isFrameLoading, setIsFrameLoading ] = useState(isPending);
const { colorMode } = useColorMode();
const handleIframeLoad = useCallback(() => {
......@@ -89,29 +87,32 @@ const MarketplaceApp = () => {
}
return (
<Center
h="100vh"
mx={{ base: -4, lg: -6 }}
>
{ (isFrameLoading) && (
<ContentLoader/>
) }
{ data && (
<Box
allow={ IFRAME_ALLOW_ATTRIBUTE }
ref={ ref }
sandbox={ IFRAME_SANDBOX_ATTRIBUTE }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ data.url }
title={ data.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
<>
{ !isPending && <PageTitle title={ data.title } backLink={ backLink }/> }
<Center
h="100vh"
mx={{ base: -4, lg: -6 }}
>
{ (isFrameLoading) && (
<ContentLoader/>
) }
{ data && (
<Box
allow={ IFRAME_ALLOW_ATTRIBUTE }
ref={ ref }
sandbox={ IFRAME_SANDBOX_ATTRIBUTE }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ data.url }
title={ data.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
</>
);
};
......
......@@ -9,11 +9,11 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => {
const { data, isLoading, isError } = useFetchProfileInfo();
const { data, isPending, isError } = useFetchProfileInfo();
useRedirectForInvalidAuthToken();
const content = (() => {
if (isLoading) {
if (isPending) {
return <ContentLoader/>;
}
......
......@@ -54,7 +54,7 @@ const SearchResultsPageContent = () => {
}
}
!redirectCheckQuery.isLoading && setShowContent(true);
!redirectCheckQuery.isPending && setShowContent(true);
}, [ redirectCheckQuery, router, debouncedSearchTerm, showContent ]);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
......
......@@ -2,24 +2,29 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { WatchlistAddress } from 'types/api/account';
import type { WatchlistAddress, WatchlistResponse } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { WATCH_LIST_ITEM_WITH_TOKEN_INFO } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => {
const { data, isPlaceholderData, isError } = useApiQuery('watchlist', {
queryOptions: {
placeholderData: Array(3).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO),
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'watchlist',
options: {
placeholderData: { items: Array(5).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO), next_page_params: null },
},
});
const queryClient = useQueryClient();
......@@ -42,7 +47,7 @@ const WatchList: React.FC = () => {
}, [ addressModalProps ]);
const onAddOrEditSuccess = useCallback(async() => {
await queryClient.refetchQueries([ resourceKey('watchlist') ]);
await queryClient.refetchQueries({ queryKey: [ resourceKey('watchlist') ] });
setAddressModalData(undefined);
addressModalProps.onClose();
}, [ addressModalProps, queryClient ]);
......@@ -58,9 +63,11 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]);
const onDeleteSuccess = useCallback(async() => {
queryClient.setQueryData([ resourceKey('watchlist') ], (prevData: Array<WatchlistAddress> | undefined) => {
return prevData?.filter((item) => item.id !== deleteModalData?.id);
});
queryClient.setQueryData(getResourceKey('watchlist'), (prevData: WatchlistResponse | undefined) => {
const newItems = prevData?.items.filter((item: WatchlistAddress) => item.id !== deleteModalData?.id);
return { ...prevData, items: newItems };
},
);
}, [ deleteModalData?.id, queryClient ]);
const description = (
......@@ -69,15 +76,17 @@ const WatchList: React.FC = () => {
</AccountPageDescription>
);
if (isError) {
return <DataFetchAlert/>;
}
const content = (() => {
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
{ data?.items.map((item, index) => (
<WatchListItem
key={ item.address_hash + (isPlaceholderData ? index : '') }
item={ item }
......@@ -89,10 +98,11 @@ const WatchList: React.FC = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<WatchlistTable
data={ data }
data={ data?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
......@@ -101,7 +111,13 @@ const WatchList: React.FC = () => {
return (
<>
{ description }
{ Boolean(data?.length) && list }
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -44,22 +44,23 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { mutate } = useMutation((formData: Inputs) => {
const body = {
name: formData?.tag,
address_hash: formData?.address,
};
const { mutate } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
name: formData?.tag,
address_hash: formData?.address,
};
const isEdit = data?.id;
if (isEdit) {
return apiFetch('private_tags_address', {
pathParams: { id: data.id },
fetchParams: { method: 'PUT', body },
});
}
const isEdit = data?.id;
if (isEdit) {
return apiFetch('private_tags_address', {
pathParams: { id: data.id },
fetchParams: { method: 'PUT', body },
});
}
return apiFetch('private_tags_address', { fetchParams: { method: 'POST', body } });
}, {
return apiFetch('private_tags_address', { fetchParams: { method: 'POST', body } });
},
onError: (error: ResourceErrorAccount<AddressTagErrors>) => {
setPending(false);
const errorMap = error.payload?.errors;
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { AddressTags, AddressTag } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
......@@ -16,18 +17,19 @@ interface Props {
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
top: number;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="116px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
......
......@@ -2,10 +2,10 @@ import { Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import type { AddressTag, TransactionTag, AddressTagsResponse, TransactionTagsResponse } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -32,12 +32,15 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const onSuccess = useCallback(async() => {
if (type === 'address') {
queryClient.setQueryData([ resourceKey('private_tags_address') ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_address'), (prevData: AddressTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: AddressTag) => item.id !== id);
return { ...prevData, items: newItems };
});
} else {
queryClient.setQueryData([ resourceKey('private_tags_tx') ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_tx'), (prevData: TransactionTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: TransactionTag) => item.id !== id);
return { ...prevData, items: newItems };
});
}
}, [ type, id, queryClient ]);
......
......@@ -3,11 +3,13 @@ import React, { useCallback, useState } from 'react';
import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem';
......@@ -15,10 +17,11 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => {
const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: {
const { data: addressTagsData, isError, isPlaceholderData, refetch, pagination } = useQueryWithPages({
resourceName: 'private_tags_address',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
placeholderData: { items: Array(5).fill(PRIVATE_TAG_ADDRESS), next_page_params: null },
},
});
......@@ -52,14 +55,10 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);
if (isError) {
return <DataFetchAlert/>;
}
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ addressTagsData?.map((item: AddressTag, index: number) => (
{ addressTagsData?.items.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id + (isPlaceholderData ? index : '') }
......@@ -72,21 +71,34 @@ const PrivateAddressTags = () => {
<Box display={{ base: 'none', lg: 'block' }}>
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
data={ addressTagsData?.items }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
<AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
{ Boolean(addressTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ addressTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -3,10 +3,12 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PRIVATE_TAG_TX } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import DeletePrivateTagModal from './DeletePrivateTagModal';
import TransactionModal from './TransactionModal/TransactionModal';
......@@ -14,10 +16,11 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => {
const { data: transactionTagsData, isPlaceholderData, isError } = useApiQuery('private_tags_tx', {
queryOptions: {
const { data: transactionTagsData, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'private_tags_tx',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_TX),
placeholderData: { items: Array(3).fill(PRIVATE_TAG_TX), next_page_params: null },
},
});
......@@ -54,14 +57,10 @@ const PrivateTransactionTags = () => {
</AccountPageDescription>
);
if (isError) {
return <DataFetchAlert/>;
}
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ transactionTagsData?.map((item, index) => (
{ transactionTagsData?.items.map((item, index) => (
<TransactionTagListItem
key={ item.id + (isPlaceholderData ? index : '') }
item={ item }
......@@ -73,19 +72,31 @@ const PrivateTransactionTags = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<TransactionTagTable
data={ transactionTagsData }
data={ transactionTagsData?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
{ description }
{ Boolean(transactionTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ transactionTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -47,22 +47,23 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
const queryClient = useQueryClient();
const apiFetch = useApiFetch();
const { mutate } = useMutation((formData: Inputs) => {
const body = {
name: formData?.tag,
transaction_hash: formData?.transaction,
};
const isEdit = data?.id;
if (isEdit) {
return apiFetch('private_tags_tx', {
pathParams: { id: data.id },
fetchParams: { method: 'PUT', body },
});
}
return apiFetch('private_tags_tx', { fetchParams: { method: 'POST', body } });
}, {
const { mutate } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
name: formData?.tag,
transaction_hash: formData?.transaction,
};
const isEdit = data?.id;
if (isEdit) {
return apiFetch('private_tags_tx', {
pathParams: { id: data.id },
fetchParams: { method: 'PUT', body },
});
}
return apiFetch('private_tags_tx', { fetchParams: { method: 'POST', body } });
},
onError: (error: ResourceErrorAccount<TransactionTagErrors>) => {
setPending(false);
const errorMap = error.payload?.errors;
......@@ -76,7 +77,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
}
},
onSuccess: async() => {
await queryClient.refetchQueries([ resourceKey('private_tags_tx') ]);
await queryClient.refetchQueries({ queryKey: [ resourceKey('private_tags_tx') ] });
await onSuccess();
onClose();
setPending(false);
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import TransactionTagTableItem from './TransactionTagTableItem';
interface Props {
......@@ -16,18 +17,19 @@ interface Props {
isLoading: boolean;
onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void;
top: number;
}
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="75%">Transaction</Th>
<Th width="25%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item, index) => (
<TransactionTagTableItem
......
......@@ -109,7 +109,8 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
});
};
const mutation = useMutation(updatePublicTag, {
const mutation = useMutation({
mutationFn: updatePublicTag,
onSuccess: async(data) => {
const response = data as unknown as PublicTag;
......@@ -237,7 +238,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
>
Send request
</Button>
......
......@@ -39,7 +39,8 @@ const DeleteModal: React.FC<Props> = ({
onClose();
}, [ onClose, setAlertVisible ]);
const mutation = useMutation(mutationFn, {
const mutation = useMutation({
mutationFn,
onSuccess: async() => {
onSuccess();
onClose();
......@@ -70,7 +71,7 @@ const DeleteModal: React.FC<Props> = ({
<Button
size="lg"
onClick={ onDeleteClick }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
// FIXME: chackra's button is disabled when isLoading
isDisabled={ false }
>
......
......@@ -6,6 +6,7 @@ import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
import config from 'configs/app';
import arrowIcon from 'icons/arrows/east-mini.svg';
import explorerIcon from 'icons/explorer.svg';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import LinkExternal from 'ui/shared/LinkExternal';
interface Props {
......@@ -17,12 +18,14 @@ interface Props {
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const explorersLinks = config.UI.explorers.items
.filter((explorer) => explorer.paths[type])
.map((explorer) => {
const url = new URL(explorer.paths[type] + '/' + pathParam, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
});
const explorersLinks = React.useMemo(() => {
return config.UI.explorers.items
.filter((explorer) => typeof explorer.paths[type] === 'string')
.map((explorer) => {
const url = new URL(stripTrailingSlash(explorer.paths[type] || '') + '/' + pathParam, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
});
}, [ pathParam, type ]);
if (explorersLinks.length === 0) {
return null;
......
......@@ -111,7 +111,7 @@ const TokenTransferTableItem = ({
/>
</Td>
<Td isNumeric verticalAlign="top">
<Skeleton isLoaded={ !isLoading } display="inline-block" my="7px">
<Skeleton isLoaded={ !isLoading } display="inline-block" my="7px" wordBreak="break-all">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td>
......
......@@ -219,8 +219,9 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile }: Props)
};
return (
<Box height={ `${ EDITOR_HEIGHT }px` } sx={ sx }>
<Box height={ `${ EDITOR_HEIGHT }px` } width="100%" sx={ sx } ref={ containerNodeRef }>
<MonacoEditor
className="editor-container"
language={ editorLanguage }
path={ data[index].file_path }
defaultValue={ data[index].source_code }
......
......@@ -13,9 +13,9 @@ export default function useNftMediaType(url: string | null, isEnabled: boolean)
const fetch = useFetch();
const { data } = useQuery<unknown, ResourceError<unknown>, MediaType>(
[ 'nft-media-type', url ],
async() => {
const { data } = useQuery<unknown, ResourceError<unknown>, MediaType>({
queryKey: [ 'nft-media-type', url ],
queryFn: async() => {
if (!url) {
return 'image';
}
......@@ -41,10 +41,9 @@ export default function useNftMediaType(url: string | null, isEnabled: boolean)
return 'image';
}
},
{
enabled: isEnabled && Boolean(url),
staleTime: Infinity,
});
enabled: isEnabled && Boolean(url),
staleTime: Infinity,
});
return data;
}
......@@ -70,7 +70,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryResult = useApiQuery(resourceName, {
pathParams,
queryParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
...options,
......
import { Skeleton } from '@chakra-ui/react';
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import VerificationStep from './VerificationStep';
......@@ -7,13 +7,16 @@ export interface Props<T extends string> {
step: T;
steps: Array<T>;
isLoading?: boolean;
rightSlot?: React.ReactNode;
className?: string;
}
const VerificationSteps = <T extends string>({ step, steps, isLoading }: Props<T>) => {
const VerificationSteps = <T extends string>({ step, steps, isLoading, rightSlot, className }: Props<T>) => {
const currentStepIndex = steps.indexOf(step);
return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
display="flex"
gap={ 2 }
......@@ -21,10 +24,11 @@ const VerificationSteps = <T extends string>({ step, steps, isLoading }: Props<T
flexWrap="wrap"
>
{ steps.map((step, index) => (
<VerificationStep step={ step } isLast={ index === steps.length - 1 } isPassed={ index <= currentStepIndex } key={ step }/>
<VerificationStep step={ step } isLast={ index === steps.length - 1 && !rightSlot } isPassed={ index <= currentStepIndex } key={ step }/>
)) }
{ rightSlot }
</Skeleton>
);
};
export default VerificationSteps;
export default chakra(VerificationSteps);
......@@ -17,7 +17,7 @@ const FOOTER_LINKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FOOTE
const BACKEND_VERSION_API_URL = buildApiUrl('config_backend_version');
const INDEXING_ALERT_API_URL = buildApiUrl('homepage_indexing_status');
base.describe('with custom links, 4 cols', () => {
base.describe('with custom links, max cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
......@@ -64,7 +64,7 @@ base.describe('with custom links, 4 cols', () => {
});
});
base.describe('with custom links, 2 cols', () => {
base.describe('with custom links, min cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
......
......@@ -23,7 +23,7 @@ import FooterLinkItem from './FooterLinkItem';
import IntTxsIndexingStatus from './IntTxsIndexingStatus';
import getApiVersionUrl from './utils/getApiVersionUrl';
const MAX_LINKS_COLUMNS = 3;
const MAX_LINKS_COLUMNS = 4;
const FRONT_VERSION_URL = `https://github.com/blockscout/frontend/tree/${ config.UI.footer.frontendVersion }`;
const FRONT_COMMIT_URL = `https://github.com/blockscout/frontend/commit/${ config.UI.footer.frontendCommit }`;
......@@ -96,13 +96,14 @@ const Footer = () => {
const fetch = useFetch();
const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>(
[ 'footer-links' ],
async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
{
enabled: Boolean(config.UI.footer.links),
staleTime: Infinity,
});
const { isPending, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>({
queryKey: [ 'footer-links' ],
queryFn: async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
enabled: Boolean(config.UI.footer.links),
staleTime: Infinity,
});
const colNum = Math.min(linksData?.length || Infinity, MAX_LINKS_COLUMNS) + 1;
return (
<Flex
......@@ -112,9 +113,9 @@ const Footer = () => {
borderTop="1px solid"
borderColor="divider"
as="footer"
columnGap="100px"
columnGap={{ lg: '32px', xl: '100px' }}
>
<Box flexGrow="1" mb={{ base: 8, lg: 0 }}>
<Box flexGrow="1" mb={{ base: 8, lg: 0 }} minW="195px">
<Flex flexWrap="wrap" columnGap={ 8 } rowGap={ 6 }>
<ColorModeToggler/>
{ !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> }
......@@ -140,28 +141,32 @@ const Footer = () => {
</VStack>
</Box>
<Grid
gap={{ base: 6, lg: 12 }}
gap={{ base: 6, lg: config.UI.footer.links && colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }}
gridTemplateColumns={ config.UI.footer.links ?
{ base: 'repeat(auto-fill, 160px)', lg: `repeat(${ (linksData?.length || MAX_LINKS_COLUMNS) + 1 }, 160px)` } :
{
base: 'repeat(auto-fill, 160px)',
lg: `repeat(${ colNum }, 135px)`,
xl: `repeat(${ colNum }, 160px)`,
} :
'auto'
}
>
<Box minW="160px" w={ config.UI.footer.links ? '160px' : '100%' }>
<Box>
{ config.UI.footer.links && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid
gap={ 1 }
gridTemplateColumns={
config.UI.footer.links ?
'160px' :
'1fr' :
{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(2, 160px)',
lg: 'repeat(3, 160px)',
xl: 'repeat(4, 160px)',
}
}
gridTemplateRows={{
base: 'auto',
lg: config.UI.footer.links ? 'auto' : 'repeat(4, auto)',
lg: config.UI.footer.links ? 'auto' : 'repeat(3, auto)',
xl: config.UI.footer.links ? 'auto' : 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: config.UI.footer.links ? 'row' : 'column' }}
......@@ -170,19 +175,19 @@ const Footer = () => {
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid>
</Box>
{ config.UI.footer.links && isLoading && (
{ config.UI.footer.links && isPending && (
Array.from(Array(3)).map((i, index) => (
<Box minW="160px" key={ index }>
<Skeleton w="120px" h="20px" mb={ 6 }/>
<Box key={ index }>
<Skeleton w="100%" h="20px" mb={ 6 }/>
<VStack spacing={ 5 } alignItems="start" mb={ 2 }>
{ Array.from(Array(5)).map((i, index) => <Skeleton w="160px" h="14px" key={ index }/>) }
{ Array.from(Array(5)).map((i, index) => <Skeleton w="100%" h="14px" key={ index }/>) }
</VStack>
</Box>
))
) }
{ config.UI.footer.links && linksData && (
linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => (
<Box minW="160px" key={ linkGroup.title }>
<Box key={ linkGroup.title }>
<Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
......
......@@ -13,7 +13,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
const IntTxsIndexingStatus = () => {
const { data, isError, isLoading } = useApiQuery('homepage_indexing_status');
const { data, isError, isPending } = useApiQuery('homepage_indexing_status');
const bgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const hintTextcolor = useColorModeValue('black', 'white');
......@@ -42,7 +42,7 @@ const IntTxsIndexingStatus = () => {
handler: handleInternalTxsIndexStatus,
});
if (isError || isLoading) {
if (isError || isPending) {
return null;
}
......
......@@ -18,17 +18,17 @@ const IndexingBlocksAlert = () => {
const cookiesString = appProps.cookies;
const [ hasAlertCookie ] = React.useState(cookies.get(cookies.NAMES.INDEXING_ALERT, cookiesString) === 'true');
const { data, isError, isLoading } = useApiQuery('homepage_indexing_status', {
const { data, isError, isPending } = useApiQuery('homepage_indexing_status', {
queryOptions: {
enabled: !config.UI.indexingAlert.blocks.isHidden,
},
});
React.useEffect(() => {
if (!isLoading && !isError) {
if (!isPending && !isError) {
cookies.set(cookies.NAMES.INDEXING_ALERT, data.finished_indexing_blocks ? 'false' : 'true');
}
}, [ data, isError, isLoading ]);
}, [ data, isError, isPending ]);
const queryClient = useQueryClient();
......@@ -62,7 +62,7 @@ const IndexingBlocksAlert = () => {
return null;
}
if (isLoading) {
if (isPending) {
return hasAlertCookie ? <Skeleton h={{ base: '96px', lg: '48px' }} w="100%"/> : null;
}
......
......@@ -13,21 +13,20 @@ export default function useNetworkMenu() {
const { isOpen, onClose, onOpen, onToggle } = useDisclosure();
const apiFetch = useApiFetch();
const { isLoading, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>(
[ 'featured-network' ],
async() => apiFetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }),
{
enabled: Boolean(config.UI.sidebar.featuredNetworks) && isOpen,
staleTime: Infinity,
});
const { isPending, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>({
queryKey: [ 'featured-network' ],
queryFn: async() => apiFetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }),
enabled: Boolean(config.UI.sidebar.featuredNetworks) && isOpen,
staleTime: Infinity,
});
return React.useMemo(() => ({
isOpen,
onClose,
onOpen,
onToggle,
isLoading,
isPending,
data,
availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)),
}), [ isOpen, onClose, onOpen, onToggle, data, isLoading ]);
}), [ isOpen, onClose, onOpen, onToggle, data, isPending ]);
}
......@@ -13,15 +13,15 @@ type Props = {
};
const ProfileMenuDesktop = ({ isHomePage }: Props) => {
const { data, error, isLoading } = useFetchProfileInfo();
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const [ hasMenu, setHasMenu ] = React.useState(false);
React.useEffect(() => {
if (!isLoading) {
if (!isPending) {
setHasMenu(Boolean(data));
}
}, [ data, error?.status, isLoading ]);
}, [ data, error?.status, isPending ]);
const handleSignInClick = React.useCallback(() => {
mixpanel.logEvent(
......
......@@ -11,7 +11,7 @@ import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { data, error, isLoading } = useFetchProfileInfo();
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const [ hasMenu, setHasMenu ] = React.useState(false);
......@@ -24,10 +24,10 @@ const ProfileMenuMobile = () => {
}, []);
React.useEffect(() => {
if (!isLoading) {
if (!isPending) {
setHasMenu(Boolean(data));
}
}, [ data, error?.status, isLoading ]);
}, [ data, error?.status, isPending ]);
const iconButtonProps: Partial<IconButtonProps> = (() => {
if (hasMenu || !loginUrl) {
......
......@@ -103,7 +103,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
const bgColor = useColorModeValue('white', 'gray.900');
const content = (() => {
if (query.isLoading || marketplaceApps.isPlaceholderData) {
if (query.isPending || marketplaceApps.isPlaceholderData) {
return <ContentLoader text="We are searching, please wait... " fontSize="sm"/>;
}
......
......@@ -71,7 +71,7 @@ const Sol2UmlDiagram = ({ addressHash }: Props) => {
throw Error('Uml diagram fetch error', { cause: contractQuery.error as unknown as Error });
}
if (contractQuery.isLoading || umlQuery.isLoading) {
if (contractQuery.isPending || umlQuery.isPending) {
return <ContentLoader/>;
}
......
......@@ -27,7 +27,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const { data, isLoading, isError } = useApiQuery('stats_line', {
const { data, isPending, isError } = useApiQuery('stats_line', {
pathParams: { id },
queryParams: {
from: startDate,
......@@ -56,7 +56,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
title={ title }
units={ units }
description={ description }
isLoading={ isLoading }
isLoading={ isPending }
minH="230px"
/>
);
......
......@@ -7,6 +7,7 @@ import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/token';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token';
......@@ -18,7 +19,7 @@ import TruncatedValue from 'ui/shared/TruncatedValue';
import TokenNftMarketplaces from './TokenNftMarketplaces';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>;
}
const TokenDetails = ({ tokenQuery }: Props) => {
......
......@@ -5,24 +5,25 @@ import React from 'react';
import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'types/api/token';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import LinkExternal from 'ui/shared/LinkExternal';
import TokenProjectInfo from './TokenProjectInfo';
interface Props {
verifiedInfoQuery: UseQueryResult<TTokenVerifiedInfo>;
verifiedInfoQuery: UseQueryResult<TTokenVerifiedInfo, ResourceError<unknown>>;
}
const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => {
const { data, isLoading, isError } = verifiedInfoQuery;
const { data, isPending, isError } = verifiedInfoQuery;
const content = (() => {
if (!config.features.verifiedTokens.isEnabled) {
return null;
}
if (isLoading) {
if (isPending) {
return (
<>
<Skeleton w="100px" h="30px" borderRadius="base"/>
......
......@@ -112,7 +112,7 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
return <DataFetchAlert/>;
}
if (configQuery.isLoading) {
if (configQuery.isPending) {
return <ContentLoader/>;
}
......
......@@ -54,6 +54,7 @@ import TxDetailsFeePerGas from 'ui/tx/details/TxDetailsFeePerGas';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
import TxDetailsOther from 'ui/tx/details/TxDetailsOther';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
......@@ -156,6 +157,10 @@ const TxDetails = () => {
</Tag>
) }
</DetailsInfoItem>
<TxDetailsWithdrawalStatus
status={ data.op_withdrawal_status }
l1TxHash={ data.op_l1_transaction_hash }
/>
{ data.zkevm_status && (
<DetailsInfoItem
title="Confirmation status"
......
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