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