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

Merge branch 'main' into marketplace-improvements

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