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

Merge branch 'main' into marketplace-improvements

parents e960b26f 11524381
......@@ -20,6 +20,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:playwright/playwright-test',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
plugins: [
'es5',
......@@ -31,6 +32,7 @@ module.exports = {
'eslint-plugin-import-helpers',
'jest',
'eslint-plugin-no-cyrillic-string',
'@tanstack/query',
],
parser: '@typescript-eslint/parser',
parserOptions: {
......@@ -305,7 +307,7 @@ module.exports = {
},
},
{
files: [ '*.config.ts', 'playwright/**', 'deploy/tools/**', 'middleware.ts', 'nextjs/**' ],
files: [ '*.config.ts', '*.config.js', 'playwright/**', 'deploy/tools/**', 'middleware.ts', 'nextjs/**' ],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
......
......@@ -89,6 +89,7 @@ jobs:
);
if (releases[0].tagName !== process.env.TAG) {
core.info(`Current latest tag: ${ releases[0].tagName }`);
core.setFailed(`Release with tag ${ process.env.TAG } is not latest one.`);
return;
}
......
......@@ -32,3 +32,8 @@ jobs:
label_name: 'pre-release'
label_description: Tasks in pre-release right now
secrets: inherit
upload_source_maps:
name: Upload source maps to Sentry
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
......@@ -78,3 +78,9 @@ jobs:
name: Publish Docker image
uses: './.github/workflows/publish-image.yml'
secrets: inherit
upload_source_maps:
name: Upload source maps to Sentry
needs: publish_image
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
name: Upload source maps to Sentry
on:
workflow_call:
workflow_dispatch:
env:
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
build_and_upload:
name: Build app with source maps and upload to Sentry
runs-on: ubuntu-latest
if: ${{ github.ref_type == 'tag' }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Make production build with source maps
run: yarn build
env:
NODE_ENV: production
GENERATE_SOURCEMAPS: true
- name: Inject Sentry debug ID
run: yarn sentry-cli sourcemaps inject ./.next
- name: Upload source maps to Sentry
run: yarn sentry-cli sourcemaps upload --release=${{ github.ref_name }} --validate ./.next
\ No newline at end of file
......@@ -21,6 +21,7 @@ const config: Feature<{
instance: string;
release: string | undefined;
environment: string;
enableTracing: boolean;
}> = (() => {
if (dsn && instance && environment) {
return Object.freeze({
......@@ -30,6 +31,7 @@ const config: Feature<{
instance,
release,
environment,
enableTracing: getEnvValue('NEXT_PUBLIC_SENTRY_ENABLE_TRACING') === 'true',
});
}
......
......@@ -54,7 +54,8 @@ releases:
name: regcred
type: kubernetes.io/dockerconfigjson
- name: bs-stack
chart: blockscout-ci-cd/blockscout-stack
chart: blockscout/blockscout-stack
version: 1.2.*
namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
labels:
app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
......@@ -78,7 +79,8 @@ releases:
name: regcred
type: kubernetes.io/dockerconfigjson
- name: bs-stack
chart: blockscout-ci-cd/blockscout-stack
chart: blockscout/blockscout-stack
version: 1.2.*
namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
labels:
app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
......
......@@ -37,11 +37,20 @@ get_target_filename() {
local name_suffix="${name_prefix%_URL}"
local name_lc="$(echo "$name_suffix" | tr '[:upper:]' '[:lower:]')"
# Check if the URL starts with "file://"
if [[ "$url" == file://* ]]; then
# Extract the local file path
local file_path="${url#file://}"
# Get the filename from the local file path
local filename=$(basename "$file_path")
# Extract the extension from the filename
local extension="${filename##*.}"
else
# Remove query parameters from the URL and get the filename
local filename=$(basename "${url%%\?*}")
# Extract the extension from the filename
local extension="${filename##*.}"
fi
# Convert the extension to lowercase
extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
......@@ -59,19 +68,25 @@ download_and_save_asset() {
# Check if the environment variable is set
if [ -z "${!env_var}" ]; then
echo " [.] Environment variable $env_var is not set. Skipping download."
echo " [.] $env_var: Variable is not set. Skipping download."
return 1
fi
# Check if the URL starts with "file://"
if [[ "$url" == file://* ]]; then
# Copy the local file to the destination
cp "${url#file://}" "$destination"
else
# Download the asset using curl
curl -s -o "$destination" "$url"
fi
# Check if the download was successful
if [ $? -eq 0 ]; then
echo " [+] Downloaded $env_var to $destination successfully."
echo " [+] $env_var: Successfully saved file from $url to $destination."
return 0
else
echo " [-] Failed to download $env_var from $url."
echo " [-] $env_var: Failed to save file from $url."
return 1
fi
}
......
......@@ -160,6 +160,12 @@ const sentrySchema = yup
then: (schema) => schema.test(urlTest),
otherwise: (schema) => schema.max(-1, 'SENTRY_CSP_REPORT_URI cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
}),
NEXT_PUBLIC_SENTRY_ENABLE_TRACING: yup
.boolean()
.when('NEXT_PUBLIC_SENTRY_DSN', {
is: (value: string) => Boolean(value),
then: (schema) => schema,
}),
NEXT_PUBLIC_APP_INSTANCE: yup
.string()
.when('NEXT_PUBLIC_SENTRY_DSN', {
......
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
NEXT_PUBLIC_APP_ENV=production
NEXT_PUBLIC_APP_INSTANCE=duck
\ No newline at end of file
global:
env: review
fullNameOverride: bs-stack
nameOverride: bs-stack
imagePullSecrets:
- name: regcred
config:
network:
id: 420
name: "Base Göerli"
shortname: Base
currency:
name: Ether
symbol: ETH
decimals: 18
account:
enabled: true
testnet: true
blockscout:
enabled: false
stats:
enabled: false
frontend:
app: blockscout
enabled: true
replicaCount: 1
image:
_default: ghcr.io/blockscout/frontend:review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
pullPolicy: Always
ingress:
enabled: true
host:
_default: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
# enable https
tls:
enabled: true
path:
exact:
# - "/(apps|auth/profile|account)"
- "/"
- "/envs.js"
prefix:
# - "/(apps|auth/profile|account)"
- "/_next"
- "/node-api"
- "/account"
- "/apps"
- "/static"
- "/favicon"
- "/assets"
- "/auth/profile"
- "/auth/unverified-email"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
- "/login"
- "/address"
- "/stats"
- "/search-results"
- "/token"
- "/tokens"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
- "/l2-output-roots"
- "/l2-txn-batches"
- "/l2-withdrawals"
- "/l2-deposits"
annotations:
kubernetes.io/ingress.class: internal-and-public
nginx.ingress.kubernetes.io/proxy-body-size: 500m
nginx.ingress.kubernetes.io/client-max-body-size: "500M"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m"
nginx.ingress.kubernetes.io/proxy-send-timeout: "15m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "15m"
cert-manager.io/cluster-issuer: "zerossl-prod"
hostname: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
resources:
limits:
memory:
_default: 768Mi
cpu:
_default: "1"
memory: 768Mi
cpu: "1"
requests:
memory:
_default: 384Mi
cpu:
_default: 250m
nodeSelector:
enabled: false
environment:
NEXT_PUBLIC_APP_ENV:
_default: development
NEXT_PUBLIC_APP_INSTANCE:
_default: review_L2
NEXT_PUBLIC_NETWORK_NAME:
_default: "Base Göerli"
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Base
NEXT_PUBLIC_NETWORK_ID:
_default: 420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_NETWORK_LOGO:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/base-goerli.json
NEXT_PUBLIC_API_HOST:
_default: blockscout-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_APP_HOST:
_default: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND:
_default: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_WALLETS:
_default: "['coinbase']"
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET:
_default: true
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
_default: true
NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
_default: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST:
_default: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK:
_default: "true"
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_L1_BASE_URL:
_default: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_SENTRY_DSN:
_default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI:
_default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY:
_default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID:
_default: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY:
_default: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_OG_IMAGE_URL:
_default: https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/base-goerli.png?raw=true
memory: 384Mi
cpu: 250m
env:
NEXT_PUBLIC_APP_ENV: development
NEXT_PUBLIC_APP_INSTANCE: review_L2
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/base-goerli.json
NEXT_PUBLIC_API_HOST: blockscout-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST: https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
NEXT_PUBLIC_NETWORK_RPC_URL: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_WALLETS: "['coinbase']"
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: true
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs']"
NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: "true"
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_L1_BASE_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_OG_IMAGE_URL: https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/base-goerli.png?raw=true
global:
env: review
fullNameOverride: bs-stack
nameOverride: bs-stack
imagePullSecrets:
- name: regcred
config:
network:
id: 5
name: Blockscout
shortname: Blockscout
currency:
name: Ether
symbol: ETH
decimals: 18
account:
enabled: true
testnet: true
blockscout:
enabled: false
stats:
enabled: false
frontend:
app: blockscout
enabled: true
replicaCount: 1
image:
_default: ghcr.io/blockscout/frontend:review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
pullPolicy: Always
ingress:
enabled: true
host:
_default: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
# enable https
tls:
enabled: true
path:
exact:
# - "/(apps|auth/profile|account)"
- "/"
- "/envs.js"
prefix:
- "/_next"
- "/node-api"
- "/account"
- "/apps"
- "/static"
- "/assets"
- "/favicon"
- "/assets"
- "/auth/profile"
- "/auth/unverified-email"
- "/txs"
- "/tx"
- "/blocks"
- "/block"
- "/login"
- "/address"
- "/stats"
- "/search-results"
- "/token"
- "/tokens"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
- "/login"
annotations:
kubernetes.io/ingress.class: internal-and-public
nginx.ingress.kubernetes.io/proxy-body-size: 500m
nginx.ingress.kubernetes.io/client-max-body-size: "500M"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m"
nginx.ingress.kubernetes.io/proxy-send-timeout: "15m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "15m"
cert-manager.io/cluster-issuer: "zerossl-prod"
hostname: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
resources:
limits:
memory:
_default: 768Mi
cpu:
_default: "1"
memory: 768Mi
cpu: "1"
requests:
memory:
_default: 384Mi
cpu:
_default: 250m
nodeSelector:
enabled: false
environment:
NEXT_PUBLIC_APP_ENV:
_default: development
NEXT_PUBLIC_APP_INSTANCE:
_default: review
NEXT_PUBLIC_NETWORK_NAME:
_default: Blockscout
NEXT_PUBLIC_NETWORK_ID:
_default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg
NEXT_PUBLIC_API_HOST:
_default: blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-test.k8s-dev.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
_default: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST:
_default: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_AUTH_URL:
_default: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_APP_HOST:
_default: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_IS_TESTNET:
_default: true
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_SENTRY_DSN:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY:
_default: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_WEB3_WALLETS:
_default: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE:
_default: gradient_avatar
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS:
_default: "['top_accounts']"
NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS:
_default: "['value','fee_currency','gas_price','gas_fees','burnt_fees']"
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS:
_default: "['fee_per_gas']"
NEXT_PUBLIC_USE_NEXT_JS_PROXY:
_default: true
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES:
_default: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
\ No newline at end of file
memory: 384Mi
cpu: 250m
env:
NEXT_PUBLIC_APP_ENV: development
NEXT_PUBLIC_APP_INSTANCE: review
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg
NEXT_PUBLIC_API_HOST: blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_STATS_API_HOST: https://stats-test.k8s-dev.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: "['top_accounts']"
NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: "['value','fee_currency','gas_price','gas_fees','burnt_fees']"
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']"
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
......@@ -530,6 +530,7 @@ For blockchains that implementing SUAVE architecture additional fields will be s
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | Required | - | `<your-secret>` |
| SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `<your-secret>` |
| NEXT_PUBLIC_SENTRY_ENABLE_TRACING | `boolean` | Enables tracing and performance monitoring in Sentry.io | - | `false` | `true` |
| NEXT_PUBLIC_APP_ENV | `string` | App env (e.g development, review or production). Passed as `environment` property to Sentry config | - | `production` | `production` |
| NEXT_PUBLIC_APP_INSTANCE | `string` | Name of app instance. Used as custom tag `app_instance` value in the main Sentry scope. If not provided, it will be constructed from `NEXT_PUBLIC_APP_HOST` | - | - | `wonderful_kepler` |
......
......@@ -3,13 +3,13 @@ import type {
UserInfo,
CustomAbis,
PublicTags,
AddressTags,
TransactionTags,
ApiKeys,
WatchlistAddress,
VerifiedAddressResponse,
TokenInfoApplicationConfig,
TokenInfoApplications,
WatchlistResponse,
TransactionTagsResponse,
AddressTagsResponse,
} from 'types/api/account';
import type {
Address,
......@@ -90,20 +90,23 @@ export const RESOURCES = {
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v1/user/watchlist/:id?',
path: '/api/account/v2/user/watchlist/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
public_tags: {
path: '/api/account/v1/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?',
path: '/api/account/v2/user/tags/address/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?',
path: '/api/account/v2/user/tags/transaction/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
api_keys: {
path: '/api/account/v1/user/api_keys/:id?',
......@@ -579,7 +582,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -588,10 +592,10 @@ export type ResourcePayload<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'private_tags_address' ? AddressTagsResponse :
Q extends 'private_tags_tx' ? TransactionTagsResponse :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'watchlist' ? WatchlistResponse :
Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_applications' ? TokenInfoApplications :
......
......@@ -23,12 +23,15 @@ export default function useApiQuery<R extends ResourceName, E = unknown>(
) {
const apiFetch = useApiFetch();
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>(
getResourceKey(resource, { pathParams, queryParams }),
async() => {
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => {
// all errors and error typing is handled by react-query
// so error response will never go to the data
// that's why we are safe here to do type conversion "as Promise<ResourcePayload<R>>"
return apiFetch(resource, { pathParams, queryParams, fetchParams }) as Promise<ResourcePayload<R>>;
}, queryOptions);
},
...queryOptions,
});
}
......@@ -18,7 +18,7 @@ export default function useQueryClientConfig() {
}
return failureCount < 2;
},
useErrorBoundary: (error) => {
throwOnError: (error) => {
const status = getErrorObjStatusCode(error);
// don't catch error for "Too many requests" response
return status === 429;
......
......@@ -10,7 +10,9 @@ import useFetch from 'lib/hooks/useFetch';
export default function useGetCsrfToken() {
const nodeApiFetch = useFetch();
useQuery(getResourceKey('csrf'), async() => {
useQuery({
queryKey: getResourceKey('csrf'),
queryFn: async() => {
if (!isNeedProxy()) {
const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
......@@ -30,7 +32,7 @@ export default function useGetCsrfToken() {
}
return nodeApiFetch('/node-api/csrf');
}, {
},
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
}
......@@ -8,20 +8,18 @@ const feature = config.features.safe;
export default function useIsSafeAddress(hash: string | undefined): boolean {
const fetch = useFetch();
const { data } = useQuery(
[ 'safe_transaction_api', hash ],
async() => {
const { data } = useQuery({
queryKey: [ 'safe_transaction_api', hash ],
queryFn: async() => {
if (!feature.isEnabled || !hash) {
return Promise.reject();
}
return fetch(`${ feature.apiUrl }/${ hash }`, undefined, { omitSentryErrorLog: true });
},
{
enabled: feature.isEnabled && Boolean(hash),
refetchOnMount: false,
},
);
});
return Boolean(data);
}
......@@ -9,11 +9,22 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
return;
}
const tracesSampleRate: number | undefined = (() => {
if (feature.environment === 'staging') {
return 1;
}
if (feature.environment === 'production' && feature.instance === 'eth') {
return 0.2;
}
})();
return {
environment: feature.environment,
dsn: feature.dsn,
release: feature.release,
enableTracing: false,
enableTracing: feature.enableTracing,
tracesSampleRate,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
......@@ -40,6 +51,10 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
// Relay and WalletConnect errors
'Attempt to connect to relay via',
'WebSocket connection failed for URL: wss://relay.walletconnect.com',
],
denyUrls: [
// Facebook flakiness
......@@ -56,6 +71,12 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
// AD fetch failed errors
/czilladx\.com/i,
/coinzilla\.com/i,
/coinzilla\.io/i,
/slise\.xyz/i,
],
};
})();
......
......@@ -73,4 +73,21 @@ export const FOOTER_LINKS: Array<CustomLinksGroup> = [
],
},
{
title: 'Partners',
links: [
{
text: 'MetaDock',
url: 'https://blocksec.com/metadock',
},
{
text: 'Sourcify',
url: 'https://sourcify.dev/',
},
{
text: 'DRPC',
url: 'https://drpc.org?ref=559183',
},
],
},
];
......@@ -38,6 +38,7 @@ const moduleExports = {
redirects,
headers,
output: 'standalone',
productionBrowserSourceMaps: process.env.GENERATE_SOURCEMAPS === 'true',
};
module.exports = withRoutes(moduleExports);
......@@ -61,7 +61,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
<ReactQueryDevtools/>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
</AppContextProvider>
......
......@@ -32,8 +32,8 @@ export default handler;
export const config = {
api: {
// disable body parser otherwise it is impossible to upload large files (over 1Mb)
// e.g. when verifying a smart contract
bodyParser: false,
bodyParser: {
sizeLimit: '100mb',
},
},
};
......@@ -8,6 +8,14 @@ export interface AddressTag {
export type AddressTags = Array<AddressTag>
export type AddressTagsResponse = {
items: AddressTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export interface ApiKey {
api_key: string;
name: string;
......@@ -48,6 +56,14 @@ export interface TransactionTag {
export type TransactionTags = Array<TransactionTag>
export type TransactionTagsResponse = {
items: TransactionTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export type Transactions = Array<Transaction>
export interface UserInfo {
......@@ -78,6 +94,14 @@ export interface WatchlistAddressNew {
export type WatchlistAddresses = Array<WatchlistAddress>
export type WatchlistResponse = {
items: WatchlistAddresses;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
......
......@@ -11,12 +11,15 @@ export type L2WithdrawalsItem = {
'status': string;
}
export type L2WithdrawalStatus =
'In challenge period' |
'Ready for relay' |
'Relayed' |
'Waiting for state root' |
'Ready to prove';
export const WITHDRAWAL_STATUSES = [
'Waiting for state root',
'Ready to prove',
'In challenge period',
'Ready for relay',
'Relayed',
] as const;
export type L2WithdrawalStatus = typeof WITHDRAWAL_STATUSES[number];
export type L2WithdrawalsResponse = {
items: Array<L2WithdrawalsItem>;
......
......@@ -2,6 +2,7 @@ import type { AddressParam } from './addressParams';
import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { L2WithdrawalStatus } from './l2Withdrawals';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';
......@@ -52,6 +53,9 @@ export type Transaction = {
l1_gas_price?: string;
l1_gas_used?: string;
has_error_in_internal_txs: boolean | null;
// optimism fields
op_withdrawal_status?: L2WithdrawalStatus;
op_l1_transaction_hash?: string;
// SUAVE fields
execution_node?: AddressParam | null;
allowed_peekers?: Array<string>;
......
......@@ -16,7 +16,7 @@ const TAB_LIST_PROPS = {
const AddressContract = ({ tabs }: Props) => {
const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code');
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code' || id.startsWith('read_'));
return (
<RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
);
......
......@@ -10,7 +10,7 @@ interface Props {
}
const AddressCoinBalanceChart = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
const { data, isPending, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { hash: addressHash },
});
......@@ -24,7 +24,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
isError={ isError }
title="Balances"
items={ items }
isLoading={ isLoading }
isLoading={ isPending }
h="300px"
units={ config.chain.currency.symbol }
/>
......
......@@ -6,6 +6,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import type { PaginationParams } from 'ui/shared/pagination/types';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -15,7 +16,7 @@ import AddressCoinBalanceListItem from './AddressCoinBalanceListItem';
import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem';
interface Props {
query: UseQueryResult<AddressCoinBalanceHistoryResponse> & {
query: UseQueryResult<AddressCoinBalanceHistoryResponse, ResourceError<unknown>> & {
pagination: PaginationParams;
};
}
......
import { Alert, Flex } from '@chakra-ui/react';
import React from 'react';
import { useAccount } from 'wagmi';
import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract';
......@@ -16,6 +15,7 @@ import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable';
import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
import useWatchAccount from './useWatchAccount';
interface Props {
addressHash?: string;
......@@ -25,13 +25,13 @@ interface Props {
const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const apiFetch = useApiFetch();
const { address: userAddress } = useAccount();
const account = useWatchAccount();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
from: userAddress,
from: account?.address,
},
queryOptions: {
enabled: Boolean(addressHash),
......@@ -50,11 +50,11 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
args,
method_id: item.method_id,
contract_type: isProxy ? 'proxy' : 'regular',
from: userAddress,
from: account?.address,
},
},
});
}, [ addressHash, apiFetch, isCustomAbi, isProxy, userAddress ]);
}, [ account?.address, addressHash, apiFetch, isCustomAbi, isProxy ]);
const renderItemContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.error) {
......@@ -83,7 +83,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
if (isPending) {
return <ContentLoader/>;
}
......@@ -94,7 +94,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return (
<>
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ account && <ContractConnectWallet/> }
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
</>
......
......@@ -29,7 +29,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
......@@ -99,7 +99,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
if (isPending) {
return <ContentLoader/>;
}
......
import { watchAccount, getAccount } from '@wagmi/core';
import React from 'react';
export function getWalletAccount() {
try {
return getAccount();
} catch (error) {
return null;
}
}
export default function useWatchAccount() {
const [ account, setAccount ] = React.useState(getWalletAccount());
React.useEffect(() => {
if (!account) {
return;
}
return watchAccount(setAccount);
}, [ account ]);
return account;
}
......@@ -53,7 +53,7 @@ describe('function prepareAbi()', () => {
expect(abi).toHaveLength(commonAbi.length);
});
it('if there are two or more methods with the same name, filters out those which inputs are not matched', () => {
it('if there are two or more methods with the same name and inputs length, filters out those which input types are not matched', () => {
const abi = prepareAbi([
...commonAbi,
{
......@@ -75,4 +75,26 @@ describe('function prepareAbi()', () => {
const item = abi.find((item) => 'name' in item ? item.name === method.name : false);
expect(item).toEqual(commonAbi[2]);
});
it('if there are two or more methods with the same name and different inputs length, filters out those which inputs are not matched', () => {
const abi = prepareAbi([
...commonAbi,
{
inputs: [
{ internalType: 'address', name: '_fallbackUser', type: 'address' },
],
name: 'directNativeDeposit',
outputs: [
{ internalType: 'uint256', name: '', type: 'uint256' },
],
stateMutability: 'payable',
type: 'function',
},
], method);
expect(abi).toHaveLength(commonAbi.length);
const item = abi.find((item) => 'name' in item ? item.name === method.name : false);
expect(item).toEqual(commonAbi[2]);
});
});
......@@ -61,6 +61,10 @@ export function prepareAbi(abi: Abi, item: SmartContractWriteMethod): Abi {
return true;
}
if (abiItem.inputs.length !== item.inputs.length) {
return false;
}
return abiItem.inputs.every(({ name, type }) => {
const itemInput = item.inputs.find((input) => input.name === name);
return Boolean(itemInput) && itemInput?.type === type;
......
......@@ -7,11 +7,12 @@ import type { AddressCounters } from 'types/api/address';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
prop: keyof AddressCounters;
query: UseQueryResult<AddressCounters>;
query: UseQueryResult<AddressCounters, ResourceError<unknown>>;
address: string;
onClick: () => void;
isAddressQueryLoading: boolean;
......
......@@ -35,7 +35,7 @@ const TokenSelect = ({ onClick }: Props) => {
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
const { data, isError, isPending, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
......@@ -72,7 +72,7 @@ const TokenSelect = ({ onClick }: Props) => {
handler: handleTokenBalanceMessage,
});
if (isLoading) {
if (isPending) {
return (
<Flex columnGap={ 3 }>
<Skeleton h={ 8 } w="150px" borderRadius="base"/>
......
......@@ -49,12 +49,12 @@ const TokenBalances = () => {
<TokenBalancesItem
name="Net Worth"
value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
/>
<TokenBalancesItem
name={ `${ config.chain.currency.symbol } Balance` }
value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ config.chain.currency.symbol }` }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
/>
<TokenBalancesItem
name="Tokens"
......@@ -62,7 +62,7 @@ const TokenBalances = () => {
`${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
tokensNumText
}
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
/>
</Flex>
);
......
......@@ -49,7 +49,7 @@ export default function useFetchTokens({ hash }: Props) {
}, [ erc1155query.data, erc20query.data, erc721query.data ]);
return {
isLoading: erc20query.isLoading || erc721query.isLoading || erc1155query.isLoading,
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data,
refetch,
......
......@@ -57,7 +57,8 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
};
const mutation = useMutation(updateApiKey, {
const mutation = useMutation({
mutationFn: updateApiKey,
onSuccess: async(data) => {
const response = data as unknown as ApiKey;
......@@ -148,7 +149,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
......
......@@ -21,7 +21,7 @@ const hooksConfig = {
test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
const query = {
data: blockMock.base,
isLoading: false,
isPending: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
......@@ -39,7 +39,7 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
test('genesis block', async({ mount, page }) => {
const query = {
data: blockMock.genesis,
isLoading: false,
isPending: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
......@@ -62,7 +62,7 @@ const customFieldsTest = test.extend({
customFieldsTest('rootstock custom fields', async({ mount, page }) => {
const query = {
data: blockMock.rootstock,
isLoading: false,
isPending: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
......
......@@ -63,7 +63,8 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const mutation = useMutation(customAbiKey, {
const mutation = useMutation({
mutationFn: customAbiKey,
onSuccess: (data) => {
const response = data as unknown as CustomAbi;
queryClient.setQueryData([ resourceKey('custom_abi') ], (prevData: CustomAbis | undefined) => {
......@@ -175,7 +176,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
>
{ data ? 'Save' : 'Create custom ABI' }
</Button>
......
......@@ -11,10 +11,10 @@ import ChainIndicatorChart from './ChainIndicatorChart';
type Props = UseQueryResult<TimeChartData>;
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => {
const content = (() => {
if (isLoading) {
if (isPending) {
return <ContentLoader mt="auto"/>;
}
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage';
import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
interface Props {
......@@ -14,7 +15,7 @@ interface Props {
icon: React.ReactNode;
isSelected: boolean;
onClick: (id: ChainIndicatorId) => void;
stats: UseQueryResult<HomeStats>;
stats: UseQueryResult<HomeStats, ResourceError<unknown>>;
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
......@@ -33,7 +34,7 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats
return null;
}
if (stats.isLoading) {
if (stats.isPending) {
return (
<Skeleton
h={ 3 }
......
......@@ -41,7 +41,7 @@ const ChainIndicators = () => {
}
const valueTitle = (() => {
if (statsQueryResult.isLoading) {
if (statsQueryResult.isPending) {
return <Skeleton h="48px" w="215px" mt={ 3 } mb={ 4 }/>;
}
......
......@@ -24,10 +24,9 @@ function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, fav
export default function useMarketplaceApps(filter: string, selectedCategoryId: string = MarketplaceCategory.ALL, favoriteApps: Array<string> = []) {
const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(configUrl, undefined, { resource: 'marketplace-apps' }),
{
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>({
queryKey: [ 'marketplace-apps' ],
queryFn: async() => apiFetch(configUrl, undefined, { resource: 'marketplace-apps' }),
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
staleTime: Infinity,
......
......@@ -179,7 +179,7 @@ const AddressPageContent = () => {
const titleSecondRow = (
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<AddressEntity
address={{ ...addressQuery.data, name: '' }}
address={{ ...addressQuery.data, hash, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
......@@ -192,7 +192,7 @@ const AddressPageContent = () => {
{ !isLoading && !addressQuery.data?.is_contract && config.features.account.isEnabled && (
<AddressFavoriteButton hash={ hash } watchListId={ addressQuery.data?.watchlist_address_id }/>
) }
<AddressQrCode address={ addressQuery.data } isLoading={ isLoading }/>
<AddressQrCode address={{ hash }} isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
<NetworkExplorers type="address" pathParam={ hash } ml="auto"/>
</Flex>
......
......@@ -64,7 +64,7 @@ const ContractVerification = () => {
return <DataFetchAlert/>;
}
if (configQuery.isLoading || contractQuery.isLoading || isVerifiedContract) {
if (configQuery.isPending || contractQuery.isPending || isVerifiedContract) {
return <ContentLoader/>;
}
......
......@@ -108,7 +108,7 @@ const CsvExport = () => {
return <DataFetchAlert/>;
}
if (addressQuery.isLoading) {
if (addressQuery.isPending) {
return <ContentLoader/>;
}
......
......@@ -31,9 +31,9 @@ const MarketplaceApp = () => {
const router = useRouter();
const id = getQueryParamString(router.query.id);
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
[ 'marketplace-apps', id ],
async() => {
const { isPending, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-apps', id ],
queryFn: async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
if (!Array.isArray(result)) {
throw result;
......@@ -46,12 +46,10 @@ const MarketplaceApp = () => {
return item;
},
{
enabled: feature.isEnabled,
},
);
});
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const [ isFrameLoading, setIsFrameLoading ] = useState(isPending);
const { colorMode } = useColorMode();
const handleIframeLoad = useCallback(() => {
......@@ -89,6 +87,8 @@ const MarketplaceApp = () => {
}
return (
<>
{ !isPending && <PageTitle title={ data.title } backLink={ backLink }/> }
<Center
h="100vh"
mx={{ base: -4, lg: -6 }}
......@@ -112,6 +112,7 @@ const MarketplaceApp = () => {
/>
) }
</Center>
</>
);
};
......
......@@ -9,11 +9,11 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => {
const { data, isLoading, isError } = useFetchProfileInfo();
const { data, isPending, isError } = useFetchProfileInfo();
useRedirectForInvalidAuthToken();
const content = (() => {
if (isLoading) {
if (isPending) {
return <ContentLoader/>;
}
......
......@@ -54,7 +54,7 @@ const SearchResultsPageContent = () => {
}
}
!redirectCheckQuery.isLoading && setShowContent(true);
!redirectCheckQuery.isPending && setShowContent(true);
}, [ redirectCheckQuery, router, debouncedSearchTerm, showContent ]);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
......
......@@ -2,24 +2,29 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { WatchlistAddress } from 'types/api/account';
import type { WatchlistAddress, WatchlistResponse } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { WATCH_LIST_ITEM_WITH_TOKEN_INFO } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => {
const { data, isPlaceholderData, isError } = useApiQuery('watchlist', {
queryOptions: {
placeholderData: Array(3).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO),
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'watchlist',
options: {
placeholderData: { items: Array(5).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO), next_page_params: null },
},
});
const queryClient = useQueryClient();
......@@ -42,7 +47,7 @@ const WatchList: React.FC = () => {
}, [ addressModalProps ]);
const onAddOrEditSuccess = useCallback(async() => {
await queryClient.refetchQueries([ resourceKey('watchlist') ]);
await queryClient.refetchQueries({ queryKey: [ resourceKey('watchlist') ] });
setAddressModalData(undefined);
addressModalProps.onClose();
}, [ addressModalProps, queryClient ]);
......@@ -58,9 +63,11 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]);
const onDeleteSuccess = useCallback(async() => {
queryClient.setQueryData([ resourceKey('watchlist') ], (prevData: Array<WatchlistAddress> | undefined) => {
return prevData?.filter((item) => item.id !== deleteModalData?.id);
});
queryClient.setQueryData(getResourceKey('watchlist'), (prevData: WatchlistResponse | undefined) => {
const newItems = prevData?.items.filter((item: WatchlistAddress) => item.id !== deleteModalData?.id);
return { ...prevData, items: newItems };
},
);
}, [ deleteModalData?.id, queryClient ]);
const description = (
......@@ -69,15 +76,17 @@ const WatchList: React.FC = () => {
</AccountPageDescription>
);
if (isError) {
return <DataFetchAlert/>;
}
const content = (() => {
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
{ data?.items.map((item, index) => (
<WatchListItem
key={ item.address_hash + (isPlaceholderData ? index : '') }
item={ item }
......@@ -89,10 +98,11 @@ const WatchList: React.FC = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<WatchlistTable
data={ data }
data={ data?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
......@@ -101,7 +111,13 @@ const WatchList: React.FC = () => {
return (
<>
{ description }
{ Boolean(data?.length) && list }
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -44,7 +44,8 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { mutate } = useMutation((formData: Inputs) => {
const { mutate } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
name: formData?.tag,
address_hash: formData?.address,
......@@ -59,7 +60,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisibl
}
return apiFetch('private_tags_address', { fetchParams: { method: 'POST', body } });
}, {
},
onError: (error: ResourceErrorAccount<AddressTagErrors>) => {
setPending(false);
const errorMap = error.payload?.errors;
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { AddressTags, AddressTag } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
......@@ -16,18 +17,19 @@ interface Props {
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
top: number;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="116px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
......
......@@ -2,10 +2,10 @@ import { Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import type { AddressTag, TransactionTag, AddressTagsResponse, TransactionTagsResponse } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -32,12 +32,15 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const onSuccess = useCallback(async() => {
if (type === 'address') {
queryClient.setQueryData([ resourceKey('private_tags_address') ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_address'), (prevData: AddressTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: AddressTag) => item.id !== id);
return { ...prevData, items: newItems };
});
} else {
queryClient.setQueryData([ resourceKey('private_tags_tx') ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_tx'), (prevData: TransactionTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: TransactionTag) => item.id !== id);
return { ...prevData, items: newItems };
});
}
}, [ type, id, queryClient ]);
......
......@@ -3,11 +3,13 @@ import React, { useCallback, useState } from 'react';
import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem';
......@@ -15,10 +17,11 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => {
const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: {
const { data: addressTagsData, isError, isPlaceholderData, refetch, pagination } = useQueryWithPages({
resourceName: 'private_tags_address',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
placeholderData: { items: Array(5).fill(PRIVATE_TAG_ADDRESS), next_page_params: null },
},
});
......@@ -52,14 +55,10 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);
if (isError) {
return <DataFetchAlert/>;
}
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ addressTagsData?.map((item: AddressTag, index: number) => (
{ addressTagsData?.items.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id + (isPlaceholderData ? index : '') }
......@@ -72,21 +71,34 @@ const PrivateAddressTags = () => {
<Box display={{ base: 'none', lg: 'block' }}>
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
data={ addressTagsData?.items }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
<AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
{ Boolean(addressTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ addressTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -3,10 +3,12 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PRIVATE_TAG_TX } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import DeletePrivateTagModal from './DeletePrivateTagModal';
import TransactionModal from './TransactionModal/TransactionModal';
......@@ -14,10 +16,11 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => {
const { data: transactionTagsData, isPlaceholderData, isError } = useApiQuery('private_tags_tx', {
queryOptions: {
const { data: transactionTagsData, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'private_tags_tx',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_TX),
placeholderData: { items: Array(3).fill(PRIVATE_TAG_TX), next_page_params: null },
},
});
......@@ -54,14 +57,10 @@ const PrivateTransactionTags = () => {
</AccountPageDescription>
);
if (isError) {
return <DataFetchAlert/>;
}
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ transactionTagsData?.map((item, index) => (
{ transactionTagsData?.items.map((item, index) => (
<TransactionTagListItem
key={ item.id + (isPlaceholderData ? index : '') }
item={ item }
......@@ -73,19 +72,31 @@ const PrivateTransactionTags = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<TransactionTagTable
data={ transactionTagsData }
data={ transactionTagsData?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
{ description }
{ Boolean(transactionTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ transactionTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -47,7 +47,8 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
const queryClient = useQueryClient();
const apiFetch = useApiFetch();
const { mutate } = useMutation((formData: Inputs) => {
const { mutate } = useMutation({
mutationFn: (formData: Inputs) => {
const body = {
name: formData?.tag,
transaction_hash: formData?.transaction,
......@@ -62,7 +63,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
}
return apiFetch('private_tags_tx', { fetchParams: { method: 'POST', body } });
}, {
},
onError: (error: ResourceErrorAccount<TransactionTagErrors>) => {
setPending(false);
const errorMap = error.payload?.errors;
......@@ -76,7 +77,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVi
}
},
onSuccess: async() => {
await queryClient.refetchQueries([ resourceKey('private_tags_tx') ]);
await queryClient.refetchQueries({ queryKey: [ resourceKey('private_tags_tx') ] });
await onSuccess();
onClose();
setPending(false);
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import TransactionTagTableItem from './TransactionTagTableItem';
interface Props {
......@@ -16,18 +17,19 @@ interface Props {
isLoading: boolean;
onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void;
top: number;
}
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="75%">Transaction</Th>
<Th width="25%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item, index) => (
<TransactionTagTableItem
......
......@@ -109,7 +109,8 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
});
};
const mutation = useMutation(updatePublicTag, {
const mutation = useMutation({
mutationFn: updatePublicTag,
onSuccess: async(data) => {
const response = data as unknown as PublicTag;
......@@ -237,7 +238,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
size="lg"
type="submit"
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
>
Send request
</Button>
......
......@@ -39,7 +39,8 @@ const DeleteModal: React.FC<Props> = ({
onClose();
}, [ onClose, setAlertVisible ]);
const mutation = useMutation(mutationFn, {
const mutation = useMutation({
mutationFn,
onSuccess: async() => {
onSuccess();
onClose();
......@@ -70,7 +71,7 @@ const DeleteModal: React.FC<Props> = ({
<Button
size="lg"
onClick={ onDeleteClick }
isLoading={ mutation.isLoading }
isLoading={ mutation.isPending }
// FIXME: chackra's button is disabled when isLoading
isDisabled={ false }
>
......
......@@ -6,6 +6,7 @@ import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
import config from 'configs/app';
import arrowIcon from 'icons/arrows/east-mini.svg';
import explorerIcon from 'icons/explorer.svg';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import LinkExternal from 'ui/shared/LinkExternal';
interface Props {
......@@ -17,12 +18,14 @@ interface Props {
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const explorersLinks = config.UI.explorers.items
.filter((explorer) => explorer.paths[type])
const explorersLinks = React.useMemo(() => {
return config.UI.explorers.items
.filter((explorer) => typeof explorer.paths[type] === 'string')
.map((explorer) => {
const url = new URL(explorer.paths[type] + '/' + pathParam, explorer.baseUrl);
const url = new URL(stripTrailingSlash(explorer.paths[type] || '') + '/' + pathParam, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
});
}, [ pathParam, type ]);
if (explorersLinks.length === 0) {
return null;
......
......@@ -111,7 +111,7 @@ const TokenTransferTableItem = ({
/>
</Td>
<Td isNumeric verticalAlign="top">
<Skeleton isLoaded={ !isLoading } display="inline-block" my="7px">
<Skeleton isLoaded={ !isLoading } display="inline-block" my="7px" wordBreak="break-all">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td>
......
......@@ -219,8 +219,9 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile }: Props)
};
return (
<Box height={ `${ EDITOR_HEIGHT }px` } sx={ sx }>
<Box height={ `${ EDITOR_HEIGHT }px` } width="100%" sx={ sx } ref={ containerNodeRef }>
<MonacoEditor
className="editor-container"
language={ editorLanguage }
path={ data[index].file_path }
defaultValue={ data[index].source_code }
......
......@@ -13,9 +13,9 @@ export default function useNftMediaType(url: string | null, isEnabled: boolean)
const fetch = useFetch();
const { data } = useQuery<unknown, ResourceError<unknown>, MediaType>(
[ 'nft-media-type', url ],
async() => {
const { data } = useQuery<unknown, ResourceError<unknown>, MediaType>({
queryKey: [ 'nft-media-type', url ],
queryFn: async() => {
if (!url) {
return 'image';
}
......@@ -41,7 +41,6 @@ export default function useNftMediaType(url: string | null, isEnabled: boolean)
return 'image';
}
},
{
enabled: isEnabled && Boolean(url),
staleTime: Infinity,
});
......
......@@ -70,7 +70,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryResult = useApiQuery(resourceName, {
pathParams,
queryParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
...options,
......
import { Skeleton } from '@chakra-ui/react';
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import VerificationStep from './VerificationStep';
......@@ -7,13 +7,16 @@ export interface Props<T extends string> {
step: T;
steps: Array<T>;
isLoading?: boolean;
rightSlot?: React.ReactNode;
className?: string;
}
const VerificationSteps = <T extends string>({ step, steps, isLoading }: Props<T>) => {
const VerificationSteps = <T extends string>({ step, steps, isLoading, rightSlot, className }: Props<T>) => {
const currentStepIndex = steps.indexOf(step);
return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
display="flex"
gap={ 2 }
......@@ -21,10 +24,11 @@ const VerificationSteps = <T extends string>({ step, steps, isLoading }: Props<T
flexWrap="wrap"
>
{ steps.map((step, index) => (
<VerificationStep step={ step } isLast={ index === steps.length - 1 } isPassed={ index <= currentStepIndex } key={ step }/>
<VerificationStep step={ step } isLast={ index === steps.length - 1 && !rightSlot } isPassed={ index <= currentStepIndex } key={ step }/>
)) }
{ rightSlot }
</Skeleton>
);
};
export default VerificationSteps;
export default chakra(VerificationSteps);
......@@ -17,7 +17,7 @@ const FOOTER_LINKS_URL = app.url + buildExternalAssetFilePath('NEXT_PUBLIC_FOOTE
const BACKEND_VERSION_API_URL = buildApiUrl('config_backend_version');
const INDEXING_ALERT_API_URL = buildApiUrl('homepage_indexing_status');
base.describe('with custom links, 4 cols', () => {
base.describe('with custom links, max cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
......@@ -64,7 +64,7 @@ base.describe('with custom links, 4 cols', () => {
});
});
base.describe('with custom links, 2 cols', () => {
base.describe('with custom links, min cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
......
......@@ -23,7 +23,7 @@ import FooterLinkItem from './FooterLinkItem';
import IntTxsIndexingStatus from './IntTxsIndexingStatus';
import getApiVersionUrl from './utils/getApiVersionUrl';
const MAX_LINKS_COLUMNS = 3;
const MAX_LINKS_COLUMNS = 4;
const FRONT_VERSION_URL = `https://github.com/blockscout/frontend/tree/${ config.UI.footer.frontendVersion }`;
const FRONT_COMMIT_URL = `https://github.com/blockscout/frontend/commit/${ config.UI.footer.frontendCommit }`;
......@@ -96,14 +96,15 @@ const Footer = () => {
const fetch = useFetch();
const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>(
[ 'footer-links' ],
async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
{
const { isPending, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>({
queryKey: [ 'footer-links' ],
queryFn: async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
enabled: Boolean(config.UI.footer.links),
staleTime: Infinity,
});
const colNum = Math.min(linksData?.length || Infinity, MAX_LINKS_COLUMNS) + 1;
return (
<Flex
direction={{ base: 'column', lg: 'row' }}
......@@ -112,9 +113,9 @@ const Footer = () => {
borderTop="1px solid"
borderColor="divider"
as="footer"
columnGap="100px"
columnGap={{ lg: '32px', xl: '100px' }}
>
<Box flexGrow="1" mb={{ base: 8, lg: 0 }}>
<Box flexGrow="1" mb={{ base: 8, lg: 0 }} minW="195px">
<Flex flexWrap="wrap" columnGap={ 8 } rowGap={ 6 }>
<ColorModeToggler/>
{ !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> }
......@@ -140,28 +141,32 @@ const Footer = () => {
</VStack>
</Box>
<Grid
gap={{ base: 6, lg: 12 }}
gap={{ base: 6, lg: config.UI.footer.links && colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }}
gridTemplateColumns={ config.UI.footer.links ?
{ base: 'repeat(auto-fill, 160px)', lg: `repeat(${ (linksData?.length || MAX_LINKS_COLUMNS) + 1 }, 160px)` } :
{
base: 'repeat(auto-fill, 160px)',
lg: `repeat(${ colNum }, 135px)`,
xl: `repeat(${ colNum }, 160px)`,
} :
'auto'
}
>
<Box minW="160px" w={ config.UI.footer.links ? '160px' : '100%' }>
<Box>
{ config.UI.footer.links && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid
gap={ 1 }
gridTemplateColumns={
config.UI.footer.links ?
'160px' :
'1fr' :
{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(2, 160px)',
lg: 'repeat(3, 160px)',
xl: 'repeat(4, 160px)',
}
}
gridTemplateRows={{
base: 'auto',
lg: config.UI.footer.links ? 'auto' : 'repeat(4, auto)',
lg: config.UI.footer.links ? 'auto' : 'repeat(3, auto)',
xl: config.UI.footer.links ? 'auto' : 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: config.UI.footer.links ? 'row' : 'column' }}
......@@ -170,19 +175,19 @@ const Footer = () => {
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid>
</Box>
{ config.UI.footer.links && isLoading && (
{ config.UI.footer.links && isPending && (
Array.from(Array(3)).map((i, index) => (
<Box minW="160px" key={ index }>
<Skeleton w="120px" h="20px" mb={ 6 }/>
<Box key={ index }>
<Skeleton w="100%" h="20px" mb={ 6 }/>
<VStack spacing={ 5 } alignItems="start" mb={ 2 }>
{ Array.from(Array(5)).map((i, index) => <Skeleton w="160px" h="14px" key={ index }/>) }
{ Array.from(Array(5)).map((i, index) => <Skeleton w="100%" h="14px" key={ index }/>) }
</VStack>
</Box>
))
) }
{ config.UI.footer.links && linksData && (
linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => (
<Box minW="160px" key={ linkGroup.title }>
<Box key={ linkGroup.title }>
<Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
......
......@@ -13,7 +13,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
const IntTxsIndexingStatus = () => {
const { data, isError, isLoading } = useApiQuery('homepage_indexing_status');
const { data, isError, isPending } = useApiQuery('homepage_indexing_status');
const bgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const hintTextcolor = useColorModeValue('black', 'white');
......@@ -42,7 +42,7 @@ const IntTxsIndexingStatus = () => {
handler: handleInternalTxsIndexStatus,
});
if (isError || isLoading) {
if (isError || isPending) {
return null;
}
......
......@@ -18,17 +18,17 @@ const IndexingBlocksAlert = () => {
const cookiesString = appProps.cookies;
const [ hasAlertCookie ] = React.useState(cookies.get(cookies.NAMES.INDEXING_ALERT, cookiesString) === 'true');
const { data, isError, isLoading } = useApiQuery('homepage_indexing_status', {
const { data, isError, isPending } = useApiQuery('homepage_indexing_status', {
queryOptions: {
enabled: !config.UI.indexingAlert.blocks.isHidden,
},
});
React.useEffect(() => {
if (!isLoading && !isError) {
if (!isPending && !isError) {
cookies.set(cookies.NAMES.INDEXING_ALERT, data.finished_indexing_blocks ? 'false' : 'true');
}
}, [ data, isError, isLoading ]);
}, [ data, isError, isPending ]);
const queryClient = useQueryClient();
......@@ -62,7 +62,7 @@ const IndexingBlocksAlert = () => {
return null;
}
if (isLoading) {
if (isPending) {
return hasAlertCookie ? <Skeleton h={{ base: '96px', lg: '48px' }} w="100%"/> : null;
}
......
......@@ -13,10 +13,9 @@ export default function useNetworkMenu() {
const { isOpen, onClose, onOpen, onToggle } = useDisclosure();
const apiFetch = useApiFetch();
const { isLoading, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>(
[ 'featured-network' ],
async() => apiFetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }),
{
const { isPending, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>({
queryKey: [ 'featured-network' ],
queryFn: async() => apiFetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }),
enabled: Boolean(config.UI.sidebar.featuredNetworks) && isOpen,
staleTime: Infinity,
});
......@@ -26,8 +25,8 @@ export default function useNetworkMenu() {
onClose,
onOpen,
onToggle,
isLoading,
isPending,
data,
availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)),
}), [ isOpen, onClose, onOpen, onToggle, data, isLoading ]);
}), [ isOpen, onClose, onOpen, onToggle, data, isPending ]);
}
......@@ -13,15 +13,15 @@ type Props = {
};
const ProfileMenuDesktop = ({ isHomePage }: Props) => {
const { data, error, isLoading } = useFetchProfileInfo();
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const [ hasMenu, setHasMenu ] = React.useState(false);
React.useEffect(() => {
if (!isLoading) {
if (!isPending) {
setHasMenu(Boolean(data));
}
}, [ data, error?.status, isLoading ]);
}, [ data, error?.status, isPending ]);
const handleSignInClick = React.useCallback(() => {
mixpanel.logEvent(
......
......@@ -11,7 +11,7 @@ import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { data, error, isLoading } = useFetchProfileInfo();
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const [ hasMenu, setHasMenu ] = React.useState(false);
......@@ -24,10 +24,10 @@ const ProfileMenuMobile = () => {
}, []);
React.useEffect(() => {
if (!isLoading) {
if (!isPending) {
setHasMenu(Boolean(data));
}
}, [ data, error?.status, isLoading ]);
}, [ data, error?.status, isPending ]);
const iconButtonProps: Partial<IconButtonProps> = (() => {
if (hasMenu || !loginUrl) {
......
......@@ -103,7 +103,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
const bgColor = useColorModeValue('white', 'gray.900');
const content = (() => {
if (query.isLoading || marketplaceApps.isPlaceholderData) {
if (query.isPending || marketplaceApps.isPlaceholderData) {
return <ContentLoader text="We are searching, please wait... " fontSize="sm"/>;
}
......
......@@ -71,7 +71,7 @@ const Sol2UmlDiagram = ({ addressHash }: Props) => {
throw Error('Uml diagram fetch error', { cause: contractQuery.error as unknown as Error });
}
if (contractQuery.isLoading || umlQuery.isLoading) {
if (contractQuery.isPending || umlQuery.isPending) {
return <ContentLoader/>;
}
......
......@@ -27,7 +27,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const { data, isLoading, isError } = useApiQuery('stats_line', {
const { data, isPending, isError } = useApiQuery('stats_line', {
pathParams: { id },
queryParams: {
from: startDate,
......@@ -56,7 +56,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
title={ title }
units={ units }
description={ description }
isLoading={ isLoading }
isLoading={ isPending }
minH="230px"
/>
);
......
......@@ -7,6 +7,7 @@ import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/token';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token';
......@@ -18,7 +19,7 @@ import TruncatedValue from 'ui/shared/TruncatedValue';
import TokenNftMarketplaces from './TokenNftMarketplaces';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>;
}
const TokenDetails = ({ tokenQuery }: Props) => {
......
......@@ -5,24 +5,25 @@ import React from 'react';
import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'types/api/token';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import LinkExternal from 'ui/shared/LinkExternal';
import TokenProjectInfo from './TokenProjectInfo';
interface Props {
verifiedInfoQuery: UseQueryResult<TTokenVerifiedInfo>;
verifiedInfoQuery: UseQueryResult<TTokenVerifiedInfo, ResourceError<unknown>>;
}
const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => {
const { data, isLoading, isError } = verifiedInfoQuery;
const { data, isPending, isError } = verifiedInfoQuery;
const content = (() => {
if (!config.features.verifiedTokens.isEnabled) {
return null;
}
if (isLoading) {
if (isPending) {
return (
<>
<Skeleton w="100px" h="30px" borderRadius="base"/>
......
......@@ -112,7 +112,7 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
return <DataFetchAlert/>;
}
if (configQuery.isLoading) {
if (configQuery.isPending) {
return <ContentLoader/>;
}
......
......@@ -54,6 +54,7 @@ import TxDetailsFeePerGas from 'ui/tx/details/TxDetailsFeePerGas';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
import TxDetailsOther from 'ui/tx/details/TxDetailsOther';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
......@@ -156,6 +157,10 @@ const TxDetails = () => {
</Tag>
) }
</DetailsInfoItem>
<TxDetailsWithdrawalStatus
status={ data.op_withdrawal_status }
l1TxHash={ data.op_l1_transaction_hash }
/>
{ data.zkevm_status && (
<DetailsInfoItem
title="Confirmation status"
......
......@@ -24,7 +24,7 @@ const TxLogs = () => {
},
});
if (!txInfo.isLoading && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
......
......@@ -49,7 +49,7 @@ const TxRawTrace = () => {
handler: handleRawTraceMessage,
});
if (!txInfo.isLoading && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
......
......@@ -31,7 +31,7 @@ const TxState = () => {
},
});
if (!txInfo.isLoading && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
if (!txInfo.isPending && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
......
......@@ -45,7 +45,7 @@ const TxTokenTransfer = () => {
setTypeFilter(nextValue);
}, [ tokenTransferQuery ]);
if (!txsInfo.isLoading && !txsInfo.isPlaceholderData && !txsInfo.isError && !txsInfo.data.status) {
if (!txsInfo.isPending && !txsInfo.isPlaceholderData && !txsInfo.isError && !txsInfo.data.status) {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>;
}
......
import { Button } from '@chakra-ui/react';
import React from 'react';
import type { L2WithdrawalStatus } from 'types/api/l2Withdrawals';
import { WITHDRAWAL_STATUSES } from 'types/api/l2Withdrawals';
import config from 'configs/app';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
interface Props {
status: L2WithdrawalStatus | undefined;
l1TxHash: string | undefined;
}
const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => {
if (!config.features.optimisticRollup.isEnabled) {
return null;
}
if (!status || !WITHDRAWAL_STATUSES.includes(status)) {
return null;
}
const hasClaimButton = status === 'Ready for relay';
const steps = hasClaimButton ? WITHDRAWAL_STATUSES.slice(0, -1) : WITHDRAWAL_STATUSES;
const rightSlot = (() => {
if (status === 'Relayed' && l1TxHash) {
return <TxEntityL1 hash={ l1TxHash } truncation="constant"/>;
}
if (hasClaimButton) {
return (
<Button
variant="outline"
size="sm"
as="a"
href="https://app.optimism.io/bridge/withdraw"
target="_blank"
>
Claim funds
</Button>
);
}
return null;
})();
return (
<DetailsInfoItem
title="Withdrawal status"
hint="Detailed status progress of the transaction"
>
<VerificationSteps
steps={ steps as unknown as Array<L2WithdrawalStatus> }
step={ status }
rightSlot={ rightSlot }
my={ hasClaimButton ? '-6px' : 0 }
lineHeight={ hasClaimButton ? 8 : undefined }
/>
</DetailsInfoItem>
);
};
export default React.memo(TxDetailsWithdrawalStatus);
......@@ -23,7 +23,7 @@ const TxStateTable = ({ data, isLoading, top }: Props) => {
<Thead top={ top }>
<Tr>
<Th width="140px">Type</Th>
<Th width="146px">Address</Th>
<Th width="160px">Address</Th>
<Th width="33%" isNumeric>Before</Th>
<Th width="33%" isNumeric>After</Th>
<Th width="33%" isNumeric>Change</Th>
......
......@@ -38,7 +38,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
placeholderData: config.features.zkEvmRollup.isEnabled ? TX_ZKEVM_L2 : TX,
},
});
const { data, isError, isLoading } = queryResult;
const { data, isError, isPending } = queryResult;
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay);
......@@ -60,7 +60,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
topic: `transactions:${ hash }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError || data.status !== null,
isDisabled: isPending || isError || data.status !== null,
});
useSocketMessage({
channel,
......
......@@ -11,14 +11,14 @@ interface Props {
}
const TxAdditionalInfoContainer = ({ hash }: Props) => {
const { data, isError, isLoading } = useApiQuery('tx', {
const { data, isError, isPending } = useApiQuery('tx', {
pathParams: { hash },
queryOptions: {
refetchOnMount: false,
},
});
if (isLoading) {
if (isPending) {
return (
<Box>
<Skeleton w="130px" h="24px" borderRadius="full" mb={ 6 }/>
......
......@@ -4,17 +4,18 @@ import React from 'react';
import type { TxsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import type { ResourceError } from 'lib/api/resources';
import * as cookies from 'lib/cookies';
import sortTxs from 'lib/tx/sortTxs';
type HookResult = UseQueryResult<TxsResponse> & {
type HookResult = UseQueryResult<TxsResponse, ResourceError<unknown>> & {
sorting: Sort;
setSortByField: (field: 'val' | 'fee') => () => void;
setSortByValue: (value: Sort | undefined) => void;
}
export default function useTxsSort(
queryResult: UseQueryResult<TxsResponse>,
queryResult: UseQueryResult<TxsResponse, ResourceError<unknown>>,
): HookResult {
const [ sorting, setSorting ] = React.useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort);
......@@ -61,7 +62,7 @@ export default function useTxsSort(
}, []);
return React.useMemo(() => {
if (queryResult.isError || queryResult.isLoading) {
if (queryResult.isError || queryResult.isPending) {
return { ...queryResult, setSortByField, setSortByValue, sorting };
}
......
......@@ -13,7 +13,7 @@ const VerifiedContractsCounters = () => {
}
const content = (() => {
if (countersQuery.isLoading) {
if (countersQuery.isPending) {
const item = <Skeleton w={{ base: '100%', lg: 'calc((100% - 12px)/2)' }} h="69px" borderRadius="12px"/>;
return (
<>
......
......@@ -106,7 +106,8 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
}
}
const { mutate } = useMutation(updateWatchlist, {
const { mutate } = useMutation({
mutationFn: updateWatchlist,
onSuccess: async() => {
await onSuccess();
setPending(false);
......
......@@ -59,7 +59,8 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) =
});
}, [ notificationToast ]);
const { mutate } = useMutation(() => {
const { mutate } = useMutation({
mutationFn: () => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
......@@ -67,7 +68,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) =
pathParams: { id: item.id },
fetchParams: { method: 'PUT', body },
});
}, {
},
onError: () => {
showErrorToast();
setNotificationEnabled(prevState => !prevState);
......
......@@ -63,7 +63,8 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro
});
}, [ notificationToast ]);
const { mutate } = useMutation(() => {
const { mutate } = useMutation({
mutationFn: () => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState);
......@@ -71,7 +72,7 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro
pathParams: { id: item.id },
fetchParams: { method: 'PUT', body },
});
}, {
},
onError: () => {
showErrorToast();
setNotificationEnabled(prevState => !prevState);
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { WatchlistAddress } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import WatchlistTableItem from './WatchListTableItem';
interface Props {
......@@ -16,19 +17,20 @@ interface Props {
isLoading?: boolean;
onEditClick: (data: WatchlistAddress) => void;
onDeleteClick: (data: WatchlistAddress) => void;
top: number;
}
const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="70%">Address</Th>
<Th width="30%">Private tag</Th>
<Th width="160px">Email notification</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item, index) => (
<WatchlistTableItem
......
......@@ -3396,6 +3396,17 @@
"@sentry/utils" "7.72.0"
tslib "^2.4.1 || ^1.9.3"
"@sentry/cli@^2.21.2":
version "2.21.2"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.21.2.tgz#89e5633ff48a83d078c76c6997fffd4b68b2da1c"
integrity sha512-X1nye89zl+QV3FSuQDGItfM51tW9PQ7ce0TtV/12DgGgTVEgnVp5uvO3wX5XauHvulQzRPzwUL3ZK+yS5bAwCw==
dependencies:
https-proxy-agent "^5.0.0"
node-fetch "^2.6.7"
progress "^2.0.3"
proxy-from-env "^1.1.0"
which "^2.0.2"
"@sentry/core@7.72.0":
version "7.72.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.72.0.tgz#df19f9dc1c2cfc5993a73c0c36283c35f9c52f94"
......@@ -4054,23 +4065,28 @@
dependencies:
tslib "^2.4.0"
"@tanstack/match-sorter-utils@^8.1.1":
version "8.5.14"
resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.5.14.tgz#12efcd536abe491d09521e0242bc4d51442f8a8a"
integrity sha512-lVNhzTcOJ2bZ4IU+PeCPQ36vowBHvviJb2ZfdRFX5uhy7G0jM8N34zAMbmS5ZmVH8D2B7oU82OWo0e/5ZFzQrw==
"@tanstack/eslint-plugin-query@^5.0.5":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.0.5.tgz#3f1fc0ad472462c3ca0a8a99601206049dfa7211"
integrity sha512-kYbh5Cboz1BzN6LeUWnI1B0BCikXMYQjxaEO7cV+0rycllU0qZqSEkd2LdgWIZhuLTc4WBt0li1s+O6RhM5Cog==
dependencies:
remove-accents "0.4.2"
"@tanstack/query-core@4.10.3":
version "4.10.3"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.10.3.tgz#a6477bab9ed1ae4561ca0a59ae06f8c615c692b7"
integrity sha512-+ME02sUmBfx3Pjd+0XtEthK8/4rVMD2jcxvnW9DSgFONpKtpjzfRzjY4ykzpDw1QEo2eoPvl7NS8J5mNI199aA==
"@typescript-eslint/utils" "^5.54.0"
"@tanstack/query-core@4.29.11":
version "4.29.11"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.29.11.tgz#fa338f7d6897c6be5de6d8dabd603d9b78ee48c7"
integrity sha512-8C+hF6SFAb/TlFZyS9FItgNwrw4PMa7YeX+KQYe2ZAiEz6uzg6yIr+QBzPkUwZ/L0bXvGd1sufTm3wotoz+GwQ==
"@tanstack/query-core@5.4.3":
version "5.4.3"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.4.3.tgz#fbdd36ccf1acf70579980f2e7cf16d2c2aa2a5e9"
integrity sha512-fnI9ORjcuLGm1sNrKatKIosRQUpuqcD4SV7RqRSVmj8JSicX2aoMyKryHEBpVQvf6N4PaBVgBxQomjsbsGPssQ==
"@tanstack/query-devtools@5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.4.2.tgz#1687645ba1b9ffc76c55ac7759461d331d0776bf"
integrity sha512-EXdaMXi8CxZuMp97J5mq6wy1RduOfoWFv5vtA1U+hyqb8Wst6M8kkkjDSdFvGZIRpYY4K8mKLlEFHNUZDG5dtw==
"@tanstack/query-persist-client-core@4.29.11":
version "4.29.11"
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-4.29.11.tgz#96b4b83bead480eb37e024a59fd59bfd84b0545e"
......@@ -4085,14 +4101,12 @@
dependencies:
"@tanstack/query-persist-client-core" "4.29.11"
"@tanstack/react-query-devtools@^4.0.10":
version "4.11.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.11.0.tgz#a2adf21fa644eae5b834ba4235b99818301ab702"
integrity sha512-g/414SruE0TEp4jeMYxVUSXGXCc+zMY9j0gB8Q6B91gmyPseNqs9WLkVrqxRXoQDyRvDcphVOaEANNygGx+zGg==
"@tanstack/react-query-devtools@^5.4.3":
version "5.4.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.4.3.tgz#7df56de0454104c229f25393cf57a957f6245186"
integrity sha512-J9EB50vpK5yvQ5W+AOp9jIQa+1mld+Wwc2GF3VLr2SEDhOyiTiHOjrFGKKL1Cal5Wg8UuS3vexf8trElrtg05A==
dependencies:
"@tanstack/match-sorter-utils" "^8.1.1"
superjson "^1.10.0"
use-sync-external-store "^1.2.0"
"@tanstack/query-devtools" "5.4.2"
"@tanstack/react-query-persist-client@^4.28.0":
version "4.29.12"
......@@ -4101,14 +4115,6 @@
dependencies:
"@tanstack/query-persist-client-core" "4.29.11"
"@tanstack/react-query@^4.0.10":
version "4.10.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.10.3.tgz#294deefa0fb6ada88bc4631d346ef5d5551c5443"
integrity sha512-4OEJjkcsCTmG3ui7RjsVzsXerWQvInTe95CBKFyOV/GchMUlNztoFnnYmlMhX7hLUqJMhbG9l7M507V7+xU8Hw==
dependencies:
"@tanstack/query-core" "4.10.3"
use-sync-external-store "^1.2.0"
"@tanstack/react-query@^4.28.0":
version "4.29.12"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.29.12.tgz#de111cf1d6c389b86acacfaf972302914cfa1208"
......@@ -4117,6 +4123,13 @@
"@tanstack/query-core" "4.29.11"
use-sync-external-store "^1.2.0"
"@tanstack/react-query@^5.4.3":
version "5.4.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.4.3.tgz#cf59120690032e44b8c1c4c463cfb43aaad2fc5f"
integrity sha512-4aSOrRNa6yEmf7mws5QPTVMn8Lp7L38tFoTZ0c1ZmhIvbr8GIA0WT7X5N3yz/nuK8hUtjw9cAzBr4BPDZZ+tzA==
dependencies:
"@tanstack/query-core" "5.4.3"
"@testing-library/dom@^9.0.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.0.tgz#ed8ce10aa5e05eb6eaf0635b5b8975d889f66075"
......@@ -4809,6 +4822,14 @@
"@typescript-eslint/types" "5.60.1"
"@typescript-eslint/visitor-keys" "5.60.1"
"@typescript-eslint/scope-manager@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"
integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/type-utils@5.60.1":
version "5.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz#17770540e98d65ab4730c7aac618003f702893f4"
......@@ -4834,6 +4855,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.1.tgz#a17473910f6b8d388ea83c9d7051af89c4eb7561"
integrity sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==
"@typescript-eslint/types@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/typescript-estree@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d"
......@@ -4873,6 +4899,19 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.60.1":
version "5.60.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.1.tgz#6861ebedbefba1ac85482d2bdef6f2ff1eb65b80"
......@@ -4901,6 +4940,20 @@
eslint-utils "^3.0.0"
semver "^7.3.7"
"@typescript-eslint/utils@^5.54.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/typescript-estree" "5.62.0"
eslint-scope "^5.1.1"
semver "^7.3.7"
"@typescript-eslint/visitor-keys@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528"
......@@ -4925,6 +4978,14 @@
"@typescript-eslint/types" "5.60.1"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
dependencies:
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
"@vitejs/plugin-react@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz#46d1c37c507447d10467be1c111595174555ef28"
......@@ -6300,13 +6361,6 @@ cookie@~0.5.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
copy-anything@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.2.tgz#7189171ff5e1893b2287e8bf574b8cd448ed50b1"
integrity sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==
dependencies:
is-what "^4.1.6"
copy-to-clipboard@3.3.3, copy-to-clipboard@^3.2.0, copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
......@@ -8361,7 +8415,7 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
https-proxy-agent@^5.0.1:
https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
......@@ -8753,11 +8807,6 @@ is-weakset@^2.0.1:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
is-what@^4.1.6:
version "4.1.7"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.7.tgz#c41dc1d2d2d6a9285c624c2505f61849c8b1f9cc"
integrity sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==
is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
......@@ -9958,6 +10007,13 @@ node-fetch@^2.6.11:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^2.6.7:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.2.9:
version "3.2.10"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
......@@ -10652,6 +10708,11 @@ process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
promise-polyfill@^8.1.3:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
......@@ -11255,11 +11316,6 @@ remarkable@^2.0.1:
argparse "^1.0.10"
autolinker "^3.11.0"
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
repeat-string@^1.5.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
......@@ -11908,13 +11964,6 @@ sucrase@^3.20.3:
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
superjson@^1.10.0:
version "1.10.1"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.10.1.tgz#9c73e9393489dddab89d638694eadcbf4bda2f36"
integrity sha512-7fvPVDHmkTKg6641B9c6vr6Zz5CwPtF9j0XFExeLxJxrMaeLU2sqebY3/yrI3l0K5zJ+H9QA3H+lIYj5ooCOkg==
dependencies:
copy-anything "^3.0.2"
superstruct@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
......@@ -12741,7 +12790,7 @@ which-typed-array@^1.1.2, which-typed-array@^1.1.8, which-typed-array@^1.1.9:
has-tostringtag "^1.0.0"
is-typed-array "^1.1.10"
which@^2.0.1:
which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
......
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