diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..cc48cec34c
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,32 @@
+{
+ "name": "blockscout dev",
+ "image": "mcr.microsoft.com/devcontainers/typescript-node:20",
+ "forwardPorts": [ 3000 ],
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "terminal.integrated.defaultProfile.linux": "zsh",
+ "terminal.integrated.profiles.linux": {
+ "zsh": {
+ "path": "/bin/zsh"
+ }
+ }
+ },
+ "extensions": [
+ "streetsidesoftware.code-spell-checker",
+ "formulahendry.auto-close-tag",
+ "formulahendry.auto-rename-tag",
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "yatki.vscode-surround",
+ "simonsiefke.svg-preview"
+ ]
+ }
+ },
+ "features": {
+ "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": {
+ "plugins": "npm",
+ "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions"
+ }
+ }
+}
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..13a543a449
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,13 @@
+Dockerfile
+.dockerignore
+node_modules
+/**/node_modules
+node_modules_linux
+npm-debug.log
+README.md
+.next
+.git
+*.tsbuildinfo
+.eslintcache
+/test-results/
+/playwright-report/
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000..4bdc5535b1
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,10 @@
+NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
+SENTRY_CSP_REPORT_URI=https://sentry.io
+NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
+NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
+NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
+NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
+NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
+NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
+FAVICON_GENERATOR_API_KEY=xxx
+NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000..37523e907c
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,8 @@
+node_modules
+node_modules_linux
+
+playwright/envs.js
+deploy/tools/envs-validator/index.js
+deploy/tools/feature-reporter/build/**
+deploy/tools/feature-reporter/index.js
+public/**
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000..49e2ad4c43
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,341 @@
+const RESTRICTED_MODULES = {
+ paths: [
+ { name: 'dayjs', message: 'Please use lib/date/dayjs.ts instead of directly importing dayjs' },
+ { name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
+ { name: '@metamask/providers', message: 'Please lazy-load @metamask/providers or use useProvider hook instead' },
+ { name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' },
+ { name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
+ {
+ name: '@chakra-ui/react',
+ importNames: [ 'Popover', 'Menu', 'useToast' ],
+ message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
+ },
+ {
+ name: 'lodash',
+ message: 'Please use `import [package] from \'lodash/[package]\'` instead.',
+ },
+ ],
+ patterns: [
+ 'icons/*',
+ '!lodash/*',
+ ],
+};
+
+module.exports = {
+ env: {
+ es6: true,
+ browser: true,
+ node: true,
+ },
+ 'extends': [
+ 'next/core-web-vitals',
+ 'eslint:recommended',
+ 'plugin:react/recommended',
+ 'plugin:regexp/recommended',
+ 'plugin:@typescript-eslint/eslint-recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:jest/recommended',
+ 'plugin:playwright/playwright-test',
+ 'plugin:@tanstack/eslint-plugin-query/recommended',
+ ],
+ plugins: [
+ 'es5',
+ 'react',
+ 'regexp',
+ '@typescript-eslint',
+ 'react-hooks',
+ 'jsx-a11y',
+ 'eslint-plugin-import-helpers',
+ 'jest',
+ 'eslint-plugin-no-cyrillic-string',
+ '@tanstack/query',
+ ],
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ settings: {
+ react: {
+ pragma: 'React',
+ version: 'detect',
+ },
+ },
+ rules: {
+ '@typescript-eslint/array-type': [ 'error', {
+ 'default': 'generic',
+ readonly: 'generic',
+ } ],
+ '@typescript-eslint/brace-style': [ 'error', '1tbs' ],
+ '@typescript-eslint/consistent-type-imports': [ 'error' ],
+ '@typescript-eslint/indent': [ 'error', 2 ],
+ '@typescript-eslint/member-delimiter-style': [ 'error' ],
+ '@typescript-eslint/naming-convention': [ 'error',
+ {
+ selector: 'default',
+ format: [ 'camelCase' ],
+ leadingUnderscore: 'allow',
+ trailingUnderscore: 'forbid',
+ },
+ {
+ selector: 'class',
+ format: [ 'PascalCase' ],
+ },
+ {
+ selector: 'enum',
+ format: [ 'PascalCase', 'UPPER_CASE' ],
+ },
+ {
+ selector: 'enumMember',
+ format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
+ },
+ {
+ selector: 'function',
+ format: [ 'camelCase', 'PascalCase' ],
+ },
+ {
+ selector: 'interface',
+ format: [ 'PascalCase' ],
+ },
+ {
+ selector: 'method',
+ format: [ 'camelCase', 'snake_case', 'UPPER_CASE' ],
+ leadingUnderscore: 'allow',
+ },
+ {
+ selector: 'parameter',
+ format: [ 'camelCase', 'PascalCase' ],
+ leadingUnderscore: 'allow',
+ },
+ {
+ selector: 'property',
+ format: null,
+ },
+ {
+ selector: 'typeAlias',
+ format: [ 'PascalCase' ],
+ },
+ {
+ selector: 'typeParameter',
+ format: [ 'PascalCase', 'UPPER_CASE' ],
+ },
+ {
+ selector: 'variable',
+ format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
+ leadingUnderscore: 'allow',
+ },
+ ],
+ '@typescript-eslint/no-duplicate-imports': [ 'error' ],
+ '@typescript-eslint/no-empty-function': [ 'off' ],
+ '@typescript-eslint/no-unused-vars': [ 'error', { ignoreRestSiblings: true } ],
+ '@typescript-eslint/no-use-before-define': 'off',
+ '@typescript-eslint/no-useless-constructor': [ 'error' ],
+ '@typescript-eslint/type-annotation-spacing': 'error',
+ '@typescript-eslint/no-explicit-any': [ 'error', { ignoreRestArgs: true } ],
+
+ // disabled in favor of @typescript-eslint
+ 'brace-style': 'off',
+ camelcase: 'off',
+ indent: 'off',
+ 'no-unused-vars': 'off',
+ 'no-use-before-define': 'off',
+ 'no-useless-constructor': 'off',
+
+ 'array-bracket-spacing': [ 'error', 'always' ],
+ 'arrow-spacing': [ 'error', { before: true, after: true } ],
+ 'comma-dangle': [ 'error', 'always-multiline' ],
+ 'comma-spacing': [ 'error' ],
+ 'comma-style': [ 'error', 'last' ],
+ curly: [ 'error', 'all' ],
+ 'eol-last': 'error',
+ eqeqeq: [ 'error', 'allow-null' ],
+ 'id-match': [ 'error', '^[\\w$]+$' ],
+ 'jsx-quotes': [ 'error', 'prefer-double' ],
+ 'key-spacing': [ 'error', {
+ beforeColon: false,
+ afterColon: true,
+ } ],
+ 'keyword-spacing': 'error',
+ 'linebreak-style': [ 'error', 'unix' ],
+ 'lines-around-comment': [ 'error', {
+ beforeBlockComment: true,
+ allowBlockStart: true,
+ } ],
+ 'max-len': [ 'error', 160, 4 ],
+ 'no-console': 'error',
+ 'no-empty': [ 'error', { allowEmptyCatch: true } ],
+ 'no-implicit-coercion': [ 'error', {
+ number: true,
+ 'boolean': true,
+ string: true,
+ } ],
+ 'no-mixed-operators': [ 'error', {
+ groups: [
+ [ '&&', '||' ],
+ ],
+ } ],
+ 'no-mixed-spaces-and-tabs': 'error',
+ 'no-multiple-empty-lines': [ 'error', {
+ max: 1,
+ maxEOF: 0,
+ maxBOF: 0,
+ } ],
+ 'no-multi-spaces': 'error',
+ 'no-multi-str': 'error',
+ 'no-nested-ternary': 'error',
+ 'no-trailing-spaces': 'error',
+ 'no-spaced-func': 'error',
+ 'no-with': 'error',
+ 'object-curly-spacing': [ 'error', 'always' ],
+ 'object-shorthand': 'off',
+ 'one-var': [ 'error', 'never' ],
+ 'operator-linebreak': [ 'error', 'after' ],
+ 'prefer-const': 'error',
+ 'quote-props': [ 'error', 'as-needed', {
+ keywords: true,
+ numbers: true,
+ } ],
+ quotes: [ 'error', 'single', {
+ allowTemplateLiterals: true,
+ } ],
+ 'space-before-function-paren': [ 'error', 'never' ],
+ 'space-before-blocks': [ 'error', 'always' ],
+ 'space-in-parens': [ 'error', 'never' ],
+ 'space-infix-ops': 'error',
+ 'space-unary-ops': 'off',
+ 'template-curly-spacing': [ 'error', 'always' ],
+ 'wrap-iife': [ 'error', 'inside' ],
+ semi: [ 'error', 'always' ],
+
+ 'import-helpers/order-imports': [
+ 'error',
+ {
+ newlinesBetween: 'always',
+ groups: [
+ 'module',
+ '/types/',
+ [
+ '/^nextjs/',
+ ],
+ [
+ '/^configs/',
+ '/^data/',
+ '/^deploy/',
+ '/^icons/',
+ '/^jest/',
+ '/^lib/',
+ '/^mocks/',
+ '/^pages/',
+ '/^playwright/',
+ '/^stubs/',
+ '/^theme/',
+ '/^ui/',
+ ],
+ [ 'parent', 'sibling', 'index' ],
+ ],
+ alphabetize: { order: 'asc', ignoreCase: true },
+ },
+ ],
+
+ 'no-restricted-imports': [ 'error', RESTRICTED_MODULES ],
+ 'no-restricted-properties': [ 2, {
+ object: 'process',
+ property: 'env',
+ // FIXME: restrict the rule only NEXT_PUBLIC variables
+ message: 'Please use configs/app/index.ts to import any NEXT_PUBLIC environment variables. For other properties please disable this rule for a while.',
+ } ],
+
+ 'react/jsx-key': 'error',
+ 'react/jsx-no-bind': [ 'error', {
+ ignoreRefs: true,
+ } ],
+ 'react/jsx-curly-brace-presence': [ 'error', {
+ props: 'never',
+ children: 'never',
+ } ],
+ 'react/jsx-curly-spacing': [ 'error', {
+ when: 'always',
+ children: true,
+ spacing: {
+ objectLiterals: 'never',
+ },
+ } ],
+ 'react/jsx-equals-spacing': [ 'error', 'never' ],
+ 'react/jsx-fragments': [ 'error', 'syntax' ],
+ 'react/jsx-no-duplicate-props': 'error',
+ 'react/jsx-no-target-blank': 'off',
+ 'react/jsx-no-useless-fragment': 'error',
+ 'react/jsx-tag-spacing': [ 'error', {
+ afterOpening: 'never',
+ beforeSelfClosing: 'never',
+ closingSlash: 'never',
+ } ],
+ 'react/jsx-wrap-multilines': [ 'error', {
+ declaration: 'parens-new-line',
+ assignment: 'parens-new-line',
+ 'return': 'parens-new-line',
+ arrow: 'parens-new-line',
+ condition: 'parens-new-line',
+ logical: 'parens-new-line',
+ prop: 'parens-new-line',
+ } ],
+ 'react/no-access-state-in-setstate': 'error',
+ 'react/no-deprecated': 'error',
+ 'react/no-direct-mutation-state': 'error',
+ 'react/no-find-dom-node': 'off',
+ 'react/no-redundant-should-component-update': 'error',
+ 'react/no-render-return-value': 'error',
+ 'react/no-string-refs': 'off',
+ 'react/no-unknown-property': 'error',
+ 'react/no-unused-state': 'error',
+ 'react/require-optimization': [ 'error' ],
+ 'react/void-dom-elements-no-children': 'error',
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'error',
+
+ 'regexp/confusing-quantifier': 'error',
+ 'regexp/control-character-escape': 'error',
+ 'regexp/negation': 'error',
+ 'regexp/no-dupe-disjunctions': 'error',
+ 'regexp/no-empty-alternative': 'error',
+ 'regexp/no-empty-capturing-group': 'error',
+ 'regexp/no-lazy-ends': 'error',
+ 'regexp/no-obscure-range': [ 'error', {
+ allowed: [ 'alphanumeric' ],
+ } ],
+ 'regexp/no-optional-assertion': 'error',
+ 'regexp/no-unused-capturing-group': [ 'error', {
+ fixable: true,
+ } ],
+ 'regexp/no-useless-character-class': 'error',
+ 'regexp/no-useless-dollar-replacements': 'error',
+
+ 'no-cyrillic-string/no-cyrillic-string': 'error',
+ },
+ overrides: [
+ {
+ files: [ '*.js', '*.jsx' ],
+ rules: {
+ '@typescript-eslint/no-var-requires': 'off',
+ },
+ },
+ {
+ files: [
+ '*.config.ts',
+ '*.config.js',
+ 'playwright/**',
+ 'deploy/tools/**',
+ 'middleware.ts',
+ 'nextjs/**',
+ 'instrumentation*.ts',
+ ],
+ rules: {
+ // for configs allow to consume env variables from process.env directly
+ 'no-restricted-properties': [ 0 ],
+ },
+ },
+ ],
+};
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..d4431e2963
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+__snapshots__/** filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..732e01d370
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,78 @@
+name: Bug Report
+description: File a bug report
+labels: [ "bug", "triage" ]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for reporting a bug 🐛!
+
+ Please search open/closed issues before submitting. Someone might have had the similar problem before 😉!
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: A brief description of the issue.
+ placeholder: |
+ When I ____, I expected ____ to happen but ____ happened instead.
+ validations:
+ required: true
+
+ - type: input
+ id: link
+ attributes:
+ label: Link to the page
+ description: The link to the page where the issue occurs.
+ placeholder: https://eth.blockscout.com
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ description: |
+ Explain how to reproduce the issue in the development environment.
+ value: |
+ 1. Go to '...'
+ 2. Click on '...'
+ 3. Scroll down to '...'
+ 4. See error
+
+ - type: input
+ id: version
+ attributes:
+ label: App version
+ description: The version of the front-end app you use. You can find it in the footer of the page.
+ placeholder: v1.2.0
+ validations:
+ required: true
+
+ - type: input
+ id: browser
+ # validations:
+ # required: true
+ attributes:
+ label: Browser
+ description: What browsers are you seeing the problem on? Please specify browser vendor and its version.
+ placeholder: Google Chrome 111
+
+ - type: dropdown
+ id: operating-system
+ # validations:
+ # required: true
+ attributes:
+ label: Operating system
+ description: The operating system this issue occurred with.
+ options:
+ - macOS
+ - Windows
+ - Linux
+
+ - type: textarea
+ id: additional-information
+ attributes:
+ label: Additional information
+ description: |
+ Use this section to provide any additional information you might have (e.g screenshots or screencasts).
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..439bca677d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Feature Request
+ url: https://blockscout.canny.io/feature-requests
+ about: Request a feature or enhancement
+ - name: Ask a question
+ url: https://github.com/orgs/blockscout/discussions
+ about: Ask questions and discuss topics with other community members
+ - name: Join our Discord Server
+ url: https://discord.gg/blockscout
+ about: The official Blockscout Discord community
\ No newline at end of file
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
new file mode 100644
index 0000000000..9496bb37e6
--- /dev/null
+++ b/.github/workflows/checks.yml
@@ -0,0 +1,228 @@
+name: Checks
+on:
+ workflow_call:
+ workflow_dispatch:
+ pull_request:
+ types: [ opened, synchronize, unlabeled ]
+ paths-ignore:
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.husky/**'
+ - '.vscode/**'
+ - 'deploy/**'
+ - 'docs/**'
+ - 'public/**'
+ - 'stub/**'
+ - 'tools/**'
+
+# concurrency:
+# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }}
+# cancel-in-progress: true
+
+jobs:
+ code_quality:
+ name: Code quality
+ runs-on: ubuntu-latest
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip checks') && !(github.event.action == 'unlabeled' && github.event.label.name != 'skip checks') }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.11.0
+ cache: 'yarn'
+
+ - name: Cache node_modules
+ uses: actions/cache@v4
+ 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: Run ESLint
+ run: yarn lint:eslint
+
+ - name: Compile TypeScript
+ run: yarn lint:tsc
+
+ envs_validation:
+ name: ENV variables validation
+ runs-on: ubuntu-latest
+ needs: [ code_quality ]
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.11.0
+ cache: 'yarn'
+
+ - name: Cache node_modules
+ uses: actions/cache@v4
+ 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: Install script dependencies
+ run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile --ignore-optional
+
+ - name: Run validation tests
+ run: |
+ set +e
+ cd ./deploy/tools/envs-validator && yarn test
+ exitcode="$?"
+ echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
+ exit "$exitcode"
+
+ jest_tests:
+ name: Jest tests
+ needs: [ code_quality, envs_validation ]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.11.0
+ cache: 'yarn'
+
+ - name: Cache node_modules
+ uses: actions/cache@v4
+ 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: Run Jest
+ run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests
+
+ pw_affected_tests:
+ name: Resolve affected Playwright tests
+ runs-on: ubuntu-latest
+ needs: [ code_quality, envs_validation ]
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.11.0
+ cache: 'yarn'
+
+ - name: Cache node_modules
+ uses: actions/cache@v4
+ 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: Install script dependencies
+ run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile
+
+ - name: Run script
+ run: yarn test:pw:detect-affected
+
+ - name: Upload result file
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-affected-tests
+ path: ./playwright/affected-tests.txt
+ retention-days: 3
+
+ pw_tests:
+ name: 'Playwright tests / Project: ${{ matrix.project }}'
+ needs: [ code_quality, envs_validation, pw_affected_tests ]
+ if: |
+ always() &&
+ needs.code_quality.result == 'success' &&
+ needs.envs_validation.result == 'success' &&
+ (needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
+ runs-on: ubuntu-latest
+ container:
+ image: mcr.microsoft.com/playwright:v1.41.1-focal
+
+ strategy:
+ fail-fast: false
+ matrix:
+ project: [ default, mobile, dark-color-mode ]
+
+ steps:
+ - name: Install git-lfs
+ run: apt-get update && apt-get install git-lfs
+
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ lfs: 'true'
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.11.0
+ cache: 'yarn'
+
+ - name: Cache node_modules
+ uses: actions/cache@v4
+ 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: Download affected tests list
+ if: ${{ needs.pw_affected_tests.result == 'success' }}
+ uses: actions/download-artifact@v4
+ continue-on-error: true
+ with:
+ name: playwright-affected-tests
+ path: ./playwright
+
+ - name: Run PlayWright
+ run: yarn test:pw:ci --affected=${{ github.event_name == 'pull_request' }} --pass-with-no-tests
+ env:
+ HOME: /root
+ PW_PROJECT: ${{ matrix.project }}
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report-${{ matrix.project }}
+ path: playwright-report
+ retention-days: 10
\ No newline at end of file
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
new file mode 100644
index 0000000000..a8ed3ee27a
--- /dev/null
+++ b/.github/workflows/cleanup.yml
@@ -0,0 +1,33 @@
+name: Cleanup environments
+
+on:
+ pull_request:
+ types:
+ - closed
+ - merged
+ workflow_dispatch:
+
+jobs:
+ cleanup_release:
+ uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master
+ with:
+ appName: review-l2-$GITHUB_REF_NAME_SLUG
+ globalEnv: review
+ helmfileDir: deploy
+ kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
+ vaultRole: ci-dev
+ secrets: inherit
+ cleanup_l2_release:
+ uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master
+ with:
+ appName: review-$GITHUB_REF_NAME_SLUG
+ globalEnv: review
+ helmfileDir: deploy
+ kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
+ vaultRole: ci-dev
+ secrets: inherit
+ cleanup_docker_image:
+ uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master
+ with:
+ dockerImage: review-$GITHUB_REF_NAME_SLUG
+ secrets: inherit
diff --git a/.github/workflows/copy-issues-labels.yml b/.github/workflows/copy-issues-labels.yml
new file mode 100644
index 0000000000..e05b6e88ee
--- /dev/null
+++ b/.github/workflows/copy-issues-labels.yml
@@ -0,0 +1,116 @@
+name: Copy issues labels to pull request
+
+on:
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: Pull request number
+ required: true
+ type: string
+ issues:
+ description: JSON encoded list of issue ids
+ required: true
+ type: string
+ workflow_call:
+ inputs:
+ pr_number:
+ description: Pull request number
+ required: true
+ type: string
+ issues:
+ description: JSON encoded list of issue ids
+ required: true
+ type: string
+
+jobs:
+ run:
+ name: Run
+ runs-on: ubuntu-latest
+ steps:
+ - name: Find unique labels
+ id: find_unique_labels
+ uses: actions/github-script@v7
+ env:
+ ISSUES: ${{ inputs.issues }}
+ with:
+ script: |
+ const issues = JSON.parse(process.env.ISSUES);
+
+ const WHITE_LISTED_LABELS = [
+ 'client feature',
+ 'feature',
+
+ 'bug',
+
+ 'dependencies',
+ 'performance',
+
+ 'chore',
+ 'enhancement',
+ 'refactoring',
+ 'tech',
+ 'ENVs',
+ ]
+
+ const labels = await Promise.all(issues.map(getIssueLabels));
+ const uniqueLabels = uniqueStringArray(labels.flat().filter((label) => WHITE_LISTED_LABELS.includes(label)));
+
+ if (uniqueLabels.length === 0) {
+ core.info('No labels found.\n');
+ return [];
+ }
+
+ core.info(`Found following labels: ${ uniqueLabels.join(', ') }.\n`);
+ return uniqueLabels;
+
+ async function getIssueLabels(issue) {
+ core.info(`Obtaining labels list for the issue #${ issue }...`);
+
+ try {
+ const response = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/labels', {
+ owner: 'blockscout',
+ repo: 'frontend',
+ issue_number: issue,
+ });
+ return response.data.map(({ name }) => name);
+ } catch (error) {
+ core.error(`Failed to obtain labels for the issue #${ issue }: ${ error.message }`);
+ return [];
+ }
+ }
+
+ function uniqueStringArray(array) {
+ return Array.from(new Set(array));
+ }
+
+ - name: Update pull request labels
+ id: update_pr_labels
+ uses: actions/github-script@v7
+ env:
+ LABELS: ${{ steps.find_unique_labels.outputs.result }}
+ PR_NUMBER: ${{ inputs.pr_number }}
+ with:
+ script: |
+ const labels = JSON.parse(process.env.LABELS);
+ const prNumber = Number(process.env.PR_NUMBER);
+
+ if (labels.length === 0) {
+ core.info('Nothing to update.\n');
+ return;
+ }
+
+ for (const label of labels) {
+ await addLabelToPr(prNumber, label);
+ }
+ core.info('Done.\n');
+
+ async function addLabelToPr(prNumber, label) {
+ console.log(`Adding label to the pull request #${ prNumber }...`);
+
+ return await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', {
+ owner: 'blockscout',
+ repo: 'frontend',
+ issue_number: prNumber,
+ labels: [ label ],
+ });
+ }
\ No newline at end of file
diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml
new file mode 100644
index 0000000000..73bdd72cf7
--- /dev/null
+++ b/.github/workflows/deploy-main.yml
@@ -0,0 +1,27 @@
+name: Deploy from main branch
+
+on:
+ push:
+ branches:
+ - main
+ paths-ignore:
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.husky/**'
+ - '.vscode/**'
+ - 'docs/**'
+ - 'jest/**'
+ - 'mocks/**'
+ - 'playwright/**'
+ - 'stubs/**'
+ - 'tools/**'
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ publish_image:
+ name: Publish Docker image
+ uses: './.github/workflows/publish-image.yml'
+ secrets: inherit
diff --git a/.github/workflows/deploy-review-l2.yml b/.github/workflows/deploy-review-l2.yml
new file mode 100644
index 0000000000..cd9cdcae53
--- /dev/null
+++ b/.github/workflows/deploy-review-l2.yml
@@ -0,0 +1,62 @@
+name: Deploy review environment (L2)
+
+on:
+ workflow_dispatch:
+ inputs:
+ envs_preset:
+ description: ENVs preset
+ required: false
+ default: ""
+ type: choice
+ options:
+ - none
+ - arbitrum
+ - base
+ - celo_alfajores
+ - garnet
+ - gnosis
+ - eth
+ - eth_sepolia
+ - eth_goerli
+ - optimism
+ - optimism_sepolia
+ - polygon
+ - rootstock
+ - stability
+ - zkevm
+ - zksync
+
+jobs:
+ make_slug:
+ name: Make GitHub reference slug
+ runs-on: ubuntu-latest
+ outputs:
+ REF_SLUG: ${{ steps.output.outputs.REF_SLUG }}
+ steps:
+ - name: Inject slug/short variables
+ uses: rlespinasse/github-slug-action@v4.4.1
+
+ - name: Set output
+ id: output
+ run: echo "REF_SLUG=${{ env.GITHUB_REF_NAME_SLUG }}" >> $GITHUB_OUTPUT
+
+ publish_image:
+ name: Publish Docker image
+ needs: make_slug
+ uses: './.github/workflows/publish-image.yml'
+ with:
+ tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }}
+ build_args: ENVS_PRESET=${{ inputs.envs_preset }}
+ secrets: inherit
+
+ deploy_review_l2:
+ name: Deploy frontend (L2)
+ needs: [ make_slug, publish_image ]
+ uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
+ with:
+ appName: review-l2-${{ needs.make_slug.outputs.REF_SLUG }}
+ globalEnv: review
+ helmfileDir: deploy
+ kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
+ vaultRole: ci-dev
+ secrets: inherit
diff --git a/.github/workflows/deploy-review.yml b/.github/workflows/deploy-review.yml
new file mode 100644
index 0000000000..9bc8fb616f
--- /dev/null
+++ b/.github/workflows/deploy-review.yml
@@ -0,0 +1,62 @@
+name: Deploy review environment
+
+on:
+ workflow_dispatch:
+ inputs:
+ envs_preset:
+ description: ENVs preset
+ required: false
+ default: ""
+ type: choice
+ options:
+ - none
+ - arbitrum
+ - base
+ - celo_alfajores
+ - garnet
+ - gnosis
+ - eth
+ - eth_sepolia
+ - eth_goerli
+ - optimism
+ - optimism_sepolia
+ - polygon
+ - rootstock
+ - stability
+ - zkevm
+ - zksync
+
+jobs:
+ make_slug:
+ name: Make GitHub reference slug
+ runs-on: ubuntu-latest
+ outputs:
+ REF_SLUG: ${{ steps.output.outputs.REF_SLUG }}
+ steps:
+ - name: Inject slug/short variables
+ uses: rlespinasse/github-slug-action@v4.4.1
+
+ - name: Set output
+ id: output
+ run: echo "REF_SLUG=${{ env.GITHUB_REF_NAME_SLUG }}" >> $GITHUB_OUTPUT
+
+ publish_image:
+ name: Publish Docker image
+ needs: make_slug
+ uses: './.github/workflows/publish-image.yml'
+ with:
+ tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }}
+ build_args: ENVS_PRESET=${{ inputs.envs_preset }}
+ secrets: inherit
+
+ deploy_review:
+ name: Deploy frontend
+ needs: [ make_slug, publish_image ]
+ uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
+ with:
+ appName: review-${{ needs.make_slug.outputs.REF_SLUG }}
+ globalEnv: review
+ helmfileDir: deploy
+ kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
+ vaultRole: ci-dev
+ secrets: inherit
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
new file mode 100644
index 0000000000..6aab1f48ff
--- /dev/null
+++ b/.github/workflows/e2e-tests.yml
@@ -0,0 +1,50 @@
+name: Run E2E tests k8s
+
+on:
+ workflow_dispatch:
+ workflow_call:
+
+# concurrency:
+# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }}
+# cancel-in-progress: true
+
+jobs:
+ publish_image:
+ name: Publish Docker image
+ uses: './.github/workflows/publish-image.yml'
+ secrets: inherit
+
+ deploy_e2e:
+ name: Deploy E2E instance
+ needs: publish_image
+ runs-on: ubuntu-latest
+ permissions: write-all
+ steps:
+ - name: Get Vault credentials
+ id: retrieve-vault-secrets
+ uses: hashicorp/vault-action@v2.4.1
+ with:
+ url: https://vault.k8s.blockscout.com
+ role: ci-dev
+ path: github-jwt
+ method: jwt
+ tlsSkipVerify: false
+ exportToken: true
+ secrets: |
+ ci/data/dev/github token | WORKFLOW_TRIGGER_TOKEN ;
+ - name: Trigger deploy
+ uses: convictional/trigger-workflow-and-wait@v1.6.1
+ with:
+ owner: blockscout
+ repo: deployment-values
+ github_token: ${{ env.WORKFLOW_TRIGGER_TOKEN }}
+ workflow_file_name: deploy_blockscout.yaml
+ ref: main
+ wait_interval: 30
+ client_payload: '{ "instance": "dev", "globalEnv": "e2e"}'
+
+ test:
+ name: Run tests
+ needs: deploy_e2e
+ uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
+ secrets: inherit
\ No newline at end of file
diff --git a/.github/workflows/label-issues-in-release.yml b/.github/workflows/label-issues-in-release.yml
new file mode 100644
index 0000000000..b95fd45616
--- /dev/null
+++ b/.github/workflows/label-issues-in-release.yml
@@ -0,0 +1,282 @@
+name: Label issues in release
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Release tag'
+ required: true
+ type: string
+ label_name:
+ description: 'Name of the label'
+ required: true
+ type: string
+ label_description:
+ description: 'Description of the label'
+ default: ''
+ required: false
+ type: string
+ label_color:
+ description: 'A color of the added label'
+ default: 'FFFFFF'
+ required: false
+ type: string
+ workflow_call:
+ inputs:
+ tag:
+ description: 'Release tag'
+ required: true
+ type: string
+ label_name:
+ description: 'Name of the label'
+ required: true
+ type: string
+ label_description:
+ description: 'Description of the label'
+ default: ''
+ required: false
+ type: string
+ label_color:
+ description: 'A color of the added label'
+ default: 'FFFFFF'
+ required: false
+ type: string
+ outputs:
+ issues:
+ description: "JSON encoded list of issues linked to commits in the release"
+ value: ${{ jobs.run.outputs.issues }}
+
+# concurrency:
+# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }}
+# cancel-in-progress: true
+
+jobs:
+ run:
+ name: Run
+ runs-on: ubuntu-latest
+ outputs:
+ issues: ${{ steps.linked_issues.outputs.result }}
+ steps:
+ - name: Getting tags of the two latestest releases
+ id: tags
+ uses: actions/github-script@v7
+ env:
+ TAG: ${{ inputs.tag }}
+ with:
+ script: |
+ const { repository: { releases: { nodes: releases } } } = await github.graphql(`
+ query ($owner: String!, $repo: String!) {
+ repository(owner: $owner, name: $repo) {
+ releases(first: 10, orderBy: { field: CREATED_AT, direction: DESC }) {
+ nodes {
+ name
+ tagName
+ tagCommit {
+ oid
+ }
+ isPrerelease
+ isDraft
+ publishedAt
+ }
+ }
+ }
+ }
+ `,
+ {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ }
+ );
+
+ if (releases[0].tagName !== process.env.TAG) {
+ core.info(`Current latest tag: ${ releases[0].tagName }`);
+ core.setFailed(`Release with tag ${ process.env.TAG } is not latest one.`);
+ return;
+ }
+
+ const latestTag = process.env.TAG;
+ const [ { tagName: previousTag } ] = releases
+ .slice(1)
+ .filter(({ isDraft }) => !isDraft);
+
+ core.info('Found following tags:');
+ core.info(` latest: ${ latestTag }`);
+ core.info(` second latest: ${ previousTag }`);
+
+ core.setOutput('latest', latestTag);
+ core.setOutput('previous', previousTag);
+
+ - name: Looking for commits between two releases
+ id: commits
+ uses: actions/github-script@v7
+ env:
+ PREVIOUS_TAG: ${{ steps.tags.outputs.previous }}
+ LATEST_TAG: ${{ steps.tags.outputs.latest }}
+ with:
+ script: |
+ const { data: { commits: commitsInRelease } } = await github.request('GET /repos/{owner}/{repo}/compare/{basehead}', {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ basehead: `${ process.env.PREVIOUS_TAG }...${ process.env.LATEST_TAG }`,
+ });
+
+ if (commitsInRelease.length === 0) {
+ core.notice(`No commits found between ${ process.env.PREVIOUS_TAG } and ${ process.env.LATEST_TAG }`);
+ return [];
+ }
+
+ const commits = commitsInRelease.map(({ sha }) => sha);
+
+ core.startGroup(`Found ${ commits.length } commits`);
+ commits.forEach((sha) => {
+ core.info(sha);
+ })
+ core.endGroup();
+
+ return commits;
+
+ - name: Looking for issues linked to commits
+ id: linked_issues
+ uses: actions/github-script@v7
+ env:
+ COMMITS: ${{ steps.commits.outputs.result }}
+ with:
+ script: |
+ const commits = JSON.parse(process.env.COMMITS);
+
+ if (commits.length === 0) {
+ return [];
+ }
+
+ const map = {};
+
+ core.startGroup(`Looking for linked issues`);
+ for (const sha of commits) {
+ const result = await getLinkedIssuesForCommitPR(sha);
+ result.forEach((issue) => {
+ map[issue] = issue;
+ });
+ }
+ core.endGroup();
+
+ const issues = Object.values(map);
+
+ if (issues.length > 0) {
+ core.startGroup(`Found ${ issues.length } unique issues`);
+ issues.sort().forEach((issue) => {
+ core.info(`#${ issue } - https://github.com/${ context.repo.owner }/${ context.repo.repo }/issues/${ issue }`);
+ })
+ core.endGroup();
+ } else {
+ core.notice('No linked issues found.');
+ }
+
+ return issues;
+
+ async function getLinkedIssuesForCommitPR(sha) {
+ core.info(`Fetching issues for commit: ${ sha }`);
+
+ const response = await github.graphql(`
+ query ($owner: String!, $repo: String!, $sha: GitObjectID!) {
+ repository(owner: $owner, name: $repo) {
+ object(oid: $sha) {
+ ... on Commit {
+ associatedPullRequests(first: 10) {
+ nodes {
+ number
+ title
+ state
+ merged
+ closingIssuesReferences(first: 10) {
+ nodes {
+ number
+ title
+ closed
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ sha,
+ });
+
+ if (!response) {
+ core.info('Nothing has found.');
+ return [];
+ }
+
+ const { repository: { object: { associatedPullRequests } } } = response;
+
+ const issues = associatedPullRequests
+ .nodes
+ .map(({ closingIssuesReferences: { nodes: issues } }) => issues.map(({ number }) => number))
+ .flat();
+
+ core.info(`Found following issues: ${ issues.join(', ') || '-' }\n`);
+
+ return issues;
+ }
+
+ - name: Creating label
+ id: label_creating
+ uses: actions/github-script@v7
+ env:
+ LABEL_NAME: ${{ inputs.label_name }}
+ LABEL_COLOR: ${{ inputs.label_color }}
+ LABEL_DESCRIPTION: ${{ inputs.label_description }}
+ with:
+ script: |
+ try {
+ const result = await github.request('POST /repos/{owner}/{repo}/labels', {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ name: process.env.LABEL_NAME,
+ color: process.env.LABEL_COLOR,
+ description: process.env.LABEL_DESCRIPTION,
+ });
+
+ core.info('Label was created.');
+ } catch (error) {
+ if (error.status === 422) {
+ core.info('Label already exist.');
+ } else {
+ core.setFailed(error.message);
+ }
+ }
+
+
+ - name: Adding label to issues
+ id: labeling_issues
+ uses: actions/github-script@v7
+ env:
+ LABEL_NAME: ${{ inputs.label_name }}
+ ISSUES: ${{ steps.linked_issues.outputs.result }}
+ with:
+ script: |
+ const issues = JSON.parse(process.env.ISSUES);
+
+ if (issues.length === 0) {
+ core.notice('No issues has found. Nothing to label.');
+ return;
+ }
+
+ for (const issue of issues) {
+ core.info(`Adding label to the issue #${ issue }...`);
+ await addLabelToIssue(issue, process.env.LABEL_NAME);
+ core.info('Done.\n');
+ }
+
+ async function addLabelToIssue(issue, label) {
+ return await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue,
+ labels: [ label ],
+ });
+ }
diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml
new file mode 100644
index 0000000000..05155cbd32
--- /dev/null
+++ b/.github/workflows/pre-release.yml
@@ -0,0 +1,64 @@
+name: Pre-release
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - 'v[0-9]+.[0-9]+.[0-9]+-[a-z]+*' # e.g v1.2.3-alpha.2
+
+jobs:
+ checks:
+ name: Run code checks
+ uses: "./.github/workflows/checks.yml"
+ secrets: inherit
+
+ # publish_image:
+ # image will be published in e2e-tests.yml workflow
+ # name: Publish Docker image
+ # uses: './.github/workflows/publish-image.yml'
+ # secrets: inherit
+
+ e2e_tests:
+ name: Run e2e tests
+ needs: checks
+ uses: "./.github/workflows/e2e-tests.yml"
+ secrets: inherit
+
+ version:
+ name: Pre-release version info
+ runs-on: ubuntu-latest
+ outputs:
+ is_initial: ${{ steps.is_initial.outputs.result }}
+ steps:
+ - name: Determine if it is the initial version of the pre-release
+ id: is_initial
+ uses: actions/github-script@v7
+ env:
+ TAG: ${{ github.ref_name }}
+ with:
+ script: |
+ const tag = process.env.TAG;
+ const REGEXP = /^v[0-9]+.[0-9]+.[0-9]+-[a-z]+((\.|-)\d+)?$/i;
+ const match = tag.match(REGEXP);
+ const isInitial = match && !match[1] ? true : false;
+ core.info('is_initial flag value: ', isInitial);
+ return isInitial;
+
+ label_issues:
+ name: Add pre-release label to issues
+ uses: './.github/workflows/label-issues-in-release.yml'
+ needs: [ version ]
+ if: ${{ needs.version.outputs.is_initial == 'true' }}
+ with:
+ tag: ${{ github.ref_name }}
+ label_name: 'pre-release'
+ label_description: Tasks in pre-release right now
+ secrets: inherit
+
+ # Temporary disable this step because it is broken
+ # There is an issue with building web3modal deps
+ upload_source_maps:
+ name: Upload source maps to Sentry
+ if: false
+ uses: './.github/workflows/upload-source-maps.yml'
+ secrets: inherit
diff --git a/.github/workflows/project-management.yml b/.github/workflows/project-management.yml
new file mode 100644
index 0000000000..1da733baeb
--- /dev/null
+++ b/.github/workflows/project-management.yml
@@ -0,0 +1,99 @@
+name: Project management
+on:
+ issues:
+ types: [ closed ]
+ pull_request:
+ types: [ review_requested ]
+
+jobs:
+ not_planned_issue:
+ name: Update task for not planned issue
+ if: ${{ github.event.issue && github.event.action == 'closed' && github.event.issue.state_reason == 'not_planned' }}
+ uses: './.github/workflows/update-project-cards.yml'
+ with:
+ project_name: ${{ vars.PROJECT_NAME }}
+ field_name: Status
+ field_value: Done
+ issues: "[${{ github.event.issue.number }}]"
+ secrets: inherit
+
+ completed_issue:
+ name: Update task for completed issue
+ if: ${{ github.event.issue && github.event.action == 'closed' && github.event.issue.state_reason == 'completed' }}
+ uses: './.github/workflows/update-project-cards.yml'
+ with:
+ project_name: ${{ vars.PROJECT_NAME }}
+ field_name: Status
+ field_value: Ready For Realease
+ issues: "[${{ github.event.issue.number }}]"
+ secrets: inherit
+
+ pr_linked_issues:
+ name: Get issues linked to PR
+ runs-on: ubuntu-latest
+ if: ${{ github.event.pull_request && github.event.action == 'review_requested' }}
+ outputs:
+ issues: ${{ steps.linked_issues.outputs.result }}
+ steps:
+ - name: Fetching issues linked to pull request
+ id: linked_issues
+ uses: actions/github-script@v7
+ env:
+ PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ with:
+ script: |
+ const response = await github.graphql(`
+ query ($owner: String!, $repo: String!, $pr: Int!) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $pr) {
+ number
+ title
+ closingIssuesReferences(first: 100) {
+ nodes {
+ number
+ title
+ closed
+ }
+ }
+ }
+ }
+ }
+ `, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pr: Number(process.env.PULL_REQUEST_NUMBER),
+ });
+
+ const { repository: { pullRequest: { closingIssuesReferences } } } = response;
+ const issues = closingIssuesReferences.nodes.map(({ number }) => number);
+
+ if (!issues.length) {
+ core.notice(`No linked issues found for pull request #${ process.env.PULL_REQUEST_NUMBER }`);
+ return;
+ }
+
+ core.info(`Found ${ issues.length } issue(s): ${ issues.join(', ') || '-' }`);
+
+ return issues;
+
+ issues_in_review:
+ name: Update status for issues in review
+ needs: [ pr_linked_issues ]
+ if: ${{ needs.pr_linked_issues.outputs.issues }}
+ uses: './.github/workflows/update-project-cards.yml'
+ secrets: inherit
+ with:
+ project_name: ${{ vars.PROJECT_NAME }}
+ field_name: Status
+ field_value: Review
+ issues: ${{ needs.pr_linked_issues.outputs.issues }}
+
+ copy_labels:
+ name: Copy issues labels to pull request
+ needs: [ pr_linked_issues ]
+ if: ${{ needs.pr_linked_issues.outputs.issues }}
+ uses: './.github/workflows/copy-issues-labels.yml'
+ secrets: inherit
+ with:
+ pr_number: ${{ github.event.pull_request.number }}
+ issues: ${{ needs.pr_linked_issues.outputs.issues }}
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
new file mode 100644
index 0000000000..97c1102e8f
--- /dev/null
+++ b/.github/workflows/publish-image.yml
@@ -0,0 +1,84 @@
+name: Publish Docker image
+
+on:
+ workflow_dispatch:
+ inputs:
+ tags:
+ description: Image tags
+ required: false
+ type: string
+ build_args:
+ description: Build-time variables
+ required: false
+ type: string
+ platforms:
+ description: Image platforms (you can specify multiple platforms separated by comma)
+ required: false
+ type: string
+ default: linux/amd64
+ workflow_call:
+ inputs:
+ tags:
+ description: Image tags
+ required: false
+ type: string
+ build_args:
+ description: Build-time variables
+ required: false
+ type: string
+ platforms:
+ description: Image platforms (you can specify multiple platforms separated by comma)
+ required: false
+ type: string
+ default: linux/amd64
+
+jobs:
+ run:
+ name: Run
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ # Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/blockscout/frontend
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Add SHORT_SHA env property with commit short sha
+ run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
+
+ - name: Debug
+ env:
+ REF_TYPE: ${{ github.ref_type }}
+ REF_NAME: ${{ github.ref_name }}
+ run: |
+ echo "ref_type: $REF_TYPE"
+ echo "ref_name: $REF_NAME"
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ cache-from: type=gha
+ tags: ${{ inputs.tags || steps.meta.outputs.tags }}
+ platforms: ${{ inputs.platforms }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
+ GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }}
+ ${{ inputs.build_args }}
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..df2c0433b1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,91 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ release:
+ types: [ released ]
+
+jobs:
+ remove_prerelease_label:
+ name: Remove pre-release label from issues
+ runs-on: ubuntu-latest
+ steps:
+ - name: Remove label
+ id: tags
+ uses: actions/github-script@v7
+ env:
+ LABEL_NAME: pre-release
+ with:
+ script: |
+ const { data: issues } = await github.request('GET /repos/{owner}/{repo}/issues', {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: process.env.LABEL_NAME,
+ state: 'all'
+ });
+
+ if (issues.length === 0) {
+ core.notice(`No issues with label "${ process.env.LABEL_NAME }" found.`);
+ return;
+ }
+
+ const issueIds = issues.map(({ node_id }) => node_id);
+ const labelId = issues[0].labels.find(({ name }) => name === process.env.LABEL_NAME).node_id;
+
+ core.info(`Found ${ issueIds.length } issues with label "${ process.env.LABEL_NAME }"`);
+
+ for (const issueId of issueIds) {
+ core.info(`Removing label for issue with node_id ${ issueId }...`);
+
+ await github.graphql(`
+ mutation($input: RemoveLabelsFromLabelableInput!) {
+ removeLabelsFromLabelable(input: $input) {
+ clientMutationId
+ }
+ }
+ `, {
+ input: {
+ labelIds: [ labelId ],
+ labelableId: issueId
+ },
+ });
+
+ core.info('Done.\n');
+ }
+
+
+ label_released_issues:
+ name: Label released issues
+ uses: './.github/workflows/label-issues-in-release.yml'
+ with:
+ tag: ${{ github.ref_name }}
+ label_name: ${{ github.ref_name }}
+ label_description: Release ${{ github.ref_name }}
+ secrets: inherit
+
+ update_project_cards:
+ name: Update project tasks statuses
+ needs: label_released_issues
+ uses: './.github/workflows/update-project-cards.yml'
+ with:
+ project_name: ${{ vars.PROJECT_NAME }}
+ field_name: Status
+ field_value: Released
+ issues: ${{ needs.label_released_issues.outputs.issues }}
+ secrets: inherit
+
+ publish_image:
+ name: Publish Docker image
+ uses: './.github/workflows/publish-image.yml'
+ secrets: inherit
+ with:
+ platforms: linux/amd64,linux/arm64/v8
+
+ # Temporary disable this step because it is broken
+ # There is an issue with building web3modal deps
+ upload_source_maps:
+ name: Upload source maps to Sentry
+ if: false
+ needs: publish_image
+ uses: './.github/workflows/upload-source-maps.yml'
+ secrets: inherit
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
new file mode 100644
index 0000000000..bcc1a36ca1
--- /dev/null
+++ b/.github/workflows/stale-issues.yml
@@ -0,0 +1,29 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: "55 1 * * *"
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v5
+ with:
+ # issues
+ only-issue-labels: "need info"
+ days-before-issue-stale: 14
+ days-before-issue-close: 7
+ stale-issue-label: "stale"
+ stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
+ close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
+
+ # pull requests
+ days-before-pr-stale: -1
+ days-before-pr-close: -1
+
+ # other settings
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ close-issue-reason: "not_planned"
\ No newline at end of file
diff --git a/.github/workflows/update-project-cards.yml b/.github/workflows/update-project-cards.yml
new file mode 100644
index 0000000000..b071ba61da
--- /dev/null
+++ b/.github/workflows/update-project-cards.yml
@@ -0,0 +1,240 @@
+name: Update project cards for issues
+
+on:
+ workflow_dispatch:
+ inputs:
+ project_name:
+ description: Name of the project
+ default: Front-end tasks
+ required: true
+ type: string
+ field_name:
+ description: Field name to be updated
+ default: Status
+ required: true
+ type: string
+ field_value:
+ description: New value of the field
+ default: Released
+ required: true
+ type: string
+ issues:
+ description: JSON encoded list of issue numbers to be updated
+ required: true
+ type: string
+ workflow_call:
+ inputs:
+ project_name:
+ description: Name of the project
+ required: true
+ type: string
+ field_name:
+ description: Field name to be updated
+ required: true
+ type: string
+ field_value:
+ description: New value of the field
+ required: true
+ type: string
+ issues:
+ description: JSON encoded list of issue numbers to be updated
+ required: true
+ type: string
+
+jobs:
+ run:
+ name: Run
+ runs-on: ubuntu-latest
+ steps:
+ - name: Getting project info
+ id: project_info
+ uses: actions/github-script@v7
+ env:
+ PROJECT_NAME: ${{ inputs.project_name }}
+ FIELD_NAME: ${{ inputs.field_name }}
+ FIELD_VALUE: ${{ inputs.field_value }}
+ with:
+ github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }}
+ script: |
+ const response = await github.graphql(`
+ query ($login: String!, $q: String!) {
+ organization(login: $login) {
+ projectsV2(query: $q, first: 1) {
+ nodes {
+ id,
+ title,
+ number,
+ fields(first: 20) {
+ nodes {
+ ... on ProjectV2Field {
+ id
+ name
+ }
+ ... on ProjectV2SingleSelectField {
+ id
+ name
+ options {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `, {
+ login: context.repo.owner,
+ q: process.env.PROJECT_NAME,
+ });
+
+ const { organization: { projectsV2: { nodes: projects } } } = response;
+
+ if (projects.length === 0) {
+ core.setFailed('Project not found.');
+ return;
+ }
+
+ if (projects.length > 1) {
+ core.info(`Fould ${ projects.length } with the similar name:`);
+ projects.forEach((issue) => {
+ core.info(` #${ projects.number } - ${ projects.title }`);
+ })
+ core.setFailed('Fould multiple projects with the similar name. Cannot determine which one to use.');
+ return;
+ }
+
+ const { id: projectId, fields: { nodes: fields } } = projects[0];
+ const field = fields.find((field) => field.name === process.env.FIELD_NAME);
+
+ if (!field) {
+ core.setFailed(`Field with name "${ process.env.FIELD_NAME }" not found in the project.`);
+ return;
+ }
+
+ const option = field.options.find((option) => option.name === process.env.FIELD_VALUE);
+
+ if (!option) {
+ core.setFailed(`Option with name "${ process.env.FIELD_VALUE }" not found in the field possible values.`);
+ return;
+ }
+
+ core.info('Found following info:');
+ core.info(` project_id: ${ projectId }`);
+ core.info(` field_id: ${ field.id }`);
+ core.info(` field_value_id: ${ option.id }`);
+
+ core.setOutput('id', projectId);
+ core.setOutput('field_id', field.id);
+ core.setOutput('field_value_id', option.id);
+
+ - name: Getting project items that linked to the issues
+ id: items
+ uses: actions/github-script@v7
+ env:
+ ISSUES: ${{ inputs.issues }}
+ with:
+ github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }}
+ script: |
+ const result = [];
+ const issues = JSON.parse(process.env.ISSUES);
+
+ for (const issue of issues) {
+ const response = await getProjectItemId(issue);
+ response?.length > 0 && result.push(...response);
+ }
+
+ return result;
+
+ async function getProjectItemId(issueId) {
+ core.info(`Fetching project items for issue #${ issueId }...`);
+
+ try {
+ const response = await github.graphql(`
+ query ($owner: String!, $repo: String!, $id: Int!) {
+ repository(owner: $owner, name: $repo) {
+ issue(number: $id) {
+ title,
+ projectItems(first: 10) {
+ nodes {
+ id,
+ }
+ }
+ }
+ }
+ }
+ `,
+ {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ id: issueId,
+ }
+ );
+
+ const { repository: { issue: { projectItems: { nodes: projectItems } } } } = response;
+
+ if (projectItems.length === 0) {
+ core.info('No project items found.\n');
+ return [];
+ }
+
+ const ids = projectItems.map((item) => item.id);
+ core.info(`Found [ ${ ids.join(', ') } ].\n`);
+ return ids;
+
+ } catch (error) {
+ if (error.status === 404) {
+ core.info('Nothing has found.\n');
+ return [];
+ }
+ }
+ }
+
+ - name: Updating field value of the project items
+ id: updating_items
+ uses: actions/github-script@v7
+ env:
+ ITEMS: ${{ steps.items.outputs.result }}
+ PROJECT_ID: ${{ steps.project_info.outputs.id }}
+ FIELD_ID: ${{ steps.project_info.outputs.field_id }}
+ FIELD_VALUE_ID: ${{ steps.project_info.outputs.field_value_id }}
+ with:
+ github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }}
+ script: |
+ const items = JSON.parse(process.env.ITEMS);
+
+ if (items.length === 0) {
+ core.info('Nothing to update.');
+ core.notice('No project items found for provided issues. Nothing to update.');
+ return;
+ }
+
+ for (const item of items) {
+ core.info(`Changing field value for item ${ item }...`);
+ await changeItemFieldValue(item);
+ core.info('Done.\n');
+ }
+
+ async function changeItemFieldValue(itemId) {
+ return await github.graphql(
+ `
+ mutation($input: UpdateProjectV2ItemFieldValueInput!) {
+ updateProjectV2ItemFieldValue(input: $input) {
+ clientMutationId
+ }
+ }
+ `,
+ {
+ input: {
+ projectId: process.env.PROJECT_ID,
+ fieldId: process.env.FIELD_ID,
+ itemId,
+ value: {
+ singleSelectOptionId: process.env.FIELD_VALUE_ID,
+ },
+ },
+ }
+ );
+ };
+
diff --git a/.github/workflows/upload-source-maps.yml b/.github/workflows/upload-source-maps.yml
new file mode 100644
index 0000000000..55b18067fe
--- /dev/null
+++ b/.github/workflows/upload-source-maps.yml
@@ -0,0 +1,48 @@
+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@v4
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.11.0
+ cache: 'yarn'
+
+ - name: Cache node_modules
+ uses: actions/cache@v4
+ 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
+
+ - 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 }} --url-prefix=~/_next/ --validate ./.next
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..24bd046ba4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,60 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/node_modules_linux
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+/public/assets/envs.js
+/public/assets/configs
+/public/icons/sprite.svg
+/public/icons/README.md
+/analyze
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+.tools
+grafana
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+/configs/envs/.env.secrets
+/configs/envs/.samples
+
+# typescript
+*.tsbuildinfo
+
+.eslintcache
+
+# Sentry
+.sentryclirc
+
+**.decrypted~**
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/playwright/.browser/
+/playwright/envs.js
+/playwright/affected-tests.txt
+
+**.dec**
+
+# build outputs
+/tools/preset-sync/index.js
diff --git a/.husky/.gitignore b/.husky/.gitignore
new file mode 100644
index 0000000000..31354ec138
--- /dev/null
+++ b/.husky/.gitignore
@@ -0,0 +1 @@
+_
diff --git a/.husky/post-checkout b/.husky/post-checkout
new file mode 100755
index 0000000000..c37815e2b5
--- /dev/null
+++ b/.husky/post-checkout
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; }
+git lfs post-checkout "$@"
diff --git a/.husky/post-commit b/.husky/post-commit
new file mode 100755
index 0000000000..e5230c305f
--- /dev/null
+++ b/.husky/post-commit
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; }
+git lfs post-commit "$@"
diff --git a/.husky/post-merge b/.husky/post-merge
new file mode 100755
index 0000000000..c99b752a52
--- /dev/null
+++ b/.husky/post-merge
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; }
+git lfs post-merge "$@"
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000000..c81f321caf
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,17 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+# lint js/ts files
+echo 🧿 Running file linter...
+npx lint-staged
+
+# format svg
+echo 🧿 Running svg formatter...
+for file in `git diff --diff-filter=ACMRT --cached --name-only | grep ".svg\$"`
+ do
+ echo "Formatting $file"
+ ./node_modules/.bin/svgo -q $file
+ git add $file
+ done
+
+echo ✅ All pre-commit jobs are done
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100755
index 0000000000..216e91527e
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; }
+git lfs pre-push "$@"
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000000..8b0beab16a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+20.11.0
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000000..e2c659e246
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+ "recommendations": [
+ "streetsidesoftware.code-spell-checker",
+ "formulahendry.auto-close-tag",
+ "formulahendry.auto-rename-tag",
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "ms-vscode-remote.remote-containers",
+ "ms-azuretools.vscode-docker",
+ "github.vscode-pull-request-github",
+ "yatki.vscode-surround",
+ "simonsiefke.svg-preview"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000..b24083bbe1
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,25 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Jest: watch current file",
+ "program": "${workspaceFolder}/node_modules/jest/bin/jest",
+ "args": [
+ "${fileBasename}",
+ "--runInBand",
+ "--verbose",
+ "-i",
+ "--no-cache",
+ "--watchAll",
+ "--testTimeout=1000000000",
+ ],
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..55712c19f1
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000000..0d8a70e1ea
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,381 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ // DEV SERVER
+ {
+ "type": "shell",
+ "command": "yarn dev:preset ${input:dev_config_preset}",
+ "problemMatcher": [],
+ "label": "dev server",
+ "detail": "start local dev server",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiMagenta",
+ "id": "server-process"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "shell",
+ "command": "yarn dev:preset:sync --name=${input:dev_config_preset}",
+ "problemMatcher": [],
+ "label": "dev preset sync",
+ "detail": "syncronize dev preset",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiMagenta",
+ "id": "repo-sync"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+
+ // CODE CHECKS
+ {
+ "type": "typescript",
+ "label": "tsc build",
+ "detail": "compile typescript",
+ "tsconfig": "tsconfig.json",
+ "problemMatcher": [
+ "$tsc"
+ ],
+ "icon": {
+ "color": "terminal.ansiCyan",
+ "id": "symbol-type-parameter"
+ },
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "group": "build",
+ },
+ {
+ "type": "npm",
+ "script": "lint:eslint:fix",
+ "problemMatcher": [],
+ "label": "eslint",
+ "detail": "run eslint",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiYellow",
+ "id": "zap"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+
+ // PW TESTS
+ {
+ "type": "shell",
+ "command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
+ "problemMatcher": [],
+ "label": "pw: local",
+ "detail": "run visual components tests for current file",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "beaker"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ },
+ },
+ {
+ "type": "shell",
+ "command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
+ "problemMatcher": [],
+ "label": "pw: docker",
+ "detail": "run visual components tests for current file",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "beaker"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ },
+ },
+ {
+ "type": "shell",
+ "command": "yarn test:pw:docker ${input:pwArgs}",
+ "problemMatcher": [],
+ "label": "pw: docker all",
+ "detail": "run visual components tests",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "beaker"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "shell",
+ "command": "npx playwright show-report",
+ "problemMatcher": [],
+ "label": "pw: report",
+ "detail": "serve test report",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "output"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "shell",
+ "command": "yarn test:pw:detect-affected",
+ "problemMatcher": [],
+ "label": "pw: detect affected",
+ "detail": "detect PW tests affected by changes in current branch",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "diff"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ },
+ },
+
+ // JEST TESTS
+ {
+ "type": "npm",
+ "script": "test:jest",
+ "problemMatcher": [],
+ "label": "jest",
+ "detail": "run jest tests",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "beaker"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "npm",
+ "script": "test:jest:watch",
+ "problemMatcher": [],
+ "label": "jest: watch all",
+ "detail": "run jest tests in watch mode",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "beaker"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "shell",
+ "command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
+ "problemMatcher": [],
+ "label": "jest: watch",
+ "detail": "run jest tests in watch mode for current file",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiBlue",
+ "id": "beaker"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ },
+ },
+
+ {
+ "type": "npm",
+ "script": "build:docker",
+ "problemMatcher": [],
+ "label": "docker: build",
+ "detail": "build docker image",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiRed",
+ "id": "symbol-structure"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "shell",
+ "command": "yarn start:docker:preset ${input:dev_config_preset}",
+ "problemMatcher": [],
+ "label": "docker: run",
+ "detail": "run docker container with env preset",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiRed",
+ "id": "browser"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "type": "npm",
+ "script": "svg:format",
+ "problemMatcher": [],
+ "label": "format svg",
+ "detail": "format svg files with svgo",
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared",
+ "focus": true,
+ "close": false,
+ "revealProblems": "onProblem",
+ },
+ "icon": {
+ "color": "terminal.ansiCyan",
+ "id": "combine"
+ },
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ ],
+ "inputs": [
+ {
+ "type": "pickString",
+ "id": "pwDebugFlag",
+ "description": "What debug flag you want to use?",
+ "options": [
+ "",
+ "PWDEBUG=1",
+ "DEBUG=pw:browser,pw:api",
+ "DEBUG=*",
+ ],
+ "default": ""
+ },
+ {
+ "type": "pickString",
+ "id": "pwArgs",
+ "description": "What args you want to pass?",
+ "options": [
+ "",
+ "--update-snapshots",
+ "--update-snapshots --affected",
+ "--ui",
+ ],
+ "default": ""
+ },
+ {
+ "type": "pickString",
+ "id": "dev_config_preset",
+ "description": "Choose dev server config preset:",
+ "options": [
+ "main",
+ "localhost",
+ "arbitrum",
+ "base",
+ "celo_alfajores",
+ "garnet",
+ "gnosis",
+ "eth",
+ "eth_goerli",
+ "eth_sepolia",
+ "optimism",
+ "optimism_sepolia",
+ "polygon",
+ "rootstock_testnet",
+ "stability_testnet",
+ "zkevm",
+ "zksync",
+ ],
+ "default": "main"
+ },
+ ],
+}
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..5726fbc353
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,73 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+education, socio-economic status, nationality, personal appearance, race,
+religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at andrew@poa.network. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..a325403e92
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,142 @@
+# *****************************
+# *** STAGE 1: Dependencies ***
+# *****************************
+FROM node:20.11.0-alpine AS deps
+# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
+RUN apk add --no-cache libc6-compat python3 make g++
+RUN ln -sf /usr/bin/python3 /usr/bin/python
+
+### APP
+# Install dependencies
+WORKDIR /app
+COPY package.json yarn.lock ./
+RUN apk add git
+RUN yarn --frozen-lockfile
+
+
+### FEATURE REPORTER
+# Install dependencies
+WORKDIR /feature-reporter
+COPY ./deploy/tools/feature-reporter/package.json ./deploy/tools/feature-reporter/yarn.lock ./
+RUN yarn --frozen-lockfile
+
+
+### ENV VARIABLES CHECKER
+# Install dependencies
+WORKDIR /envs-validator
+COPY ./deploy/tools/envs-validator/package.json ./deploy/tools/envs-validator/yarn.lock ./
+RUN yarn --frozen-lockfile
+
+
+# *****************************
+# ****** STAGE 2: Build *******
+# *****************************
+FROM node:20.11.0-alpine AS builder
+RUN apk add --no-cache --upgrade libc6-compat bash
+
+# pass build args to env variables
+ARG GIT_COMMIT_SHA
+ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
+ARG GIT_TAG
+ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG
+ARG NEXT_OPEN_TELEMETRY_ENABLED
+ENV NEXT_OPEN_TELEMETRY_ENABLED=$NEXT_OPEN_TELEMETRY_ENABLED
+
+ENV NODE_ENV production
+
+### APP
+# Copy dependencies and source code
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Generate .env.registry with ENVs list and save build args into .env file
+COPY --chmod=755 ./deploy/scripts/collect_envs.sh ./
+RUN ./collect_envs.sh ./docs/ENVS.md
+
+# Next.js collects completely anonymous telemetry data about general usage.
+# Learn more here: https://nextjs.org/telemetry
+# Uncomment the following line in case you want to disable telemetry during the build.
+# ENV NEXT_TELEMETRY_DISABLED 1
+
+# Build app for production
+RUN yarn svg:build-sprite
+RUN yarn build
+
+
+### FEATURE REPORTER
+# Copy dependencies and source code, then build
+COPY --from=deps /feature-reporter/node_modules ./deploy/tools/feature-reporter/node_modules
+RUN cd ./deploy/tools/feature-reporter && yarn compile_config
+RUN cd ./deploy/tools/feature-reporter && yarn build
+
+
+### ENV VARIABLES CHECKER
+# Copy dependencies and source code, then build
+COPY --from=deps /envs-validator/node_modules ./deploy/tools/envs-validator/node_modules
+RUN cd ./deploy/tools/envs-validator && yarn build
+
+
+# *****************************
+# ******* STAGE 3: Run ********
+# *****************************
+# Production image, copy all the files and run next
+FROM node:20.11.0-alpine AS runner
+RUN apk add --no-cache --upgrade bash curl jq unzip
+
+### APP
+WORKDIR /app
+
+# Uncomment the following line in case you want to disable telemetry during runtime.
+# ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+# Set the correct permission for prerender cache
+RUN mkdir .next
+RUN chown nextjs:nodejs .next
+
+COPY --from=builder /app/next.config.js ./
+COPY --from=builder /app/public ./public
+COPY --from=builder /app/package.json ./package.json
+COPY --from=builder /app/deploy/tools/envs-validator/index.js ./envs-validator.js
+COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-reporter.js
+
+# Copy scripts
+## Entripoint
+COPY --chmod=755 ./deploy/scripts/entrypoint.sh .
+## ENV validator and client script maker
+COPY --chmod=755 ./deploy/scripts/validate_envs.sh .
+COPY --chmod=755 ./deploy/scripts/make_envs_script.sh .
+## Assets downloader
+COPY --chmod=755 ./deploy/scripts/download_assets.sh .
+## Favicon generator
+COPY --chmod=755 ./deploy/scripts/favicon_generator.sh .
+COPY ./deploy/tools/favicon-generator ./deploy/tools/favicon-generator
+RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"]
+RUN ["chmod", "-R", "777", "./public"]
+
+# Copy ENVs files
+COPY --from=builder /app/.env.registry .
+COPY --from=builder /app/.env .
+
+# Copy ENVs presets
+ARG ENVS_PRESET
+ENV ENVS_PRESET=$ENVS_PRESET
+COPY ./configs/envs ./configs/envs
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+ENTRYPOINT ["./entrypoint.sh"]
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT 3000
+
+CMD ["node", "server.js"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..20d40b6bce
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..1256043efe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+
Blockscout frontend
+
+
+ Frontend application for
+ Blockscout
+ blockchain explorer
+
+
+## Running and configuring the app
+
+App is distributed as a docker image. Here you can find information about the [package](https://github.com/blockscout/frontend/pkgs/container/frontend) and its recent [releases](https://github.com/blockscout/frontend/releases).
+
+You can configure your app by passing necessary environment variables when starting the container. See full list of ENVs and their description [here](./docs/ENVS.md).
+
+```sh
+docker run -p 3000:3000 --env-file ghcr.io/blockscout/frontend:latest
+```
+
+Alternatively, you can build your own docker image and run your app from that. Please follow this [guide](./docs/CUSTOM_BUILD.md).
+
+For more information on migrating from the previous frontend, please see the [frontend migration docs](https://docs.blockscout.com/for-developers/frontend-migration).
+
+## Contributing
+
+See our [Contribution guide](./docs/CONTRIBUTING.md) for pull request protocol. We expect contributors to follow our [code of conduct](./CODE_OF_CONDUCT.md) when submitting code or comments.
+
+## Resources
+- [App ENVs list](./docs/ENVS.md)
+- [Contribution guide](./docs/CONTRIBUTING.md)
+- [Making a custom build](./docs/CUSTOM_BUILD.md)
+- [Frontend migration guide](https://docs.blockscout.com/for-developers/frontend-migration)
+- [Manual deployment guide with backend and microservices](https://docs.blockscout.com/for-developers/deployment/manual-deployment-guide)
+
+## License
+
+[![License: GPL v3.0](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
+
+This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
new file mode 100644
index 0000000000..30ec4f8b6f
--- /dev/null
+++ b/RELEASE_NOTES.md
@@ -0,0 +1,33 @@
+## 🚀 New Features
+- Description of the new feature 1.
+- Description of the new feature 2.
+
+## 🐛 Bug Fixes
+- Description of the bug fix 1.
+- Description of the bug fix 2.
+
+## ⚡ Performance Improvements
+- Description of the performance improvement 1.
+- Description of the performance improvement 2.
+
+## 📦 Dependencies updates
+- Updated dependency: PackageName 1 to version x.x.x.
+- Updated dependency: PackageName 2 to version x.x.x.
+
+## ✨ Other Changes
+- Another minor change 1.
+- Another minor change 2.
+
+## 🚨 Changes in ENV variables
+- Added new environment variable: ENV_VARIABLE_NAME with value.
+- Updated existing environment variable: ENV_VARIABLE_NAME to new value.
+
+**Full list of the ENV variables**: [v1.2.3](https://github.com/blockscout/frontend/blob/v1.2.3/docs/ENVS.md)
+
+## 🦄 New Contributors
+- @contributor1 made their first contribution in https://github.com/blockscout/frontend/pull/1
+- @contributor2 made their first contribution in https://github.com/blockscout/frontend/pull/2
+
+---
+
+**Full Changelog**: https://github.com/blockscout/frontend/compare/v1.2.2...v1.2.3
diff --git a/configs/app/api.ts b/configs/app/api.ts
new file mode 100644
index 0000000000..0afe9a8aec
--- /dev/null
+++ b/configs/app/api.ts
@@ -0,0 +1,32 @@
+import stripTrailingSlash from 'lib/stripTrailingSlash';
+
+import { getEnvValue } from './utils';
+
+const apiHost = getEnvValue('NEXT_PUBLIC_API_HOST');
+const apiSchema = getEnvValue('NEXT_PUBLIC_API_PROTOCOL') || 'https';
+const apiPort = getEnvValue('NEXT_PUBLIC_API_PORT');
+const apiEndpoint = [
+ apiSchema || 'https',
+ '://',
+ apiHost,
+ apiPort && ':' + apiPort,
+].filter(Boolean).join('');
+
+const socketSchema = getEnvValue('NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL') || 'wss';
+const socketEndpoint = [
+ socketSchema,
+ '://',
+ apiHost,
+ apiPort && ':' + apiPort,
+].filter(Boolean).join('');
+
+const api = Object.freeze({
+ host: apiHost,
+ protocol: apiSchema,
+ port: apiPort,
+ endpoint: apiEndpoint,
+ socket: socketEndpoint,
+ basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_API_BASE_PATH') || ''),
+});
+
+export default api;
diff --git a/configs/app/app.ts b/configs/app/app.ts
new file mode 100644
index 0000000000..b0403fc673
--- /dev/null
+++ b/configs/app/app.ts
@@ -0,0 +1,23 @@
+import { getEnvValue } from './utils';
+
+const appPort = getEnvValue('NEXT_PUBLIC_APP_PORT');
+const appSchema = getEnvValue('NEXT_PUBLIC_APP_PROTOCOL');
+const appHost = getEnvValue('NEXT_PUBLIC_APP_HOST');
+const baseUrl = [
+ appSchema || 'https',
+ '://',
+ appHost,
+ appPort && ':' + appPort,
+].filter(Boolean).join('');
+const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development';
+
+const app = Object.freeze({
+ isDev,
+ protocol: appSchema,
+ host: appHost,
+ port: appPort,
+ baseUrl,
+ useProxy: getEnvValue('NEXT_PUBLIC_USE_NEXT_JS_PROXY') === 'true',
+});
+
+export default app;
diff --git a/configs/app/chain.ts b/configs/app/chain.ts
new file mode 100644
index 0000000000..0ee6d2de6f
--- /dev/null
+++ b/configs/app/chain.ts
@@ -0,0 +1,25 @@
+import { getEnvValue } from './utils';
+
+const DEFAULT_CURRENCY_DECIMALS = 18;
+
+const chain = Object.freeze({
+ id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'),
+ name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'),
+ shortName: getEnvValue('NEXT_PUBLIC_NETWORK_SHORT_NAME'),
+ currency: {
+ name: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_NAME'),
+ weiName: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME'),
+ symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'),
+ decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS,
+ },
+ secondaryCoin: {
+ symbol: getEnvValue('NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL'),
+ },
+ hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true',
+ tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
+ rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'),
+ isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
+ verificationType: getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') || 'mining',
+});
+
+export default chain;
diff --git a/configs/app/features/account.ts b/configs/app/features/account.ts
new file mode 100644
index 0000000000..120a94b7e9
--- /dev/null
+++ b/configs/app/features/account.ts
@@ -0,0 +1,52 @@
+import type { Feature } from './types';
+
+import stripTrailingSlash from 'lib/stripTrailingSlash';
+
+import app from '../app';
+import { getEnvValue } from '../utils';
+
+const authUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_AUTH_URL') || app.baseUrl);
+
+const logoutUrl = (() => {
+ try {
+ const envUrl = getEnvValue('NEXT_PUBLIC_LOGOUT_URL');
+ const auth0ClientId = getEnvValue('NEXT_PUBLIC_AUTH0_CLIENT_ID');
+ const returnUrl = authUrl + '/auth/logout';
+
+ if (!envUrl || !auth0ClientId) {
+ throw Error();
+ }
+
+ const url = new URL(envUrl);
+ url.searchParams.set('client_id', auth0ClientId);
+ url.searchParams.set('returnTo', returnUrl);
+
+ return url.toString();
+ } catch (error) {
+ return;
+ }
+})();
+
+const title = 'My account';
+
+const config: Feature<{ authUrl: string; logoutUrl: string }> = (() => {
+ if (
+ getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
+ authUrl &&
+ logoutUrl
+ ) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ authUrl,
+ logoutUrl,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/addressMetadata.ts b/configs/app/features/addressMetadata.ts
new file mode 100644
index 0000000000..5ca5b78ada
--- /dev/null
+++ b/configs/app/features/addressMetadata.ts
@@ -0,0 +1,27 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST');
+
+const title = 'Address metadata';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (apiHost) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: apiHost,
+ basePath: '',
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/addressVerification.ts b/configs/app/features/addressVerification.ts
new file mode 100644
index 0000000000..f5f6cf4cf3
--- /dev/null
+++ b/configs/app/features/addressVerification.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+import account from './account';
+import verifiedTokens from './verifiedTokens';
+
+const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
+
+const title = 'Address verification in "My account"';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (account.isEnabled && verifiedTokens.isEnabled && adminServiceApiHost) {
+ return Object.freeze({
+ title: 'Address verification in "My account"',
+ isEnabled: true,
+ api: {
+ endpoint: adminServiceApiHost,
+ basePath: '',
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/adsBanner.ts b/configs/app/features/adsBanner.ts
new file mode 100644
index 0000000000..f9860d1afc
--- /dev/null
+++ b/configs/app/features/adsBanner.ts
@@ -0,0 +1,89 @@
+import type { Feature } from './types';
+import type { AdButlerConfig } from 'types/client/adButlerConfig';
+import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders';
+import type { AdBannerProviders, AdBannerAdditionalProviders } from 'types/client/adProviders';
+
+import { getEnvValue, parseEnvJson } from '../utils';
+
+const provider: AdBannerProviders = (() => {
+ const envValue = getEnvValue('NEXT_PUBLIC_AD_BANNER_PROVIDER') as AdBannerProviders;
+
+ return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise';
+})();
+
+const additionalProvider = getEnvValue('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER') as AdBannerAdditionalProviders;
+
+const title = 'Banner ads';
+
+type AdsBannerFeaturePayload = {
+ provider: Exclude;
+} | {
+ provider: 'adbutler';
+ adButler: {
+ config: {
+ desktop: AdButlerConfig;
+ mobile: AdButlerConfig;
+ };
+ };
+} | {
+ provider: Exclude;
+ additionalProvider: 'adbutler';
+ adButler: {
+ config: {
+ desktop: AdButlerConfig;
+ mobile: AdButlerConfig;
+ };
+ };
+}
+
+const config: Feature = (() => {
+ if (provider === 'adbutler') {
+ const desktopConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP'));
+ const mobileConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE'));
+
+ if (desktopConfig && mobileConfig) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ provider,
+ adButler: {
+ config: {
+ desktop: desktopConfig,
+ mobile: mobileConfig,
+ },
+ },
+ });
+ }
+ } else if (provider !== 'none') {
+
+ if (additionalProvider === 'adbutler') {
+ const desktopConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP'));
+ const mobileConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE'));
+
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ provider,
+ additionalProvider,
+ adButler: {
+ config: {
+ desktop: desktopConfig,
+ mobile: mobileConfig,
+ },
+ },
+ });
+ }
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ provider,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/adsText.ts b/configs/app/features/adsText.ts
new file mode 100644
index 0000000000..82946f984f
--- /dev/null
+++ b/configs/app/features/adsText.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+import { SUPPORTED_AD_TEXT_PROVIDERS } from 'types/client/adProviders';
+import type { AdTextProviders } from 'types/client/adProviders';
+
+import { getEnvValue } from '../utils';
+
+const provider: AdTextProviders = (() => {
+ const envValue = getEnvValue('NEXT_PUBLIC_AD_TEXT_PROVIDER') as AdTextProviders;
+ return envValue && SUPPORTED_AD_TEXT_PROVIDERS.includes(envValue) ? envValue : 'coinzilla';
+})();
+
+const title = 'Text ads';
+
+const config: Feature<{ provider: AdTextProviders }> = (() => {
+ if (provider !== 'none') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ provider,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/beaconChain.ts b/configs/app/features/beaconChain.ts
new file mode 100644
index 0000000000..a29540b756
--- /dev/null
+++ b/configs/app/features/beaconChain.ts
@@ -0,0 +1,27 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const title = 'Beacon chain';
+
+const config: Feature<{ currency: { symbol: string } }> = (() => {
+ if (getEnvValue('NEXT_PUBLIC_HAS_BEACON_CHAIN') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ currency: {
+ symbol:
+ getEnvValue('NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL') ||
+ getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL') ||
+ '', // maybe we need some other default value here
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/blockchainInteraction.ts b/configs/app/features/blockchainInteraction.ts
new file mode 100644
index 0000000000..788e059ec9
--- /dev/null
+++ b/configs/app/features/blockchainInteraction.ts
@@ -0,0 +1,38 @@
+import type { Feature } from './types';
+
+import chain from '../chain';
+import { getEnvValue } from '../utils';
+
+const walletConnectProjectId = getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID');
+
+const title = 'Blockchain interaction (writing to contract, etc.)';
+
+const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
+
+ if (
+ // all chain parameters are required for wagmi provider
+ // @wagmi/chains/dist/index.d.ts
+ chain.id &&
+ chain.name &&
+ chain.currency.name &&
+ chain.currency.symbol &&
+ chain.currency.decimals &&
+ chain.rpcUrl &&
+ walletConnectProjectId
+ ) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ walletConnect: {
+ projectId: walletConnectProjectId,
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/bridgedTokens.ts b/configs/app/features/bridgedTokens.ts
new file mode 100644
index 0000000000..ae275c5397
--- /dev/null
+++ b/configs/app/features/bridgedTokens.ts
@@ -0,0 +1,27 @@
+import type { Feature } from './types';
+import type { BridgedTokenChain, TokenBridge } from 'types/client/token';
+
+import { getEnvValue, parseEnvJson } from '../utils';
+
+const title = 'Bridged tokens';
+
+const config: Feature<{ chains: Array; bridges: Array }> = (() => {
+ const chains = parseEnvJson>(getEnvValue('NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS'));
+ const bridges = parseEnvJson>(getEnvValue('NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES'));
+
+ if (chains && chains.length > 0 && bridges && bridges.length > 0) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ chains,
+ bridges,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/csvExport.ts b/configs/app/features/csvExport.ts
new file mode 100644
index 0000000000..442b4bedc7
--- /dev/null
+++ b/configs/app/features/csvExport.ts
@@ -0,0 +1,23 @@
+import type { Feature } from './types';
+
+import services from '../services';
+
+const title = 'Export data to CSV file';
+
+const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
+ if (services.reCaptcha.siteKey) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ reCaptcha: {
+ siteKey: services.reCaptcha.siteKey,
+ },
+ });
+ }
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/dataAvailability.ts b/configs/app/features/dataAvailability.ts
new file mode 100644
index 0000000000..add9e5fec5
--- /dev/null
+++ b/configs/app/features/dataAvailability.ts
@@ -0,0 +1,21 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const title = 'Data availability';
+
+const config: Feature<{ isEnabled: true }> = (() => {
+ if (getEnvValue('NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/deFiDropdown.ts b/configs/app/features/deFiDropdown.ts
new file mode 100644
index 0000000000..62b8fdcd14
--- /dev/null
+++ b/configs/app/features/deFiDropdown.ts
@@ -0,0 +1,21 @@
+import type { Feature } from './types';
+import type { DeFiDropdownItem } from 'types/client/deFiDropdown';
+
+import { getEnvValue, parseEnvJson } from '../utils';
+
+const items = parseEnvJson>(getEnvValue('NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS')) || [];
+
+const title = 'DeFi dropdown';
+
+const config: Feature<{ items: Array }> = items.length > 0 ?
+ Object.freeze({
+ title,
+ isEnabled: true,
+ items,
+ }) :
+ Object.freeze({
+ title,
+ isEnabled: false,
+ });
+
+export default config;
diff --git a/configs/app/features/faultProofSystem.ts b/configs/app/features/faultProofSystem.ts
new file mode 100644
index 0000000000..38a8f021db
--- /dev/null
+++ b/configs/app/features/faultProofSystem.ts
@@ -0,0 +1,22 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+import rollup from './rollup';
+
+const title = 'Fault proof system';
+
+const config: Feature<{ isEnabled: true }> = (() => {
+ if (rollup.isEnabled && rollup.type === 'optimistic' && getEnvValue('NEXT_PUBLIC_FAULT_PROOF_ENABLED') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/gasTracker.ts b/configs/app/features/gasTracker.ts
new file mode 100644
index 0000000000..c20242e602
--- /dev/null
+++ b/configs/app/features/gasTracker.ts
@@ -0,0 +1,37 @@
+import type { Feature } from './types';
+import { GAS_UNITS } from 'types/client/gasTracker';
+import type { GasUnit } from 'types/client/gasTracker';
+
+import { getEnvValue, parseEnvJson } from '../utils';
+
+const isDisabled = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_ENABLED') === 'false';
+
+const units = ((): Array => {
+ const envValue = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_UNITS');
+ if (!envValue) {
+ return [ 'usd', 'gwei' ];
+ }
+
+ const units = parseEnvJson>(envValue)?.filter((type) => GAS_UNITS.includes(type)) || [];
+
+ return units;
+})();
+
+const title = 'Gas tracker';
+
+const config: Feature<{ units: Array }> = (() => {
+ if (!isDisabled && units.length > 0) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ units,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/getGasButton.ts b/configs/app/features/getGasButton.ts
new file mode 100644
index 0000000000..db392f3c9a
--- /dev/null
+++ b/configs/app/features/getGasButton.ts
@@ -0,0 +1,35 @@
+import type { Feature } from './types';
+import type { GasRefuelProviderConfig } from 'types/client/gasRefuelProviderConfig';
+
+import chain from '../chain';
+import { getEnvValue, parseEnvJson } from '../utils';
+import marketplace from './marketplace';
+
+const value = parseEnvJson(getEnvValue('NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG'));
+
+const title = 'Get gas button';
+
+const config: Feature<{
+ name: string;
+ logoUrl?: string;
+ url: string;
+ dappId?: string;
+}> = (() => {
+ if (value) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ name: value.name,
+ logoUrl: value.logo,
+ url: value.url_template.replace('{chainId}', chain.id || ''),
+ dappId: marketplace.isEnabled ? value.dapp_id : undefined,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/googleAnalytics.ts b/configs/app/features/googleAnalytics.ts
new file mode 100644
index 0000000000..4fe9a9bf9e
--- /dev/null
+++ b/configs/app/features/googleAnalytics.ts
@@ -0,0 +1,24 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const propertyId = getEnvValue('NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID');
+
+const title = 'Google analytics';
+
+const config: Feature<{ propertyId: string }> = (() => {
+ if (propertyId) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ propertyId,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/graphqlApiDocs.ts b/configs/app/features/graphqlApiDocs.ts
new file mode 100644
index 0000000000..d26c3bfde3
--- /dev/null
+++ b/configs/app/features/graphqlApiDocs.ts
@@ -0,0 +1,25 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const defaultTxHash = getEnvValue('NEXT_PUBLIC_GRAPHIQL_TRANSACTION');
+
+const title = 'GraphQL API documentation';
+
+const config: Feature<{ defaultTxHash: string | undefined }> = (() => {
+
+ if (defaultTxHash === 'none') {
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ defaultTxHash,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/growthBook.ts b/configs/app/features/growthBook.ts
new file mode 100644
index 0000000000..af672c5ac9
--- /dev/null
+++ b/configs/app/features/growthBook.ts
@@ -0,0 +1,24 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY');
+
+const title = 'GrowthBook feature flagging and A/B testing';
+
+const config: Feature<{ clientKey: string }> = (() => {
+ if (clientKey) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ clientKey,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts
new file mode 100644
index 0000000000..3b25e5570c
--- /dev/null
+++ b/configs/app/features/index.ts
@@ -0,0 +1,36 @@
+export { default as account } from './account';
+export { default as addressVerification } from './addressVerification';
+export { default as addressMetadata } from './addressMetadata';
+export { default as adsBanner } from './adsBanner';
+export { default as adsText } from './adsText';
+export { default as beaconChain } from './beaconChain';
+export { default as bridgedTokens } from './bridgedTokens';
+export { default as blockchainInteraction } from './blockchainInteraction';
+export { default as csvExport } from './csvExport';
+export { default as dataAvailability } from './dataAvailability';
+export { default as deFiDropdown } from './deFiDropdown';
+export { default as faultProofSystem } from './faultProofSystem';
+export { default as gasTracker } from './gasTracker';
+export { default as getGasButton } from './getGasButton';
+export { default as googleAnalytics } from './googleAnalytics';
+export { default as graphqlApiDocs } from './graphqlApiDocs';
+export { default as growthBook } from './growthBook';
+export { default as marketplace } from './marketplace';
+export { default as metasuites } from './metasuites';
+export { default as mixpanel } from './mixpanel';
+export { default as mudFramework } from './mudFramework';
+export { default as multichainButton } from './multichainButton';
+export { default as nameService } from './nameService';
+export { default as publicTagsSubmission } from './publicTagsSubmission';
+export { default as restApiDocs } from './restApiDocs';
+export { default as rollup } from './rollup';
+export { default as safe } from './safe';
+export { default as sentry } from './sentry';
+export { default as sol2uml } from './sol2uml';
+export { default as stats } from './stats';
+export { default as suave } from './suave';
+export { default as txInterpretation } from './txInterpretation';
+export { default as userOps } from './userOps';
+export { default as validators } from './validators';
+export { default as verifiedTokens } from './verifiedTokens';
+export { default as web3Wallet } from './web3Wallet';
diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts
new file mode 100644
index 0000000000..72f5d1ebde
--- /dev/null
+++ b/configs/app/features/marketplace.ts
@@ -0,0 +1,77 @@
+import type { Feature } from './types';
+
+import chain from '../chain';
+import { getEnvValue, getExternalAssetFilePath } from '../utils';
+
+// config file will be downloaded at run-time and saved in the public folder
+const enabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ENABLED');
+const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL');
+const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM');
+const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM');
+const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
+const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
+const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL');
+const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP');
+const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL');
+const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
+const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY');
+const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID');
+
+const title = 'Marketplace';
+
+const config: Feature<(
+ { configUrl: string } |
+ { api: { endpoint: string; basePath: string } }
+) & {
+ submitFormUrl: string;
+ categoriesUrl: string | undefined;
+ suggestIdeasFormUrl: string | undefined;
+ securityReportsUrl: string | undefined;
+ featuredApp: string | undefined;
+ banner: { contentUrl: string; linkUrl: string } | undefined;
+ rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
+}> = (() => {
+ if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
+ const props = {
+ submitFormUrl,
+ categoriesUrl,
+ suggestIdeasFormUrl,
+ securityReportsUrl,
+ featuredApp,
+ banner: bannerContentUrl && bannerLinkUrl ? {
+ contentUrl: bannerContentUrl,
+ linkUrl: bannerLinkUrl,
+ } : undefined,
+ rating: ratingAirtableApiKey && ratingAirtableBaseId ? {
+ airtableApiKey: ratingAirtableApiKey,
+ airtableBaseId: ratingAirtableBaseId,
+ } : undefined,
+ };
+
+ if (configUrl) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ configUrl,
+ ...props,
+ });
+ } else if (adminServiceApiHost) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: adminServiceApiHost,
+ basePath: '',
+ },
+ ...props,
+ });
+ }
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/metasuites.ts b/configs/app/features/metasuites.ts
new file mode 100644
index 0000000000..333e7d5a8a
--- /dev/null
+++ b/configs/app/features/metasuites.ts
@@ -0,0 +1,21 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const title = 'MetaSuites extension';
+
+const config: Feature<{ isEnabled: true }> = (() => {
+ if (getEnvValue('NEXT_PUBLIC_METASUITES_ENABLED') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/mixpanel.ts b/configs/app/features/mixpanel.ts
new file mode 100644
index 0000000000..ef9fabd91f
--- /dev/null
+++ b/configs/app/features/mixpanel.ts
@@ -0,0 +1,24 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const projectToken = getEnvValue('NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN');
+
+const title = 'Mixpanel analytics';
+
+const config: Feature<{ projectToken: string }> = (() => {
+ if (projectToken) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ projectToken,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/mudFramework.ts b/configs/app/features/mudFramework.ts
new file mode 100644
index 0000000000..86df2af34a
--- /dev/null
+++ b/configs/app/features/mudFramework.ts
@@ -0,0 +1,22 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+import rollup from './rollup';
+
+const title = 'MUD framework';
+
+const config: Feature<{ isEnabled: true }> = (() => {
+ if (rollup.isEnabled && rollup.type === 'optimistic' && getEnvValue('NEXT_PUBLIC_HAS_MUD_FRAMEWORK') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/multichainButton.ts b/configs/app/features/multichainButton.ts
new file mode 100644
index 0000000000..47b1433d05
--- /dev/null
+++ b/configs/app/features/multichainButton.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+import type { MultichainProviderConfig } from 'types/client/multichainProviderConfig';
+
+import { getEnvValue, parseEnvJson } from '../utils';
+import marketplace from './marketplace';
+
+const value = parseEnvJson(getEnvValue('NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG'));
+
+const title = 'Multichain balance';
+
+const config: Feature<{name: string; logoUrl?: string; urlTemplate: string; dappId?: string }> = (() => {
+ if (value) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ name: value.name,
+ logoUrl: value.logo,
+ urlTemplate: value.url_template,
+ dappId: marketplace.isEnabled ? value.dapp_id : undefined,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/nameService.ts b/configs/app/features/nameService.ts
new file mode 100644
index 0000000000..3af536bbe2
--- /dev/null
+++ b/configs/app/features/nameService.ts
@@ -0,0 +1,27 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST');
+
+const title = 'Name service integration';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (apiHost) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: apiHost,
+ basePath: '',
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/publicTagsSubmission.ts b/configs/app/features/publicTagsSubmission.ts
new file mode 100644
index 0000000000..296d5e143a
--- /dev/null
+++ b/configs/app/features/publicTagsSubmission.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+
+import services from '../services';
+import { getEnvValue } from '../utils';
+import addressMetadata from './addressMetadata';
+
+const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
+
+const title = 'Public tag submission';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (services.reCaptcha.siteKey && addressMetadata.isEnabled && apiHost) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: apiHost,
+ basePath: '',
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/restApiDocs.ts b/configs/app/features/restApiDocs.ts
new file mode 100644
index 0000000000..ae25f05c0a
--- /dev/null
+++ b/configs/app/features/restApiDocs.ts
@@ -0,0 +1,25 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const DEFAULT_URL = `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml`;
+const envValue = getEnvValue('NEXT_PUBLIC_API_SPEC_URL');
+
+const title = 'REST API documentation';
+
+const config: Feature<{ specUrl: string }> = (() => {
+ if (envValue === 'none') {
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ specUrl: envValue || DEFAULT_URL,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/rollup.ts b/configs/app/features/rollup.ts
new file mode 100644
index 0000000000..2560734097
--- /dev/null
+++ b/configs/app/features/rollup.ts
@@ -0,0 +1,35 @@
+import type { Feature } from './types';
+import type { RollupType } from 'types/client/rollup';
+import { ROLLUP_TYPES } from 'types/client/rollup';
+
+import { getEnvValue } from '../utils';
+
+const type = (() => {
+ const envValue = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE');
+ return ROLLUP_TYPES.find((type) => type === envValue);
+})();
+
+const L1BaseUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL');
+const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL');
+
+const title = 'Rollup (L2) chain';
+
+const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string }> = (() => {
+
+ if (type && L1BaseUrl) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ type,
+ L1BaseUrl,
+ L2WithdrawalUrl,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/safe.ts b/configs/app/features/safe.ts
new file mode 100644
index 0000000000..b2762a78da
--- /dev/null
+++ b/configs/app/features/safe.ts
@@ -0,0 +1,33 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+function getApiUrl(): string | undefined {
+ try {
+ const envValue = getEnvValue('NEXT_PUBLIC_SAFE_TX_SERVICE_URL');
+ return new URL('/api/v1/safes', envValue).toString();
+ } catch (error) {
+ return;
+ }
+}
+
+const title = 'Safe address tags';
+
+const config: Feature<{ apiUrl: string }> = (() => {
+ const apiUrl = getApiUrl();
+
+ if (apiUrl) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ apiUrl,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/sentry.ts b/configs/app/features/sentry.ts
new file mode 100644
index 0000000000..fb840ca9e5
--- /dev/null
+++ b/configs/app/features/sentry.ts
@@ -0,0 +1,44 @@
+import type { Feature } from './types';
+
+import app from '../app';
+import { getEnvValue } from '../utils';
+
+const dsn = getEnvValue('NEXT_PUBLIC_SENTRY_DSN');
+const instance = (() => {
+ const envValue = getEnvValue('NEXT_PUBLIC_APP_INSTANCE');
+ if (envValue) {
+ return envValue;
+ }
+
+ return app.host?.replace('.blockscout.com', '').replaceAll('-', '_');
+})();
+const environment = getEnvValue('NEXT_PUBLIC_APP_ENV') || 'production';
+const release = getEnvValue('NEXT_PUBLIC_GIT_TAG');
+const title = 'Sentry error monitoring';
+
+const config: Feature<{
+ dsn: string;
+ instance: string;
+ release: string | undefined;
+ environment: string;
+ enableTracing: boolean;
+}> = (() => {
+ if (dsn && instance && environment) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ dsn,
+ instance,
+ release,
+ environment,
+ enableTracing: getEnvValue('NEXT_PUBLIC_SENTRY_ENABLE_TRACING') === 'true',
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/sol2uml.ts b/configs/app/features/sol2uml.ts
new file mode 100644
index 0000000000..5a0ac2d4be
--- /dev/null
+++ b/configs/app/features/sol2uml.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+
+import stripTrailingSlash from 'lib/stripTrailingSlash';
+
+import { getEnvValue } from '../utils';
+
+const apiEndpoint = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST');
+
+const title = 'Solidity to UML diagrams';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (apiEndpoint) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: apiEndpoint,
+ basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_VISUALIZE_API_BASE_PATH') || ''),
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/stats.ts b/configs/app/features/stats.ts
new file mode 100644
index 0000000000..d3a90ce061
--- /dev/null
+++ b/configs/app/features/stats.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+
+import stripTrailingSlash from 'lib/stripTrailingSlash';
+
+import { getEnvValue } from '../utils';
+
+const apiEndpoint = getEnvValue('NEXT_PUBLIC_STATS_API_HOST');
+
+const title = 'Blockchain statistics';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (apiEndpoint) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: apiEndpoint,
+ basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_STATS_API_BASE_PATH') || ''),
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/suave.ts b/configs/app/features/suave.ts
new file mode 100644
index 0000000000..f96b80ede4
--- /dev/null
+++ b/configs/app/features/suave.ts
@@ -0,0 +1,21 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const title = 'SUAVE chain';
+
+const config: Feature<{ isEnabled: true }> = (() => {
+ if (getEnvValue('NEXT_PUBLIC_IS_SUAVE_CHAIN') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/txInterpretation.ts b/configs/app/features/txInterpretation.ts
new file mode 100644
index 0000000000..c22067ee27
--- /dev/null
+++ b/configs/app/features/txInterpretation.ts
@@ -0,0 +1,34 @@
+import type { Feature } from './types';
+import type { Provider } from 'types/client/txInterpretation';
+import { PROVIDERS } from 'types/client/txInterpretation';
+
+import { getEnvValue } from '../utils';
+
+const title = 'Transaction interpretation';
+
+const provider: Provider = (() => {
+ const value = getEnvValue('NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER');
+
+ if (value && PROVIDERS.includes(value as Provider)) {
+ return value as Provider;
+ }
+
+ return 'none';
+})();
+
+const config: Feature<{ provider: Provider }> = (() => {
+ if (provider !== 'none') {
+ return Object.freeze({
+ title,
+ provider,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/types.ts b/configs/app/features/types.ts
new file mode 100644
index 0000000000..e0c4a26b52
--- /dev/null
+++ b/configs/app/features/types.ts
@@ -0,0 +1,10 @@
+type FeatureEnabled = Record> = { title: string; isEnabled: true } & Payload;
+type FeatureDisabled = { title: string; isEnabled: false };
+
+export type Feature = Record> = FeatureEnabled | FeatureDisabled;
+
+// typescript cannot properly resolve unions in nested objects - https://github.com/microsoft/TypeScript/issues/18758
+// so we use this little helper where it is needed
+export const getFeaturePayload = >(feature: Feature): Payload | undefined => {
+ return feature.isEnabled ? feature : undefined;
+};
diff --git a/configs/app/features/userOps.ts b/configs/app/features/userOps.ts
new file mode 100644
index 0000000000..0e127f62f6
--- /dev/null
+++ b/configs/app/features/userOps.ts
@@ -0,0 +1,21 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const title = 'User operations';
+
+const config: Feature<{ isEnabled: true }> = (() => {
+ if (getEnvValue('NEXT_PUBLIC_HAS_USER_OPS') === 'true') {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/validators.ts b/configs/app/features/validators.ts
new file mode 100644
index 0000000000..668501e28c
--- /dev/null
+++ b/configs/app/features/validators.ts
@@ -0,0 +1,29 @@
+import type { Feature } from './types';
+import { VALIDATORS_CHAIN_TYPE } from 'types/client/validators';
+import type { ValidatorsChainType } from 'types/client/validators';
+
+import { getEnvValue } from '../utils';
+
+const chainType = ((): ValidatorsChainType | undefined => {
+ const envValue = getEnvValue('NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE') as ValidatorsChainType | undefined;
+ return envValue && VALIDATORS_CHAIN_TYPE.includes(envValue) ? envValue : undefined;
+})();
+
+const title = 'Validators list';
+
+const config: Feature<{ chainType: ValidatorsChainType }> = (() => {
+ if (chainType) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ chainType,
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/verifiedTokens.ts b/configs/app/features/verifiedTokens.ts
new file mode 100644
index 0000000000..521851d295
--- /dev/null
+++ b/configs/app/features/verifiedTokens.ts
@@ -0,0 +1,27 @@
+import type { Feature } from './types';
+
+import { getEnvValue } from '../utils';
+
+const contractInfoApiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST');
+
+const title = 'Verified tokens info';
+
+const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
+ if (contractInfoApiHost) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ api: {
+ endpoint: contractInfoApiHost,
+ basePath: '',
+ },
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/features/web3Wallet.ts b/configs/app/features/web3Wallet.ts
new file mode 100644
index 0000000000..f4df4d56ea
--- /dev/null
+++ b/configs/app/features/web3Wallet.ts
@@ -0,0 +1,43 @@
+import type { Feature } from './types';
+import { SUPPORTED_WALLETS } from 'types/client/wallets';
+import type { WalletType } from 'types/client/wallets';
+
+import { getEnvValue, parseEnvJson } from '../utils';
+
+const wallets = ((): Array | undefined => {
+ const envValue = getEnvValue('NEXT_PUBLIC_WEB3_WALLETS');
+ if (envValue === 'none') {
+ return;
+ }
+
+ const wallets = parseEnvJson>(envValue)?.filter((type) => SUPPORTED_WALLETS.includes(type));
+
+ if (!wallets || wallets.length === 0) {
+ return [ 'metamask' ];
+ }
+
+ return wallets;
+})();
+
+const title = 'Web3 wallet integration (add token or network to the wallet)';
+
+const config: Feature<{ wallets: Array; addToken: { isDisabled: boolean }}> = (() => {
+ if (wallets && wallets.length > 0) {
+ return Object.freeze({
+ title,
+ isEnabled: true,
+ wallets,
+ addToken: {
+ isDisabled: getEnvValue('NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET') === 'true',
+ },
+ addNetwork: {},
+ });
+ }
+
+ return Object.freeze({
+ title,
+ isEnabled: false,
+ });
+})();
+
+export default config;
diff --git a/configs/app/index.ts b/configs/app/index.ts
new file mode 100644
index 0000000000..a946604593
--- /dev/null
+++ b/configs/app/index.ts
@@ -0,0 +1,19 @@
+import api from './api';
+import app from './app';
+import chain from './chain';
+import * as features from './features';
+import meta from './meta';
+import services from './services';
+import UI from './ui';
+
+const config = Object.freeze({
+ app,
+ chain,
+ api,
+ UI,
+ features,
+ services,
+ meta,
+});
+
+export default config;
diff --git a/configs/app/meta.ts b/configs/app/meta.ts
new file mode 100644
index 0000000000..3d7b777e03
--- /dev/null
+++ b/configs/app/meta.ts
@@ -0,0 +1,18 @@
+import app from './app';
+import { getEnvValue, getExternalAssetFilePath } from './utils';
+
+const defaultImageUrl = '/static/og_placeholder.png';
+
+const meta = Object.freeze({
+ promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') === 'false' ? false : true,
+ og: {
+ description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '',
+ imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl),
+ enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true',
+ },
+ seo: {
+ enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED') === 'true',
+ },
+});
+
+export default meta;
diff --git a/configs/app/services.ts b/configs/app/services.ts
new file mode 100644
index 0000000000..86df538215
--- /dev/null
+++ b/configs/app/services.ts
@@ -0,0 +1,7 @@
+import { getEnvValue } from './utils';
+
+export default Object.freeze({
+ reCaptcha: {
+ siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'),
+ },
+});
diff --git a/configs/app/ui.ts b/configs/app/ui.ts
new file mode 100644
index 0000000000..73308ede5b
--- /dev/null
+++ b/configs/app/ui.ts
@@ -0,0 +1,93 @@
+import type { ContractCodeIde } from 'types/client/contract';
+import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation';
+import type { ChainIndicatorId } from 'types/homepage';
+import type { NetworkExplorer } from 'types/networks';
+import type { ColorThemeId } from 'types/settings';
+
+import { COLOR_THEMES } from 'lib/settings/colorTheme';
+
+import * as views from './ui/views';
+import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
+
+const hiddenLinks = (() => {
+ const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS')) || [];
+
+ if (!Array.isArray(parsedValue)) {
+ return undefined;
+ }
+
+ const result = NAVIGATION_LINK_IDS.reduce((result, item) => {
+ result[item] = parsedValue.includes(item);
+ return result;
+ }, {} as Record);
+
+ return result;
+})();
+
+const highlightedRoutes = (() => {
+ const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES'));
+ return Array.isArray(parsedValue) ? parsedValue : [];
+})();
+
+const defaultColorTheme = (() => {
+ const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined;
+ return COLOR_THEMES.find((theme) => theme.id === envValue);
+})();
+
+// eslint-disable-next-line max-len
+const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
+
+const UI = Object.freeze({
+ navigation: {
+ logo: {
+ 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO'),
+ dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO_DARK'),
+ },
+ icon: {
+ 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON'),
+ dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'),
+ },
+ hiddenLinks,
+ highlightedRoutes,
+ otherLinks: parseEnvJson>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [],
+ featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'),
+ layout: (getEnvValue('NEXT_PUBLIC_NAVIGATION_LAYOUT') || 'vertical') as NavigationLayout,
+ },
+ footer: {
+ links: getExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS'),
+ frontendVersion: getEnvValue('NEXT_PUBLIC_GIT_TAG'),
+ frontendCommit: getEnvValue('NEXT_PUBLIC_GIT_COMMIT_SHA'),
+ },
+ homepage: {
+ charts: parseEnvJson>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [],
+ plate: {
+ background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT,
+ textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white',
+ },
+ showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
+ },
+ views,
+ indexingAlert: {
+ blocks: {
+ isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS') === 'true' ? true : false,
+ },
+ intTxs: {
+ isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS') === 'true' ? true : false,
+ },
+ },
+ maintenanceAlert: {
+ message: getEnvValue('NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE'),
+ },
+ explorers: {
+ items: parseEnvJson>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [],
+ },
+ ides: {
+ items: parseEnvJson>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
+ },
+ hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false,
+ colorTheme: {
+ 'default': defaultColorTheme,
+ },
+});
+
+export default UI;
diff --git a/configs/app/ui/views/address.ts b/configs/app/ui/views/address.ts
new file mode 100644
index 0000000000..088d288deb
--- /dev/null
+++ b/configs/app/ui/views/address.ts
@@ -0,0 +1,51 @@
+import type { SmartContractVerificationMethodExtra } from 'types/client/contract';
+import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS } from 'types/client/contract';
+import type { AddressViewId, IdenticonType } from 'types/views/address';
+import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address';
+
+import { getEnvValue, parseEnvJson } from 'configs/app/utils';
+
+const identiconType: IdenticonType = (() => {
+ const value = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE');
+
+ return IDENTICON_TYPES.find((type) => value === type) || 'jazzicon';
+})();
+
+const hiddenViews = (() => {
+ const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS')) || [];
+
+ if (!Array.isArray(parsedValue)) {
+ return undefined;
+ }
+
+ const result = ADDRESS_VIEWS_IDS.reduce((result, item) => {
+ result[item] = parsedValue.includes(item);
+ return result;
+ }, {} as Record);
+
+ return result;
+})();
+
+const extraVerificationMethods: Array = (() => {
+ const envValue = getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS');
+ if (envValue === 'none') {
+ return [];
+ }
+
+ if (!envValue) {
+ return SMART_CONTRACT_EXTRA_VERIFICATION_METHODS;
+ }
+
+ const parsedMethods = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS')) || [];
+
+ return SMART_CONTRACT_EXTRA_VERIFICATION_METHODS.filter((method) => parsedMethods.includes(method));
+})();
+
+const config = Object.freeze({
+ identiconType,
+ hiddenViews,
+ solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
+ extraVerificationMethods,
+});
+
+export default config;
diff --git a/configs/app/ui/views/block.ts b/configs/app/ui/views/block.ts
new file mode 100644
index 0000000000..5e42136a42
--- /dev/null
+++ b/configs/app/ui/views/block.ts
@@ -0,0 +1,25 @@
+import type { BlockFieldId } from 'types/views/block';
+import { BLOCK_FIELDS_IDS } from 'types/views/block';
+
+import { getEnvValue, parseEnvJson } from 'configs/app/utils';
+
+const blockHiddenFields = (() => {
+ const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS')) || [];
+
+ if (!Array.isArray(parsedValue)) {
+ return undefined;
+ }
+
+ const result = BLOCK_FIELDS_IDS.reduce((result, item) => {
+ result[item] = parsedValue.includes(item);
+ return result;
+ }, {} as Record);
+
+ return result;
+})();
+
+const config = Object.freeze({
+ hiddenFields: blockHiddenFields,
+});
+
+export default config;
diff --git a/configs/app/ui/views/index.ts b/configs/app/ui/views/index.ts
new file mode 100644
index 0000000000..4f1d77073b
--- /dev/null
+++ b/configs/app/ui/views/index.ts
@@ -0,0 +1,4 @@
+export { default as address } from './address';
+export { default as block } from './block';
+export { default as nft } from './nft';
+export { default as tx } from './tx';
diff --git a/configs/app/ui/views/nft.ts b/configs/app/ui/views/nft.ts
new file mode 100644
index 0000000000..b0d9f9b28c
--- /dev/null
+++ b/configs/app/ui/views/nft.ts
@@ -0,0 +1,9 @@
+import type { NftMarketplaceItem } from 'types/views/nft';
+
+import { getEnvValue, parseEnvJson } from 'configs/app/utils';
+
+const config = Object.freeze({
+ marketplaces: parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES')) || [],
+});
+
+export default config;
diff --git a/configs/app/ui/views/tx.ts b/configs/app/ui/views/tx.ts
new file mode 100644
index 0000000000..f725363504
--- /dev/null
+++ b/configs/app/ui/views/tx.ts
@@ -0,0 +1,41 @@
+import type { TxAdditionalFieldsId, TxFieldsId } from 'types/views/tx';
+import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from 'types/views/tx';
+
+import { getEnvValue, parseEnvJson } from 'configs/app/utils';
+
+const hiddenFields = (() => {
+ const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS')) || [];
+
+ if (!Array.isArray(parsedValue)) {
+ return undefined;
+ }
+
+ const result = TX_FIELDS_IDS.reduce((result, item) => {
+ result[item] = parsedValue.includes(item);
+ return result;
+ }, {} as Record);
+
+ return result;
+})();
+
+const additionalFields = (() => {
+ const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS')) || [];
+
+ if (!Array.isArray(parsedValue)) {
+ return undefined;
+ }
+
+ const result = TX_ADDITIONAL_FIELDS_IDS.reduce((result, item) => {
+ result[item] = parsedValue.includes(item);
+ return result;
+ }, {} as Record);
+
+ return result;
+})();
+
+const config = Object.freeze({
+ hiddenFields,
+ additionalFields,
+});
+
+export default config;
diff --git a/configs/app/utils.ts b/configs/app/utils.ts
new file mode 100644
index 0000000000..97a9afd05d
--- /dev/null
+++ b/configs/app/utils.ts
@@ -0,0 +1,48 @@
+import isBrowser from 'lib/isBrowser';
+import * as regexp from 'lib/regexp';
+
+export const replaceQuotes = (value: string | undefined) => value?.replaceAll('\'', '"');
+
+export const getEnvValue = (envName: string) => {
+ // eslint-disable-next-line no-restricted-properties
+ const envs = isBrowser() ? window.__envs : process.env;
+
+ if (isBrowser() && envs.NEXT_PUBLIC_APP_INSTANCE === 'pw') {
+ const storageValue = localStorage.getItem(envName);
+
+ if (typeof storageValue === 'string') {
+ return storageValue;
+ }
+ }
+
+ return replaceQuotes(envs[envName]);
+};
+
+export const parseEnvJson = (env: string | undefined): DataType | null => {
+ try {
+ return JSON.parse(env || 'null') as DataType | null;
+ } catch (error) {
+ return null;
+ }
+};
+
+export const getExternalAssetFilePath = (envName: string) => {
+ const parsedValue = getEnvValue(envName);
+
+ if (!parsedValue) {
+ return;
+ }
+
+ return buildExternalAssetFilePath(envName, parsedValue);
+};
+
+export const buildExternalAssetFilePath = (name: string, value: string) => {
+ try {
+ const fileName = name.replace(/^NEXT_PUBLIC_/, '').replace(/_URL$/, '').toLowerCase();
+ const url = new URL(value);
+ const fileExtension = url.pathname.match(regexp.FILE_EXTENSION)?.[1];
+ return `/assets/configs/${ fileName }.${ fileExtension }`;
+ } catch (error) {
+ return;
+ }
+};
diff --git a/configs/envs/.env.arbitrum b/configs/envs/.env.arbitrum
new file mode 100644
index 0000000000..a1ec2fef59
--- /dev/null
+++ b/configs/envs/.env.arbitrum
@@ -0,0 +1,44 @@
+# Set of ENVs for Arbitrum One network explorer
+# https://arbitrum.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=arbitrum"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=arbitrum.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x37c798810d49ba132b40efe7f4fdf6806a8fc58226bb5e185ddc91f896577abf
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(27, 74, 221, 1)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://blockscout-arbitrum.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_ENABLED=false
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/arbitrum/pools'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-one-icon-light.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-one-icon-dark.svg
+NEXT_PUBLIC_NETWORK_ID=42161
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-one-logo-light.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-one-logo-dark.svg
+NEXT_PUBLIC_NETWORK_NAME=Arbitrum One
+NEXT_PUBLIC_NETWORK_RPC_URL=https://arbitrum.llamarpc.com
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum One
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-one.png
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com
+NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.base b/configs/envs/.env.base
new file mode 100644
index 0000000000..2c9cdf6699
--- /dev/null
+++ b/configs/envs/.env.base
@@ -0,0 +1,66 @@
+# Set of ENVs for Base Mainnet network explorer
+# https://base.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=base"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "728301", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "728302", "width": "320", "height": "100" }
+NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=base.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'aerodrome'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/base-mainnet.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xfd5c5dae7b69fe29e61d19b9943e688aa0f1be1e983c4fba8fe985f90ff69d5f
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+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_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://basechain.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/0d18fc309ff499075127b364cc69306d/raw/2a51f961a8c1b9f884f2ab7eed79d4b69330e1ae/peanut_protocol_banner.html
+NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://base.blockscout.com/apps/peanut-protocol
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/base/pools'}},{'title':'L2scan','baseUrl':'https://base.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/base'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/base/transaction','address':'/base/address'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
+NEXT_PUBLIC_NETWORK_ID=8453
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
+NEXT_PUBLIC_NETWORK_NAME=Base Mainnet
+NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.base.org/
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Mainnet
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://base.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
+NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.base.org/withdraw
+NEXT_PUBLIC_ROLLUP_TYPE=optimistic
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-base.safe.global
+NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-base-mainnet.k8s-prod-1.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.celo_alfajores b/configs/envs/.env.celo_alfajores
new file mode 100644
index 0000000000..2cbb790629
--- /dev/null
+++ b/configs/envs/.env.celo_alfajores
@@ -0,0 +1,39 @@
+# Set of ENVs for Celo Alfajores network explorer
+# https://celo-alfajores.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=celo_alfajores"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=celo-alfajores.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(252, 255, 82, 1)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(0, 0, 0, 1)
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_MARKETPLACE_ENABLED=false
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=CELO
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=CELO
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-light.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-dark.svg
+NEXT_PUBLIC_NETWORK_ID=44787
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-light.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-dark.svg
+NEXT_PUBLIC_NETWORK_NAME=Celo Alfajores
+NEXT_PUBLIC_NETWORK_RPC_URL=https://alfajores-forno.celo-testnet.org
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Alfajores
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/celo.png
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth
new file mode 100644
index 0000000000..1c1524cc72
--- /dev/null
+++ b/configs/envs/.env.eth
@@ -0,0 +1,64 @@
+# Set of ENVs for Ethereum network explorer
+# https://eth.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=eth"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "728471", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "728470", "width": "320", "height": "100" }
+NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=eth.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/eth-mainnet.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xd01175f1efa23f36c5579b3c13e2bbd0885017643a7efef5cbcb6b474384dfa8
+NEXT_PUBLIC_HAS_BEACON_CHAIN=true
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap']
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://ethereum-mainnet.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=gearbox-protocol
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/eth/pools'}},{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'blockchair','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/blockchair.png','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'sentio','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/sentio.png','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/0xPPL.png','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ]
+NEXT_PUBLIC_NETWORK_ID=1
+NEXT_PUBLIC_NETWORK_NAME=Ethereum
+NEXT_PUBLIC_NETWORK_RPC_URL=https://eth.llamarpc.com
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Ethereum
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/eth.jpg
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://eth.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
+NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
+NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
diff --git a/configs/envs/.env.eth_goerli b/configs/envs/.env.eth_goerli
new file mode 100644
index 0000000000..5bf4167f20
--- /dev/null
+++ b/configs/envs/.env.eth_goerli
@@ -0,0 +1,59 @@
+# Set of ENVs for Goerli testnet network explorer
+# https://eth-goerli.blockscout.com/
+
+# app configuration
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+
+# blockchain parameters
+NEXT_PUBLIC_NETWORK_NAME=Goerli
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
+NEXT_PUBLIC_NETWORK_ID=5
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
+NEXT_PUBLIC_IS_TESTNET=true
+
+# api configuration
+NEXT_PUBLIC_API_HOST=eth-goerli.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+
+# ui config
+## homepage
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+## sidebar
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
+## footer
+##views
+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'}]
+## misc
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
+NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
+
+# app features
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_AUTH_URL=http://localhost:3000
+NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
+NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
+NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
+NEXT_PUBLIC_HAS_BEACON_CHAIN=true
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+
+#meta
+NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia
new file mode 100644
index 0000000000..1304b37c72
--- /dev/null
+++ b/configs/envs/.env.eth_sepolia
@@ -0,0 +1,65 @@
+# Set of ENVs for Sepolia network explorer
+# https://eth-sepolia.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=eth_sepolia"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363
+NEXT_PUBLIC_HAS_BEACON_CHAIN=true
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(51, 53, 67, 1)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(165, 252, 122, 1)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/sepolia-testnet/pools'}},{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png','baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}} ]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
+NEXT_PUBLIC_NETWORK_ID=11155111
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
+NEXT_PUBLIC_NETWORK_NAME=Sepolia
+NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Sepolia
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
+NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
+NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.garnet b/configs/envs/.env.garnet
new file mode 100644
index 0000000000..a6763934bc
--- /dev/null
+++ b/configs/envs/.env.garnet
@@ -0,0 +1,50 @@
+# Set of ENVs for Garnet (dev only)
+# https://https://explorer.garnetchain.com//
+
+# app configuration
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+
+# blockchain parameters
+NEXT_PUBLIC_NETWORK_NAME="Garnet Testnet"
+NEXT_PUBLIC_NETWORK_ID=17069
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_RPC_URL=https://partner-rpc.garnetchain.com/tireless-strand-dreamt-overcome
+
+# api configuration
+NEXT_PUBLIC_API_HOST=explorer.garnetchain.com
+NEXT_PUBLIC_API_BASE_PATH=/
+
+# ui config
+## homepage
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+## views
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+# app features
+NEXT_PUBLIC_APP_INSTANCE=local
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/redstone-testnet.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/redstone.json
+NEXT_PUBLIC_AD_BANNER_PROVIDER=none
+## sidebar
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet.svg
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet-dark.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet-dark.svg
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(169, 31, 47)
+NEXT_PUBLIC_OG_DESCRIPTION="Redstone is the home for onchain games, worlds, and other MUD applications"
+# rollup
+NEXT_PUBLIC_ROLLUP_TYPE=optimistic
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-holesky.blockscout.com/
+NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://garnet.qry.live/withdraw
+NEXT_PUBLIC_HAS_MUD_FRAMEWORK=true
\ No newline at end of file
diff --git a/configs/envs/.env.gnosis b/configs/envs/.env.gnosis
new file mode 100644
index 0000000000..fad2dde745
--- /dev/null
+++ b/configs/envs/.env.gnosis
@@ -0,0 +1,69 @@
+# Set of ENVs for Gnosis chain network explorer
+# https://gnosis.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=gnosis"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'523705','width':'728','height':'90'}
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'539876','width':'320','height':'100'}
+NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=gnosis.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL=GNO
+NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'},{'type':'amb','title':'Arbitrary Message Bridge','short_title':'AMB'}]
+NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://eth.blockscout.com/token/'},{'id':'56','title':'Binance Smart Chain','short_title':'BSC','base_url':'https://bscscan.com/token/'},{'id':'99','title':'POA','short_title':'POA','base_url':'https://blockscout.com/poa/core/token/'}]
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/gnosis-chain-mainnet.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/gnosis.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x082762f95047d39d612daafec832f88163f3815fde4ddd8944f2a5198a396e0f
+NEXT_PUBLIC_HAS_BEACON_CHAIN=true
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'tvl']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(46, 74, 60)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://login.blockscout.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/0d18fc309ff499075127b364cc69306d/raw/2a51f961a8c1b9f884f2ab7eed79d4b69330e1ae/peanut_protocol_banner.html
+NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://gnosis.blockscout.com/apps/peanut-protocol
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=XDAI
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=XDAI
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/xdai/pools'}},{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/gnosis-chain'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/gnosis-chain/transaction','address':'/gnosis-chain/address'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/gnosis.svg
+NEXT_PUBLIC_NETWORK_ID=100
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/gnosis.svg
+NEXT_PUBLIC_NETWORK_NAME=Gnosis chain
+NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com
+NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Gnosis chain
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/gnosis-chain-mainnet.png
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://gnosis.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-gnosis-chain.safe.global
+NEXT_PUBLIC_STATS_API_HOST=https://stats-gnosis-mainnet.k8s-prod-1.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.jest b/configs/envs/.env.jest
new file mode 100644
index 0000000000..e1f80c7e75
--- /dev/null
+++ b/configs/envs/.env.jest
@@ -0,0 +1,53 @@
+# Set of ENVs for Jest unit tests
+
+# app configuration
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+
+# blockchain parameters
+NEXT_PUBLIC_NETWORK_NAME=Blockscout
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout
+NEXT_PUBLIC_NETWORK_ID=1
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_NETWORK_RPC_URL=https://localhost:1111
+NEXT_PUBLIC_IS_TESTNET=true
+
+# api configuration
+NEXT_PUBLIC_API_HOST=localhost
+NEXT_PUBLIC_API_PORT=3003
+NEXT_PUBLIC_API_BASE_PATH=/
+
+# ui config
+## homepage
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
+NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
+## sidebar
+NEXT_PUBLIC_NETWORK_LOGO=
+NEXT_PUBLIC_NETWORK_LOGO_DARK=
+NEXT_PUBLIC_NETWORK_ICON=
+NEXT_PUBLIC_NETWORK_ICON_DARK=
+NEXT_PUBLIC_FEATURED_NETWORKS=
+## footer
+NEXT_PUBLIC_FOOTER_LINKS=
+NEXT_PUBLIC_GIT_TAG=v1.0.11
+## misc
+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'}}]
+
+# app features
+NEXT_PUBLIC_APP_INSTANCE=jest
+NEXT_PUBLIC_APP_ENV=testing
+NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_AUTH_URL=http://localhost:3100
+NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
+NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
+NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006
+NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
diff --git a/configs/envs/.env.localhost b/configs/envs/.env.localhost
new file mode 100644
index 0000000000..3956c0d11c
--- /dev/null
+++ b/configs/envs/.env.localhost
@@ -0,0 +1,39 @@
+# Set of ENVs for local network explorer
+# frontend app URL - https://localhost:3000/
+# API URL - https://localhost:3001/
+
+# app configuration
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+
+# blockchain parameters
+NEXT_PUBLIC_NETWORK_NAME=POA
+NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
+NEXT_PUBLIC_NETWORK_ID=99
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
+
+# api configuration
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=localhost
+NEXT_PUBLIC_API_PROTOCOL=http
+NEXT_PUBLIC_API_PORT=3001
+
+# ui config
+## homepage
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
+## sidebar
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
+## footer
+## misc
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
+
+# app features
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_AUTH_URL=http://localhost:3000
+NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
diff --git a/configs/envs/.env.main b/configs/envs/.env.main
new file mode 100644
index 0000000000..d33f7ec621
--- /dev/null
+++ b/configs/envs/.env.main
@@ -0,0 +1,68 @@
+# Set of ENVs for Sepolia network explorer
+# https://eth-sepolia.k8s-dev.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=main"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
+NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.k8s-dev.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
+NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363
+NEXT_PUBLIC_HAS_BEACON_CHAIN=true
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(51, 53, 67, 1)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(165, 252, 122, 1)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/test-configs/marketplace-security-report-mock.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps']
+NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png', 'baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}} ]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
+NEXT_PUBLIC_NETWORK_ID=11155111
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
+NEXT_PUBLIC_NETWORK_NAME=Sepolia
+NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Sepolia
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
+NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
+NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer-test.k8s-dev.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.optimism b/configs/envs/.env.optimism
new file mode 100644
index 0000000000..f23300e95f
--- /dev/null
+++ b/configs/envs/.env.optimism
@@ -0,0 +1,69 @@
+# Set of ENVs for OP Mainnet network explorer
+# https://optimism.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "749780", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "749779", "width": "320", "height": "100" }
+NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=optimism.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'velodrome'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
+NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97f34a4cf685e365460dd38dbe16e092d8e4cc4b6ac779e3abcf4c18df6b1329
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap', 'secondary_coin_price']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://optimism-goerli.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/0d18fc309ff499075127b364cc69306d/raw/2a51f961a8c1b9f884f2ab7eed79d4b69330e1ae/peanut_protocol_banner.html
+NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://optimism.blockscout.com/apps/peanut-protocol
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/optimism/pools'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
+NEXT_PUBLIC_NETWORK_ID=10
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
+NEXT_PUBLIC_NETWORK_NAME=OP Mainnet
+NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io
+NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP
+NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Mainnet
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
+NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
+NEXT_PUBLIC_ROLLUP_TYPE=optimistic
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-optimism.safe.global
+NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-mainnet.k8s-prod-1.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
\ No newline at end of file
diff --git a/configs/envs/.env.optimism_sepolia b/configs/envs/.env.optimism_sepolia
new file mode 100644
index 0000000000..8636924480
--- /dev/null
+++ b/configs/envs/.env.optimism_sepolia
@@ -0,0 +1,54 @@
+# Set of ENVs for OP Sepolia network explorer
+# https://optimism-sepolia.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism_sepolia"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=optimism-sepolia.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-sepolia.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x59d26836041ab35169bdce431d68d070b7b8acb589fa52e126e6c828b6ece5e9
+NEXT_PUBLIC_HAS_USER_OPS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_LOGOUT_URL=https://optimism-goerli.us.auth0.com/v2/logout
+NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=
Build faster with the Superchain Dev Console: Get testnet ETH and tools to help you build, launch, and grow your app on the Superchain
+NEXT_PUBLIC_MARKETPLACE_ENABLED=false
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic-sepolia'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
+NEXT_PUBLIC_NETWORK_ID=11155420
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
+NEXT_PUBLIC_NETWORK_NAME=OP Sepolia
+NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia.optimism.io
+NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Sepolia
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/
+NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
+NEXT_PUBLIC_ROLLUP_TYPE=optimistic
+NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-sepolia.k8s.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
\ No newline at end of file
diff --git a/configs/envs/.env.polygon b/configs/envs/.env.polygon
new file mode 100644
index 0000000000..4fe0631ac0
--- /dev/null
+++ b/configs/envs/.env.polygon
@@ -0,0 +1,47 @@
+# Set of ENVs for Polygon Mainnet network explorer
+# https://polygon.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=polygon"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
+NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=polygon.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1)
+NEXT_PUBLIC_MARKETPLACE_ENABLED=false
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=MATIC
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=MATIC
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/polygon_pos/pools'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg
+NEXT_PUBLIC_NETWORK_ID=137
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg
+NEXT_PUBLIC_NETWORK_NAME=Polygon Mainnet
+NEXT_PUBLIC_NETWORK_RPC_URL=https://polygon.blockpi.network/v1/rpc/public
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Polygon
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/polygon-mainnet.png
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-polygon.safe.global
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
\ No newline at end of file
diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw
new file mode 100644
index 0000000000..7840c51b42
--- /dev/null
+++ b/configs/envs/.env.pw
@@ -0,0 +1,56 @@
+# Set of ENVs for Playwright components tests
+
+# app configuration
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3100
+
+# blockchain parameters
+NEXT_PUBLIC_NETWORK_NAME=Blockscout
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout
+NEXT_PUBLIC_NETWORK_ID=1
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_RPC_URL=https://localhost:1111
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+
+# api configuration
+NEXT_PUBLIC_API_PROTOCOL=http
+NEXT_PUBLIC_API_HOST=localhost
+NEXT_PUBLIC_API_PORT=3003
+NEXT_PUBLIC_API_BASE_PATH=/
+
+# ui config
+## homepage
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
+NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
+## sidebar
+## footer
+NEXT_PUBLIC_GIT_TAG=v1.0.11
+## views
+NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'}]
+## misc
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/%23address={hash}&blockscout=eth-goerli.blockscout.com'}]
+NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=
+
+# app features
+NEXT_PUBLIC_APP_ENV=testing
+NEXT_PUBLIC_APP_INSTANCE=pw
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://localhost:3000/marketplace-suggest-ideas-form
+NEXT_PUBLIC_AD_BANNER_PROVIDER=slise
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_AUTH_URL=http://localhost:3100
+NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
+NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
+NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
+NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
+NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
diff --git a/configs/envs/.env.rootstock_testnet b/configs/envs/.env.rootstock_testnet
new file mode 100644
index 0000000000..2c3f426912
--- /dev/null
+++ b/configs/envs/.env.rootstock_testnet
@@ -0,0 +1,45 @@
+# Set of ENVs for Rootstock Testnet network explorer
+# https://rootstock-testnet.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=rootstock_testnet"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=rootstock-testnet.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-testnet.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/rootstock.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x98b25020fa6551a439dfee58fb16ca11d9e93d4cdf15f3f07b697cf08cf11643
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(255, 145, 0)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_LOGOUT_URL=https://rootstock.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_ENABLED=false
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=tRBTC
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=tRBTC
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/rsk-testnet'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/rootstock-short.svg
+NEXT_PUBLIC_NETWORK_ID=31
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/rootstock.svg
+NEXT_PUBLIC_NETWORK_NAME=Rootstock Testnet
+NEXT_PUBLIC_NETWORK_RPC_URL=https://public-node.testnet.rsk.co
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Rootstock
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/rootstock-testnet.png
+NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward','nonce']
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.stability_testnet b/configs/envs/.env.stability_testnet
new file mode 100644
index 0000000000..c057fda27c
--- /dev/null
+++ b/configs/envs/.env.stability_testnet
@@ -0,0 +1,55 @@
+# Set of ENVs for Stability Testnet network explorer
+# https://stability-testnet.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=stability_testnet"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=stability-testnet.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com/
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/stability-testnet.json
+NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x38125475465a4113a216448af2c9570d0e2c25ef313f8cfbef74f1daad7a97b5
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(46, 51, 81, 1)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(122, 235, 246, 1)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_LOGOUT_URL=https://blockscout-stability.us.auth0.com/v2/logout
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=FREE
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=FREE
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short-dark.svg
+NEXT_PUBLIC_NETWORK_ID=20180427
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability-dark.svg
+NEXT_PUBLIC_NETWORK_NAME=Stability Testnet
+NEXT_PUBLIC_NETWORK_RPC_URL=https://free.testnet.stabilityprotocol.com
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Stability Testnet
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/stability.png
+NEXT_PUBLIC_STATS_API_HOST=https://stats-stability-testnet.k8s.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
+NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts']
+NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas']
+NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','gas_fees','burnt_fees']
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/configs/envs/.env.zkevm b/configs/envs/.env.zkevm
new file mode 100644
index 0000000000..8b7dc2e566
--- /dev/null
+++ b/configs/envs/.env.zkevm
@@ -0,0 +1,48 @@
+# Set of ENVs for Polygon zkEVM network explorer
+# https://zkevm.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=zkevm"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
+NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=zkevm.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zkevm.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1)
+NEXT_PUBLIC_MARKETPLACE_ENABLED=false
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=MATIC
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=MATIC
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/polygon-zkevm/pools'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg
+NEXT_PUBLIC_NETWORK_ID=1101
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg
+NEXT_PUBLIC_NETWORK_NAME=Polygon zkEVM
+NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com
+NEXT_PUBLIC_NETWORK_SHORT_NAME=zkEVM
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
+NEXT_PUBLIC_ROLLUP_TYPE=zkEvm
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-zkevm.safe.global
+NEXT_PUBLIC_STATS_API_HOST=https://stats-polygon-zkevm.k8s-prod-1.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
\ No newline at end of file
diff --git a/configs/envs/.env.zksync b/configs/envs/.env.zksync
new file mode 100644
index 0000000000..56e104488b
--- /dev/null
+++ b/configs/envs/.env.zksync
@@ -0,0 +1,50 @@
+# Set of ENVs for ZkSync Era network explorer
+# https://zksync.blockscout.com
+# This is an auto-generated file. To update all values, run "yarn preset:sync --name=zksync"
+
+# Local ENVs
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+
+# Instance ENVs
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_HOST=zksync.blockscout.com
+NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
+NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zksync.json
+NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/zksync.json
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x79c7802ccdf3be5a49c47cc751aad351b0027e8275f6f54878eda50ee559a648
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(53, 103, 246, 1)
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1)
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/zksync/pools'}},{'title':'L2scan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/zksync.png','baseUrl':'https://zksync-era.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
+NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short.svg
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short-dark.svg
+NEXT_PUBLIC_NETWORK_ID=324
+NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync.svg
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync-dark.svg
+NEXT_PUBLIC_NETWORK_NAME=ZkSync Era
+NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.era.zksync.io
+NEXT_PUBLIC_NETWORK_SHORT_NAME=ZkSync Era
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zksync.png
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://zksync.drpc.org?ref=559183','text':'Public RPC'}]
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
+NEXT_PUBLIC_ROLLUP_TYPE=zkSync
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-zksync.safe.global
+NEXT_PUBLIC_STATS_API_HOST=https://stats-zksync-era-mainnet.k8s-prod-2.blockscout.com
+NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
+NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
diff --git a/decs.d.ts b/decs.d.ts
new file mode 100644
index 0000000000..434d031e55
--- /dev/null
+++ b/decs.d.ts
@@ -0,0 +1 @@
+declare module 'react-identicons'
diff --git a/deploy/helmfile.yaml b/deploy/helmfile.yaml
new file mode 100644
index 0000000000..bad95f0a82
--- /dev/null
+++ b/deploy/helmfile.yaml
@@ -0,0 +1,68 @@
+environments:
+ {{ .Environment.Name }}:
+---
+helmDefaults:
+ timeout: 600
+ kubeContext: k8s-dev
+ wait: true
+ recreatePods: false
+
+repositories:
+ - name: blockscout-ci-cd
+ url: https://blockscout.github.io/blockscout-ci-cd
+ - name: blockscout
+ url: https://blockscout.github.io/helm-charts
+ - name: bedag
+ url: https://bedag.github.io/helm-charts
+
+releases:
+ # Deploy review L2
+ - name: reg-secret
+ chart: bedag/raw
+ namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ labels:
+ app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ values:
+ - resources:
+ - apiVersion: v1
+ data:
+ .dockerconfigjson: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/dockerRegistryCreds
+ kind: Secret
+ metadata:
+ name: regcred
+ type: kubernetes.io/dockerconfigjson
+ - name: bs-stack
+ chart: blockscout/blockscout-stack
+ version: 1.2.*
+ namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ labels:
+ app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ values:
+ - values/review-l2/values.yaml.gotmpl
+ - global:
+ env: "review"
+ # Deploy review
+ - name: reg-secret
+ chart: bedag/raw
+ namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ labels:
+ app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ values:
+ - resources:
+ - apiVersion: v1
+ data:
+ .dockerconfigjson: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/dockerRegistryCreds
+ kind: Secret
+ metadata:
+ name: regcred
+ type: kubernetes.io/dockerconfigjson
+ - name: bs-stack
+ chart: blockscout/blockscout-stack
+ version: 1.2.*
+ namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ labels:
+ app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ values:
+ - values/review/values.yaml.gotmpl
+ - global:
+ env: "review"
\ No newline at end of file
diff --git a/deploy/scripts/collect_envs.sh b/deploy/scripts/collect_envs.sh
new file mode 100755
index 0000000000..d53dc989e4
--- /dev/null
+++ b/deploy/scripts/collect_envs.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# Check if the number of arguments provided is correct
+if [ "$#" -ne 1 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+input_file="$1"
+prefix="NEXT_PUBLIC_"
+
+# Function to make the environment variables registry file based on documentation file ENVS.md
+# It will read the input file, extract all prefixed string and use them as variables names
+# This variables will have dummy values assigned to them
+make_registry_file() {
+ output_file=".env.registry"
+
+ # Check if file already exists and empty its content if it does
+ if [ -f "$output_file" ]; then
+ > "$output_file"
+ fi
+
+ grep -oE "${prefix}[[:alnum:]_]+" "$input_file" | sort -u | while IFS= read -r var_name; do
+ echo "$var_name=__" >> "$output_file"
+ done
+}
+
+# Function to save build-time environment variables to .env file
+save_build-time_envs() {
+ output_file=".env"
+
+ # Check if file already exists and empty its content if it does or create a new one
+ if [ -f "$output_file" ]; then
+ > "$output_file"
+ else
+ touch "$output_file"
+ fi
+
+ env | grep "^${prefix}" | while IFS= read -r line; do
+ echo "$line" >> "$output_file"
+ done
+}
+
+make_registry_file
+save_build-time_envs
\ No newline at end of file
diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh
new file mode 100755
index 0000000000..747b7269c9
--- /dev/null
+++ b/deploy/scripts/download_assets.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+echo
+echo "⬇️ Downloading external assets..."
+
+# Check if the number of arguments provided is correct
+if [ "$#" -ne 1 ]; then
+ echo "🛑 Error: incorrect amount of arguments. Usage: $0 ."
+ exit 1
+fi
+
+# Define the directory to save the downloaded assets
+ASSETS_DIR="$1"
+
+# Define a list of environment variables containing URLs of external assets
+ASSETS_ENVS=(
+ "NEXT_PUBLIC_MARKETPLACE_CONFIG_URL"
+ "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
+ "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
+ "NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL"
+ "NEXT_PUBLIC_FEATURED_NETWORKS"
+ "NEXT_PUBLIC_FOOTER_LINKS"
+ "NEXT_PUBLIC_NETWORK_LOGO"
+ "NEXT_PUBLIC_NETWORK_LOGO_DARK"
+ "NEXT_PUBLIC_NETWORK_ICON"
+ "NEXT_PUBLIC_NETWORK_ICON_DARK"
+ "NEXT_PUBLIC_OG_IMAGE_URL"
+)
+
+# Create the assets directory if it doesn't exist
+mkdir -p "$ASSETS_DIR"
+
+# Function to determine the target file name based on the environment variable
+get_target_filename() {
+ local env_var="$1"
+ local url="${!env_var}"
+
+ # Extract the middle part of the variable name (between "NEXT_PUBLIC_" and "_URL") in lowercase
+ local name_prefix="${env_var#NEXT_PUBLIC_}"
+ local name_suffix="${name_prefix%_URL}"
+ local name_lc="$(echo "$name_suffix" | tr '[:upper:]' '[:lower:]')"
+
+ # Check if the URL starts with "file://"
+ if [[ "$url" == file://* ]]; then
+ # Extract the local file path
+ local file_path="${url#file://}"
+ # Get the filename from the local file path
+ local filename=$(basename "$file_path")
+ # Extract the extension from the filename
+ local extension="${filename##*.}"
+ else
+ # Remove query parameters from the URL and get the filename
+ local filename=$(basename "${url%%\?*}")
+ # Extract the extension from the filename
+ local extension="${filename##*.}"
+ fi
+
+ # Convert the extension to lowercase
+ extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
+
+ # Construct the custom file name
+ echo "$name_lc.$extension"
+}
+
+# Function to download and save an asset
+download_and_save_asset() {
+ local env_var="$1"
+ local url="$2"
+ local filename="$3"
+ local destination="$ASSETS_DIR/$filename"
+
+ # Check if the environment variable is set
+ if [ -z "${!env_var}" ]; then
+ echo " [.] $env_var: Variable is not set. Skipping download."
+ return 1
+ fi
+
+ # Check if the URL starts with "file://"
+ if [[ "$url" == file://* ]]; then
+ # Copy the local file to the destination
+ cp "${url#file://}" "$destination"
+ else
+ # Download the asset using curl
+ curl -s -o "$destination" "$url"
+ fi
+
+ # Check if the download was successful
+ if [ $? -eq 0 ]; then
+ echo " [+] $env_var: Successfully saved file from $url to $destination."
+ return 0
+ else
+ echo " [-] $env_var: Failed to save file from $url."
+ return 1
+ fi
+}
+
+# Iterate through the list and download assets
+for env_var in "${ASSETS_ENVS[@]}"; do
+ url="${!env_var}"
+ filename=$(get_target_filename "$env_var")
+ download_and_save_asset "$env_var" "$url" "$filename"
+done
+
+echo "✅ Done."
+echo
diff --git a/deploy/scripts/entrypoint.sh b/deploy/scripts/entrypoint.sh
new file mode 100755
index 0000000000..298303c7a0
--- /dev/null
+++ b/deploy/scripts/entrypoint.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+
+export_envs_from_preset() {
+ if [ -z "$ENVS_PRESET" ]; then
+ return
+ fi
+
+ if [ "$ENVS_PRESET" = "none" ]; then
+ return
+ fi
+
+ local preset_file="./configs/envs/.env.$ENVS_PRESET"
+
+ if [ ! -f "$preset_file" ]; then
+ return
+ fi
+
+ local blacklist=(
+ "NEXT_PUBLIC_APP_PROTOCOL"
+ "NEXT_PUBLIC_APP_HOST"
+ "NEXT_PUBLIC_APP_PORT"
+ "NEXT_PUBLIC_APP_ENV"
+ "NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL"
+ )
+
+ while IFS='=' read -r name value; do
+ name="${name#"${name%%[![:space:]]*}"}" # Trim leading whitespace
+ if [[ -n $name && $name == "NEXT_PUBLIC_"* && ! "${blacklist[*]}" =~ "$name" ]]; then
+ export "$name"="$value"
+ fi
+ done < <(grep "^[^#;]" "$preset_file")
+}
+
+# If there is a preset, load the environment variables from the its file
+export_envs_from_preset
+
+# Download external assets
+./download_assets.sh ./public/assets/configs
+
+# Check run-time ENVs values
+./validate_envs.sh
+if [ $? -ne 0 ]; then
+ exit 1
+fi
+
+# Generate favicons bundle
+./favicon_generator.sh
+if [ $? -ne 0 ]; then
+ echo "👎 Unable to generate favicons bundle."
+else
+ echo "👍 Favicons bundle successfully generated."
+fi
+echo
+
+# Create envs.js file with run-time environment variables for the client app
+./make_envs_script.sh
+
+# Print list of enabled features
+node ./feature-reporter.js
+
+echo "Starting Next.js application"
+exec "$@"
\ No newline at end of file
diff --git a/deploy/scripts/favicon_generator.sh b/deploy/scripts/favicon_generator.sh
new file mode 100755
index 0000000000..0cfc9f7f8a
--- /dev/null
+++ b/deploy/scripts/favicon_generator.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+master_url="${FAVICON_MASTER_URL:-$NEXT_PUBLIC_NETWORK_ICON}"
+export MASTER_URL="$master_url"
+
+cd ./deploy/tools/favicon-generator
+./script.sh
+if [ $? -ne 0 ]; then
+ cd ../../../
+ exit 1
+else
+ cd ../../../
+ favicon_folder="./public/assets/favicon/"
+
+ echo "⏳ Replacing default favicons with freshly generated pack..."
+ if [ -d "$favicon_folder" ]; then
+ rm -r "$favicon_folder"
+ fi
+ mkdir -p "$favicon_folder"
+ cp -r ./deploy/tools/favicon-generator/output/* "$favicon_folder"
+fi
\ No newline at end of file
diff --git a/deploy/scripts/make_envs_script.sh b/deploy/scripts/make_envs_script.sh
new file mode 100755
index 0000000000..0634296c45
--- /dev/null
+++ b/deploy/scripts/make_envs_script.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+echo "🌀 Creating client script with ENV values..."
+
+# Define the output file name
+output_file="${1:-./public/assets/envs.js}"
+
+touch $output_file;
+truncate -s 0 $output_file;
+
+# Check if the .env file exists and load ENVs from it
+if [ -f .env ]; then
+ source .env
+ export $(cut -d= -f1 .env)
+fi
+
+echo "window.__envs = {" >> $output_file;
+
+# Iterate through all environment variables
+for var in $(env | grep '^NEXT_PUBLIC_' | cut -d= -f1); do
+ # Get the value of the variable
+ value="${!var}"
+
+ # Replace double quotes with single quotes
+ value="${value//\"/\'}"
+
+ # Write the variable name and value to the output file
+ echo "${var}: \"${value}\"," >> "$output_file"
+done
+
+echo "}" >> $output_file;
+
+echo "✅ Done."
diff --git a/deploy/scripts/validate_envs.sh b/deploy/scripts/validate_envs.sh
new file mode 100755
index 0000000000..55211d1c59
--- /dev/null
+++ b/deploy/scripts/validate_envs.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# Check if the .env file exists
+if [ -f .env ]; then
+ # Load the environment variables from .env
+ source .env
+fi
+
+# Check run-time ENVs values integrity
+node "$(dirname "$0")/envs-validator.js" "$input"
+if [ $? != 0 ]; then
+ exit 1
+fi
\ No newline at end of file
diff --git a/deploy/tools/affected-tests/.gitignore b/deploy/tools/affected-tests/.gitignore
new file mode 100644
index 0000000000..30bc162798
--- /dev/null
+++ b/deploy/tools/affected-tests/.gitignore
@@ -0,0 +1 @@
+/node_modules
\ No newline at end of file
diff --git a/deploy/tools/affected-tests/index.js b/deploy/tools/affected-tests/index.js
new file mode 100644
index 0000000000..f2fc450eb6
--- /dev/null
+++ b/deploy/tools/affected-tests/index.js
@@ -0,0 +1,208 @@
+/* eslint-disable no-console */
+const { execSync } = require('child_process');
+const dependencyTree = require('dependency-tree');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT_DIR = path.resolve(__dirname, '../../../');
+
+const TARGET_FILE = path.resolve(ROOT_DIR, './playwright/affected-tests.txt');
+
+const NON_EXISTENT_DEPS = [];
+
+const DIRECTORIES_WITH_TESTS = [
+ path.resolve(ROOT_DIR, './ui'),
+];
+const VISITED = {};
+
+function getAllPwFilesInDirectory(directory) {
+ const files = fs.readdirSync(directory, { recursive: true });
+ return files
+ .filter((file) => file.endsWith('.pw.tsx'))
+ .map((file) => path.join(directory, file));
+}
+
+function getFileDeps(filename, changedNpmModules) {
+ return dependencyTree.toList({
+ filename,
+ directory: ROOT_DIR,
+ filter: (path) => {
+ if (path.indexOf('node_modules') === -1) {
+ return true;
+ }
+
+ if (changedNpmModules.some((module) => path.startsWith(module))) {
+ return true;
+ }
+
+ return false;
+ },
+ tsConfig: path.resolve(ROOT_DIR, './tsconfig.json'),
+ nonExistent: NON_EXISTENT_DEPS,
+ visited: VISITED,
+ });
+}
+
+async function getChangedFiles() {
+ const command = process.env.CI ?
+ `git diff --name-only origin/${ process.env.GITHUB_BASE_REF } ${ process.env.GITHUB_SHA } -- ${ ROOT_DIR }` :
+ `git diff --name-only main $(git branch --show-current) -- ${ ROOT_DIR }`;
+
+ console.log('Executing command: ', command);
+ const files = execSync(command)
+ .toString()
+ .trim()
+ .split('\n')
+ .filter(Boolean);
+
+ return files.map((file) => path.join(ROOT_DIR, file));
+}
+
+function checkChangesInChakraTheme(changedFiles) {
+ const themeDir = path.resolve(ROOT_DIR, './theme');
+ return changedFiles.some((file) => file.startsWith(themeDir));
+}
+
+function checkChangesInSvgSprite(changedFiles) {
+ const iconDir = path.resolve(ROOT_DIR, './icons');
+ const areIconsChanged = changedFiles.some((file) => file.startsWith(iconDir));
+
+ if (!areIconsChanged) {
+ return false;
+ }
+
+ const svgNamesFile = path.resolve(ROOT_DIR, './public/icons/name.d.ts');
+ const areSvgNamesChanged = changedFiles.some((file) => file === svgNamesFile);
+
+ if (!areSvgNamesChanged) {
+ // If only the icons have changed and not the names in the SVG file, we will need to run all tests.
+ // This is because we cannot correctly identify the test files that depend on these changes.
+ return true;
+ }
+
+ // If the icon names have changed, then there should be changes in the components that use them.
+ // Otherwise, typescript would complain about that.
+ return false;
+}
+
+function createTargetFile(content) {
+ fs.writeFileSync(TARGET_FILE, content);
+}
+
+function getPackageJsonUpdatedProps(packageJsonFile) {
+ const command = process.env.CI ?
+ `git diff --unified=0 origin/${ process.env.GITHUB_BASE_REF } ${ process.env.GITHUB_SHA } -- ${ packageJsonFile }` :
+ `git diff --unified=0 main $(git branch --show-current) -- ${ packageJsonFile }`;
+
+ console.log('Executing command: ', command);
+ const changedLines = execSync(command)
+ .toString()
+ .trim()
+ .split('\n')
+ .filter(Boolean)
+ .filter((line) => line.startsWith('+ ') || line.startsWith('- '));
+
+ const changedProps = [ ...new Set(
+ changedLines
+ .map((line) => line.replaceAll(' ', '').replaceAll('+', '').replaceAll('-', ''))
+ .map((line) => line.split(':')[0].replaceAll('"', '')),
+ ) ];
+
+ return changedProps;
+}
+
+function getUpdatedNpmModules(changedFiles) {
+ const packageJsonFile = path.resolve(ROOT_DIR, './package.json');
+
+ if (!changedFiles.includes(packageJsonFile)) {
+ return [];
+ }
+
+ try {
+ const packageJsonContent = JSON.parse(fs.readFileSync(packageJsonFile, 'utf-8'));
+ const usedNpmModules = [
+ ...Object.keys(packageJsonContent.dependencies || {}),
+ ...Object.keys(packageJsonContent.devDependencies || {}),
+ ];
+ const updatedProps = getPackageJsonUpdatedProps(packageJsonFile);
+
+ return updatedProps.filter((prop) => usedNpmModules.includes(prop));
+ } catch (error) {}
+}
+
+async function run() {
+ // NOTES:
+ // - The absence of TARGET_FILE implies that all tests should be run.
+ // - The empty TARGET_FILE implies that no tests should be run.
+
+ const start = Date.now();
+
+ fs.unlink(TARGET_FILE, () => {});
+
+ const changedFiles = await getChangedFiles();
+
+ if (!changedFiles.length) {
+ createTargetFile('');
+ console.log('No changed files found. Exiting...');
+ return;
+ }
+
+ console.log('Changed files in the branch: ', changedFiles);
+
+ if (checkChangesInChakraTheme(changedFiles)) {
+ console.log('Changes in Chakra theme detected. It is advisable to run all test suites. Exiting...');
+ return;
+ }
+
+ if (checkChangesInSvgSprite(changedFiles)) {
+ console.log('There are some changes in the SVG sprite that cannot be linked to a specific component. It is advisable to run all test suites. Exiting...');
+ return;
+ }
+
+ let changedNpmModules = getUpdatedNpmModules(changedFiles);
+
+ if (!changedNpmModules) {
+ console.log('Some error occurred while detecting changed NPM modules. It is advisable to run all test suites. Exiting...');
+ return;
+ }
+
+ console.log('Changed NPM modules in the branch: ', changedNpmModules);
+
+ changedNpmModules = [
+ ...changedNpmModules,
+ ...changedNpmModules.map((module) => `@types/${ module }`), // there are some deps that are resolved to .d.ts files
+ ].map((module) => path.resolve(ROOT_DIR, `./node_modules/${ module }`));
+
+ const allTestFiles = DIRECTORIES_WITH_TESTS.reduce((acc, dir) => {
+ return acc.concat(getAllPwFilesInDirectory(dir));
+ }, []);
+
+ const isDepChanged = (dep) => changedFiles.includes(dep) || changedNpmModules.some((module) => dep.startsWith(module));
+
+ const testFilesToRun = allTestFiles
+ .map((file) => ({ file, deps: getFileDeps(file, changedNpmModules) }))
+ .filter(({ deps }) => deps.some(isDepChanged));
+ const testFileNamesToRun = testFilesToRun.map(({ file }) => path.relative(ROOT_DIR, file));
+
+ if (!testFileNamesToRun.length) {
+ createTargetFile('');
+ console.log('No tests to run. Exiting...');
+ return;
+ }
+
+ createTargetFile(testFileNamesToRun.join('\n'));
+
+ const end = Date.now();
+
+ const testFilesToRunWithFilteredDeps = testFilesToRun.map(({ file, deps }) => ({
+ file,
+ deps: deps.filter(isDepChanged),
+ }));
+
+ console.log('Total time: ', ((end - start) / 1_000).toLocaleString());
+ console.log('Total test to run: ', testFileNamesToRun.length);
+ console.log('Tests to run with changed deps: ', testFilesToRunWithFilteredDeps);
+ console.log('Non existent deps: ', NON_EXISTENT_DEPS);
+}
+
+run();
diff --git a/deploy/tools/affected-tests/package.json b/deploy/tools/affected-tests/package.json
new file mode 100644
index 0000000000..bfba5734fa
--- /dev/null
+++ b/deploy/tools/affected-tests/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "affected-tests",
+ "version": "1.0.0",
+ "main": "index.js",
+ "author": "Vasilii (tom) Goriunov ",
+ "license": "MIT",
+ "dependencies": {
+ "dependency-tree": "10.0.9"
+ }
+}
diff --git a/deploy/tools/affected-tests/yarn.lock b/deploy/tools/affected-tests/yarn.lock
new file mode 100644
index 0000000000..385918d425
--- /dev/null
+++ b/deploy/tools/affected-tests/yarn.lock
@@ -0,0 +1,716 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/parser@^7.21.8":
+ version "7.23.9"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b"
+ integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==
+
+"@dependents/detective-less@^4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@dependents/detective-less/-/detective-less-4.1.0.tgz#4a979ee7a6a79eb33602862d6a1263e30f98002e"
+ integrity sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg==
+ dependencies:
+ gonzales-pe "^4.3.0"
+ node-source-walk "^6.0.1"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@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.59.5":
+ 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/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"
+
+app-module-path@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"
+ integrity sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==
+
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+ast-module-types@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-5.0.0.tgz#32b2b05c56067ff38e95df66f11d6afd6c9ba16b"
+ integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ==
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+color-name@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+commander@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+debug@^4.3.4:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+dependency-tree@10.0.9:
+ version "10.0.9"
+ resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-10.0.9.tgz#0c6c0dbeb0c5ec2cf83bf755f30e9cb12e7b4ac7"
+ integrity sha512-dwc59FRIsht+HfnTVM0BCjJaEWxdq2YAvEDy4/Hn6CwS3CBWMtFnL3aZGAkQn3XCYxk/YcTDE4jX2Q7bFTwCjA==
+ dependencies:
+ commander "^10.0.1"
+ filing-cabinet "^4.1.6"
+ precinct "^11.0.5"
+ typescript "^5.0.4"
+
+detective-amd@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/detective-amd/-/detective-amd-5.0.2.tgz#579900f301c160efe037a6377ec7e937434b2793"
+ integrity sha512-XFd/VEQ76HSpym80zxM68ieB77unNuoMwopU2TFT/ErUk5n4KvUTwW4beafAVUugrjV48l4BmmR0rh2MglBaiA==
+ dependencies:
+ ast-module-types "^5.0.0"
+ escodegen "^2.0.0"
+ get-amd-module-type "^5.0.1"
+ node-source-walk "^6.0.1"
+
+detective-cjs@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/detective-cjs/-/detective-cjs-5.0.1.tgz#836ad51c6de4863efc7c419ec243694f760ff8b2"
+ integrity sha512-6nTvAZtpomyz/2pmEmGX1sXNjaqgMplhQkskq2MLrar0ZAIkHMrDhLXkRiK2mvbu9wSWr0V5/IfiTrZqAQMrmQ==
+ dependencies:
+ ast-module-types "^5.0.0"
+ node-source-walk "^6.0.0"
+
+detective-es6@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-4.0.1.tgz#38d5d49a6d966e992ef8f2d9bffcfe861a58a88a"
+ integrity sha512-k3Z5tB4LQ8UVHkuMrFOlvb3GgFWdJ9NqAa2YLUU/jTaWJIm+JJnEh4PsMc+6dfT223Y8ACKOaC0qcj7diIhBKw==
+ dependencies:
+ node-source-walk "^6.0.1"
+
+detective-postcss@^6.1.3:
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/detective-postcss/-/detective-postcss-6.1.3.tgz#51a2d4419327ad85d0af071c7054c79fafca7e73"
+ integrity sha512-7BRVvE5pPEvk2ukUWNQ+H2XOq43xENWbH0LcdCE14mwgTBEAMoAx+Fc1rdp76SmyZ4Sp48HlV7VedUnP6GA1Tw==
+ dependencies:
+ is-url "^1.2.4"
+ postcss "^8.4.23"
+ postcss-values-parser "^6.0.2"
+
+detective-sass@^5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/detective-sass/-/detective-sass-5.0.3.tgz#63e54bc9b32f4bdbd9d5002308f9592a3d3a508f"
+ integrity sha512-YsYT2WuA8YIafp2RVF5CEfGhhyIVdPzlwQgxSjK+TUm3JoHP+Tcorbk3SfG0cNZ7D7+cYWa0ZBcvOaR0O8+LlA==
+ dependencies:
+ gonzales-pe "^4.3.0"
+ node-source-walk "^6.0.1"
+
+detective-scss@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-4.0.3.tgz#79758baa0158f72bfc4481eb7e21cc3b5f1ea6eb"
+ integrity sha512-VYI6cHcD0fLokwqqPFFtDQhhSnlFWvU614J42eY6G0s8c+MBhi9QAWycLwIOGxlmD8I/XvGSOUV1kIDhJ70ZPg==
+ dependencies:
+ gonzales-pe "^4.3.0"
+ node-source-walk "^6.0.1"
+
+detective-stylus@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detective-stylus/-/detective-stylus-4.0.0.tgz#ce97b6499becdc291de7b3c11df8c352c1eee46e"
+ integrity sha512-TfPotjhszKLgFBzBhTOxNHDsutIxx9GTWjrL5Wh7Qx/ydxKhwUrlSFeLIn+ZaHPF+h0siVBkAQSuy6CADyTxgQ==
+
+detective-typescript@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/detective-typescript/-/detective-typescript-11.1.0.tgz#2deea5364cae1f0d9d3688bc596e662b049438cc"
+ integrity sha512-Mq8egjnW2NSCkzEb/Az15/JnBI/Ryyl6Po0Y+0mABTFvOS6DAyUGRZqz1nyhu4QJmWWe0zaGs/ITIBeWkvCkGw==
+ dependencies:
+ "@typescript-eslint/typescript-estree" "^5.59.5"
+ ast-module-types "^5.0.0"
+ node-source-walk "^6.0.1"
+ typescript "^5.0.4"
+
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
+enhanced-resolve@^5.14.1:
+ version "5.15.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
+ integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
+escodegen@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
+ integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^5.2.0"
+ esutils "^2.0.2"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+eslint-visitor-keys@^3.3.0:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+esprima@^4.0.0, esprima@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+fast-glob@^3.2.9:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fastq@^1.6.0:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.0.tgz#ca5e1a90b5e68f97fc8b61330d5819b82f5fab03"
+ integrity sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==
+ dependencies:
+ reusify "^1.0.4"
+
+filing-cabinet@^4.1.6:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/filing-cabinet/-/filing-cabinet-4.1.6.tgz#8d6d12cf3a84365bbd94e1cbf07d71c113420dd2"
+ integrity sha512-C+HZbuQTER36sKzGtUhrAPAoK6+/PrrUhYDBQEh3kBRdsyEhkLbp1ML8S0+6e6gCUrUlid+XmubxJrhvL2g/Zw==
+ dependencies:
+ app-module-path "^2.2.0"
+ commander "^10.0.1"
+ enhanced-resolve "^5.14.1"
+ is-relative-path "^1.0.2"
+ module-definition "^5.0.1"
+ module-lookup-amd "^8.0.5"
+ resolve "^1.22.3"
+ resolve-dependency-path "^3.0.2"
+ sass-lookup "^5.0.1"
+ stylus-lookup "^5.0.1"
+ tsconfig-paths "^4.2.0"
+ typescript "^5.0.4"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-amd-module-type@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-5.0.1.tgz#bef38ea3674e1aa1bda9c59c8b0da598582f73f2"
+ integrity sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw==
+ dependencies:
+ ast-module-types "^5.0.0"
+ node-source-walk "^6.0.1"
+
+get-own-enumerable-property-symbols@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
+ integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
+
+glob-parent@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob@^7.2.3:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globby@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+ integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.2.9"
+ ignore "^5.2.0"
+ merge2 "^1.4.1"
+ slash "^3.0.0"
+
+gonzales-pe@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
+ integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
+ dependencies:
+ minimist "^1.2.5"
+
+graceful-fs@^4.2.4:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+hasown@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+ integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
+ dependencies:
+ function-bind "^1.1.2"
+
+ignore@^5.2.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
+ integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-core-module@^2.13.0:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
+ dependencies:
+ hasown "^2.0.0"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+ integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
+
+is-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
+ integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==
+
+is-relative-path@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-relative-path/-/is-relative-path-1.0.2.tgz#091b46a0d67c1ed0fe85f1f8cfdde006bb251d46"
+ integrity sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA==
+
+is-url-superb@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2"
+ integrity sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==
+
+is-url@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
+ integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+
+json5@^2.2.2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+merge2@^1.3.0, merge2@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.5, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+module-definition@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-5.0.1.tgz#62d1194e5d5ea6176b7dc7730f818f466aefa32f"
+ integrity sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA==
+ dependencies:
+ ast-module-types "^5.0.0"
+ node-source-walk "^6.0.1"
+
+module-lookup-amd@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.yarnpkg.com/module-lookup-amd/-/module-lookup-amd-8.0.5.tgz#aaeea41979105b49339380ca3f7d573db78c32a5"
+ integrity sha512-vc3rYLjDo5Frjox8NZpiyLXsNWJ5BWshztc/5KSOMzpg9k5cHH652YsJ7VKKmtM4SvaxuE9RkrYGhiSjH3Ehow==
+ dependencies:
+ commander "^10.0.1"
+ glob "^7.2.3"
+ requirejs "^2.3.6"
+ requirejs-config-file "^4.0.0"
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nanoid@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
+node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-6.0.2.tgz#ba81bc4bc0f6f05559b084bea10be84c3f87f211"
+ integrity sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==
+ dependencies:
+ "@babel/parser" "^7.21.8"
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+postcss-values-parser@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz#636edc5b86c953896f1bb0d7a7a6615df00fb76f"
+ integrity sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==
+ dependencies:
+ color-name "^1.1.4"
+ is-url-superb "^4.0.0"
+ quote-unquote "^1.0.0"
+
+postcss@^8.4.23:
+ version "8.4.33"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
+ integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
+ dependencies:
+ nanoid "^3.3.7"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
+precinct@^11.0.5:
+ version "11.0.5"
+ resolved "https://registry.yarnpkg.com/precinct/-/precinct-11.0.5.tgz#3e15b3486670806f18addb54b8533e23596399ff"
+ integrity sha512-oHSWLC8cL/0znFhvln26D14KfCQFFn4KOLSw6hmLhd+LQ2SKt9Ljm89but76Pc7flM9Ty1TnXyrA2u16MfRV3w==
+ dependencies:
+ "@dependents/detective-less" "^4.1.0"
+ commander "^10.0.1"
+ detective-amd "^5.0.2"
+ detective-cjs "^5.0.1"
+ detective-es6 "^4.0.1"
+ detective-postcss "^6.1.3"
+ detective-sass "^5.0.3"
+ detective-scss "^4.0.3"
+ detective-stylus "^4.0.0"
+ detective-typescript "^11.1.0"
+ module-definition "^5.0.1"
+ node-source-walk "^6.0.2"
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quote-unquote@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b"
+ integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==
+
+requirejs-config-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc"
+ integrity sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==
+ dependencies:
+ esprima "^4.0.0"
+ stringify-object "^3.2.1"
+
+requirejs@^2.3.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
+ integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
+
+resolve-dependency-path@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-3.0.2.tgz#012816717bcbe8b846835da11af9d2beb5acef50"
+ integrity sha512-Tz7zfjhLfsvR39ADOSk9us4421J/1ztVBo4rWUkF38hgHK5m0OCZ3NxFVpqHRkjctnwVa15igEUHFJp8MCS7vA==
+
+resolve@^1.22.3:
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
+ dependencies:
+ is-core-module "^2.13.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+sass-lookup@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-5.0.1.tgz#1f01d7ff21e09d8c9dcf8d05b3fca28f2f96e6ed"
+ integrity sha512-t0X5PaizPc2H4+rCwszAqHZRtr4bugo4pgiCvrBFvIX0XFxnr29g77LJcpyj9A0DcKf7gXMLcgvRjsonYI6x4g==
+ dependencies:
+ commander "^10.0.1"
+
+semver@^7.3.7:
+ version "7.5.4"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
+ integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
+ dependencies:
+ lru-cache "^6.0.0"
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+source-map-js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+stringify-object@^3.2.1:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629"
+ integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==
+ dependencies:
+ get-own-enumerable-property-symbols "^3.0.0"
+ is-obj "^1.0.1"
+ is-regexp "^1.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+stylus-lookup@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-5.0.1.tgz#3c4d116c3b1e8e1a8169c0d9cd20e608595560f4"
+ integrity sha512-tLtJEd5AGvnVy4f9UHQMw4bkJJtaAcmo54N+ovQBjDY3DuWyK9Eltxzr5+KG0q4ew6v2EHyuWWNnHeiw/Eo7rQ==
+ dependencies:
+ commander "^10.0.1"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tapable@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+ integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+tsconfig-paths@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c"
+ integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==
+ dependencies:
+ json5 "^2.2.2"
+ minimist "^1.2.6"
+ strip-bom "^3.0.0"
+
+tslib@^1.8.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tsutils@^3.21.0:
+ version "3.21.0"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+ integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+ dependencies:
+ tslib "^1.8.1"
+
+typescript@^5.0.4:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
+ integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
diff --git a/deploy/tools/envs-validator/.gitignore b/deploy/tools/envs-validator/.gitignore
new file mode 100644
index 0000000000..caba20fba5
--- /dev/null
+++ b/deploy/tools/envs-validator/.gitignore
@@ -0,0 +1,6 @@
+/node_modules
+/public
+.env
+.env.registry
+.env.secrets
+index.js
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts
new file mode 100644
index 0000000000..9770ea6399
--- /dev/null
+++ b/deploy/tools/envs-validator/index.ts
@@ -0,0 +1,137 @@
+/* eslint-disable no-console */
+import fs from 'fs';
+import path from 'path';
+import type { ValidationError } from 'yup';
+
+import { buildExternalAssetFilePath } from '../../../configs/app/utils';
+import schema from './schema';
+
+const silent = process.argv.includes('--silent');
+
+run();
+
+async function run() {
+ !silent && console.log();
+ try {
+ const appEnvs = Object.entries(process.env)
+ .filter(([ key ]) => key.startsWith('NEXT_PUBLIC_'))
+ .reduce((result, [ key, value ]) => {
+ result[key] = value || '';
+ return result;
+ }, {} as Record);
+
+ await checkPlaceholdersCongruity(appEnvs);
+ await validateEnvs(appEnvs);
+
+ } catch (error) {
+ process.exit(1);
+ }
+}
+
+async function validateEnvs(appEnvs: Record) {
+ !silent && console.log(`🌀 Validating ENV variables values...`);
+
+ try {
+ // replace ENVs with external JSON files content
+ const envsWithJsonConfig = [
+ 'NEXT_PUBLIC_FEATURED_NETWORKS',
+ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL',
+ 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL',
+ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL',
+ 'NEXT_PUBLIC_FOOTER_LINKS',
+ ];
+
+ for await (const envName of envsWithJsonConfig) {
+ if (appEnvs[envName]) {
+ appEnvs[envName] = await getExternalJsonContent(envName) || '[]';
+ }
+ }
+
+ await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false });
+ !silent && console.log('👍 All good!');
+ } catch (_error) {
+ if (typeof _error === 'object' && _error !== null && 'errors' in _error) {
+ console.log('🚨 ENVs validation failed with the following errors:');
+ (_error as ValidationError).errors.forEach((error) => {
+ console.log(' ', error);
+ });
+ } else {
+ console.log('🚨 Unexpected error occurred during validation.');
+ console.error(_error);
+ }
+
+ throw _error;
+ }
+
+ !silent && console.log();
+}
+
+async function getExternalJsonContent(envName: string): Promise {
+ return new Promise((resolve, reject) => {
+ const fileName = `./public${ buildExternalAssetFilePath(envName, 'https://foo.bar/baz.json') }`;
+
+ fs.readFile(path.resolve(__dirname, fileName), 'utf8', (err, data) => {
+ if (err) {
+ console.log(`🚨 Unable to read file: ${ fileName }`);
+ reject(err);
+ return;
+ }
+
+ resolve(data);
+ });
+ });
+}
+
+async function checkPlaceholdersCongruity(envsMap: Record) {
+ try {
+ !silent && console.log(`🌀 Checking environment variables and their placeholders congruity...`);
+
+ const runTimeEnvs = await getEnvsPlaceholders(path.resolve(__dirname, '.env.registry'));
+ const buildTimeEnvs = await getEnvsPlaceholders(path.resolve(__dirname, '.env'));
+ const envs = Object.keys(envsMap).filter((env) => !buildTimeEnvs.includes(env));
+
+ const inconsistencies: Array = [];
+ for (const env of envs) {
+ const hasPlaceholder = runTimeEnvs.includes(env);
+ if (!hasPlaceholder) {
+ inconsistencies.push(env);
+ }
+ }
+
+ if (inconsistencies.length > 0) {
+ console.log('🚸 For the following environment variables placeholders were not generated at build-time:');
+ inconsistencies.forEach((env) => {
+ console.log(` ${ env }`);
+ });
+ console.log(` They are either deprecated or running the app with them may lead to unexpected behavior.
+ Please check the documentation for more details - https://github.com/blockscout/frontend/blob/main/docs/ENVS.md
+ `);
+ throw new Error();
+ }
+
+ !silent && console.log('👍 All good!\n');
+ } catch (error) {
+ console.log('🚨 Congruity check failed.\n');
+ throw error;
+ }
+}
+
+function getEnvsPlaceholders(filePath: string): Promise> {
+ return new Promise((resolve, reject) => {
+ fs.readFile(filePath, 'utf8', (err, data) => {
+ if (err) {
+ console.log(`🚨 Unable to read placeholders file.`);
+ reject(err);
+ return;
+ }
+
+ const lines = data.split('\n');
+ const variables = lines.map(line => {
+ const variable = line.split('=')[0];
+ return variable.trim();
+ });
+
+ resolve(variables.filter(Boolean));
+ });
+ });
+}
diff --git a/deploy/tools/envs-validator/package.json b/deploy/tools/envs-validator/package.json
new file mode 100644
index 0000000000..1cc1803950
--- /dev/null
+++ b/deploy/tools/envs-validator/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "envs-validator",
+ "version": "1.0.0",
+ "main": "index.js",
+ "license": "MIT",
+ "scripts": {
+ "build": "yarn webpack-cli -c ./webpack.config.js",
+ "validate": "node ./index.js",
+ "test": "./test.sh"
+ },
+ "dependencies": {
+ "ts-loader": "^9.4.4",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4",
+ "yup": "^1.2.0"
+ },
+ "devDependencies": {
+ "dotenv-cli": "^7.2.1",
+ "tsconfig-paths-webpack-plugin": "^4.1.0"
+ }
+}
diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts
new file mode 100644
index 0000000000..a4daf66371
--- /dev/null
+++ b/deploy/tools/envs-validator/schema.ts
@@ -0,0 +1,751 @@
+/* eslint-disable max-len */
+declare module 'yup' {
+ interface StringSchema {
+ // Yup's URL validator is not perfect so we made our own
+ // https://github.com/jquense/yup/pull/1859
+ url(): never;
+ }
+}
+
+import * as yup from 'yup';
+
+import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
+import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders';
+import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
+import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract';
+import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
+import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
+import { GAS_UNITS } from '../../../types/client/gasTracker';
+import type { GasUnit } from '../../../types/client/gasTracker';
+import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
+import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
+import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation';
+import type { NavItemExternal, NavigationLinkId, NavigationLayout } from '../../../types/client/navigation';
+import { ROLLUP_TYPES } from '../../../types/client/rollup';
+import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token';
+import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation';
+import { VALIDATORS_CHAIN_TYPE } from '../../../types/client/validators';
+import type { ValidatorsChainType } from '../../../types/client/validators';
+import type { WalletType } from '../../../types/client/wallets';
+import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
+import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
+import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
+import type { ChainIndicatorId } from '../../../types/homepage';
+import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
+import { COLOR_THEME_IDS } from '../../../types/settings';
+import type { AddressViewId } from '../../../types/views/address';
+import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
+import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
+import type { BlockFieldId } from '../../../types/views/block';
+import type { NftMarketplaceItem } from '../../../types/views/nft';
+import type { TxAdditionalFieldsId, TxFieldsId } from '../../../types/views/tx';
+import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from '../../../types/views/tx';
+
+import { replaceQuotes } from '../../../configs/app/utils';
+import * as regexp from '../../../lib/regexp';
+import type { IconName } from '../../../ui/shared/IconSvg';
+
+const protocols = [ 'http', 'https' ];
+
+const urlTest: yup.TestConfig = {
+ name: 'url',
+ test: (value: unknown) => {
+ if (!value) {
+ return true;
+ }
+
+ try {
+ if (typeof value === 'string') {
+ new URL(value);
+ return true;
+ }
+ } catch (error) {}
+
+ return false;
+ },
+ message: '${path} is not a valid URL',
+ exclusive: true,
+};
+
+const marketplaceAppSchema: yup.ObjectSchema = yup
+ .object({
+ id: yup.string().required(),
+ external: yup.boolean(),
+ title: yup.string().required(),
+ logo: yup.string().test(urlTest).required(),
+ logoDarkMode: yup.string().test(urlTest),
+ shortDescription: yup.string().required(),
+ categories: yup.array().of(yup.string().required()).required(),
+ url: yup.string().test(urlTest).required(),
+ author: yup.string().required(),
+ description: yup.string().required(),
+ site: yup.string().test(urlTest),
+ twitter: yup.string().test(urlTest),
+ telegram: yup.string().test(urlTest),
+ github: yup.lazy(value =>
+ Array.isArray(value) ?
+ yup.array().of(yup.string().required().test(urlTest)) :
+ yup.string().test(urlTest),
+ ),
+ discord: yup.string().test(urlTest),
+ internalWallet: yup.boolean(),
+ priority: yup.number(),
+ });
+
+const issueSeverityDistributionSchema: yup.ObjectSchema = yup
+ .object({
+ critical: yup.number().required(),
+ gas: yup.number().required(),
+ high: yup.number().required(),
+ informational: yup.number().required(),
+ low: yup.number().required(),
+ medium: yup.number().required(),
+ });
+
+const solidityscanReportSchema: yup.ObjectSchema = yup
+ .object({
+ contractname: yup.string().required(),
+ scan_status: yup.string().required(),
+ scan_summary: yup
+ .object({
+ issue_severity_distribution: issueSeverityDistributionSchema.required(),
+ lines_analyzed_count: yup.number().required(),
+ scan_time_taken: yup.number().required(),
+ score: yup.string().required(),
+ score_v2: yup.string().required(),
+ threat_score: yup.string().required(),
+ })
+ .required(),
+ scanner_reference_url: yup.string().test(urlTest).required(),
+ });
+
+const contractDataSchema: yup.ObjectSchema = yup
+ .object({
+ address: yup.string().required(),
+ isVerified: yup.boolean().required(),
+ solidityScanReport: solidityscanReportSchema.nullable().notRequired(),
+ });
+
+const chainsDataSchema = yup.lazy((objValue) => {
+ let schema = yup.object();
+ Object.keys(objValue).forEach((key) => {
+ schema = schema.shape({
+ [key]: yup.object({
+ overallInfo: yup.object({
+ verifiedNumber: yup.number().required(),
+ totalContractsNumber: yup.number().required(),
+ solidityScanContractsNumber: yup.number().required(),
+ securityScore: yup.number().required(),
+ issueSeverityDistribution: issueSeverityDistributionSchema.required(),
+ }).required(),
+ contractsData: yup.array().of(contractDataSchema).required(),
+ }),
+ });
+ });
+ return schema;
+});
+
+const securityReportSchema: yup.ObjectSchema = yup
+ .object({
+ appName: yup.string().required(),
+ chainsData: chainsDataSchema,
+ });
+
+const marketplaceSchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_MARKETPLACE_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: yup
+ .array()
+ .json()
+ .of(marketplaceAppSchema)
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema,
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: yup
+ .array()
+ .json()
+ .of(yup.string())
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema,
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema.test(urlTest).required(),
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema.test(urlTest),
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: yup
+ .array()
+ .json()
+ .of(securityReportSchema)
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema,
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema,
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema.test(urlTest),
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema.test(urlTest),
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema,
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID: yup
+ .string()
+ .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
+ is: true,
+ then: (schema) => schema,
+ // eslint-disable-next-line max-len
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
+ }),
+ });
+
+const beaconChainSchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_HAS_BEACON_CHAIN: yup.boolean(),
+ NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL: yup
+ .string()
+ .when('NEXT_PUBLIC_HAS_BEACON_CHAIN', {
+ is: (value: boolean) => value,
+ then: (schema) => schema.min(1).optional(),
+ otherwise: (schema) => schema.max(
+ -1,
+ 'NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL cannot not be used if NEXT_PUBLIC_HAS_BEACON_CHAIN is not set to "true"',
+ ),
+ }),
+ });
+
+const rollupSchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_ROLLUP_TYPE: yup.string().oneOf(ROLLUP_TYPES),
+ NEXT_PUBLIC_ROLLUP_L1_BASE_URL: yup
+ .string()
+ .when('NEXT_PUBLIC_ROLLUP_TYPE', {
+ is: (value: string) => value,
+ then: (schema) => schema.test(urlTest).required(),
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined'),
+ }),
+ NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL: yup
+ .string()
+ .when('NEXT_PUBLIC_ROLLUP_TYPE', {
+ is: (value: string) => value === 'optimistic',
+ then: (schema) => schema.test(urlTest).required(),
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL can be used only if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' '),
+ }),
+ });
+
+const adButlerConfigSchema = yup
+ .object()
+ .transform(replaceQuotes)
+ .json()
+ .when('NEXT_PUBLIC_AD_BANNER_PROVIDER', {
+ is: (value: AdBannerProviders) => value === 'adbutler',
+ then: (schema) => schema
+ .shape({
+ id: yup.string().required(),
+ width: yup.number().positive().required(),
+ height: yup.number().positive().required(),
+ })
+ .required(),
+ })
+ .when('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER', {
+ is: (value: AdBannerProviders) => value === 'adbutler',
+ then: (schema) => schema
+ .shape({
+ id: yup.string().required(),
+ width: yup.number().positive().required(),
+ height: yup.number().positive().required(),
+ })
+ .required(),
+ });
+
+const adsBannerSchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_AD_BANNER_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_PROVIDERS),
+ NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS),
+ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema,
+ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema,
+ });
+
+const sentrySchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_SENTRY_DSN: yup.string().test(urlTest),
+ SENTRY_CSP_REPORT_URI: yup
+ .string()
+ .when('NEXT_PUBLIC_SENTRY_DSN', {
+ is: (value: string) => Boolean(value),
+ then: (schema) => schema.test(urlTest),
+ otherwise: (schema) => schema.max(-1, 'SENTRY_CSP_REPORT_URI cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
+ }),
+ NEXT_PUBLIC_SENTRY_ENABLE_TRACING: yup
+ .boolean()
+ .when('NEXT_PUBLIC_SENTRY_DSN', {
+ is: (value: string) => Boolean(value),
+ then: (schema) => schema,
+ }),
+ NEXT_PUBLIC_APP_INSTANCE: yup
+ .string()
+ .when('NEXT_PUBLIC_SENTRY_DSN', {
+ is: (value: string) => Boolean(value),
+ then: (schema) => schema,
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_APP_INSTANCE cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
+ }),
+ NEXT_PUBLIC_APP_ENV: yup
+ .string()
+ .when('NEXT_PUBLIC_SENTRY_DSN', {
+ is: (value: string) => Boolean(value),
+ then: (schema) => schema,
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_APP_ENV cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
+ }),
+ });
+
+const accountSchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(),
+ NEXT_PUBLIC_AUTH0_CLIENT_ID: yup
+ .string()
+ .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', {
+ is: (value: boolean) => value,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_AUTH0_CLIENT_ID cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'),
+ }),
+ NEXT_PUBLIC_AUTH_URL: yup
+ .string()
+ .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', {
+ is: (value: boolean) => value,
+ then: (schema) => schema.test(urlTest),
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_AUTH_URL cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'),
+ }),
+ NEXT_PUBLIC_LOGOUT_URL: yup
+ .string()
+ .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', {
+ is: (value: boolean) => value,
+ then: (schema) => schema.test(urlTest).required(),
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_LOGOUT_URL cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'),
+ }),
+ });
+
+const featuredNetworkSchema: yup.ObjectSchema = yup
+ .object()
+ .shape({
+ title: yup.string().required(),
+ url: yup.string().test(urlTest).required(),
+ group: yup.string().oneOf(NETWORK_GROUPS).required(),
+ icon: yup.string().test(urlTest),
+ isActive: yup.boolean(),
+ invertIconInDarkMode: yup.boolean(),
+ });
+
+const navItemExternalSchema: yup.ObjectSchema = yup
+ .object({
+ text: yup.string().required(),
+ url: yup.string().test(urlTest).required(),
+ });
+
+const footerLinkSchema: yup.ObjectSchema = yup
+ .object({
+ text: yup.string().required(),
+ url: yup.string().test(urlTest).required(),
+ });
+
+const footerLinkGroupSchema: yup.ObjectSchema = yup
+ .object({
+ title: yup.string().required(),
+ links: yup
+ .array()
+ .of(footerLinkSchema)
+ .required(),
+ });
+
+const networkExplorerSchema: yup.ObjectSchema = yup
+ .object({
+ title: yup.string().required(),
+ logo: yup.string().test(urlTest),
+ baseUrl: yup.string().test(urlTest).required(),
+ paths: yup
+ .object()
+ .shape({
+ tx: yup.string(),
+ address: yup.string(),
+ token: yup.string(),
+ block: yup.string(),
+ }),
+ });
+
+const contractCodeIdeSchema: yup.ObjectSchema = yup
+ .object({
+ title: yup.string().required(),
+ url: yup.string().test(urlTest).required(),
+ icon_url: yup.string().test(urlTest).required(),
+ });
+
+const nftMarketplaceSchema: yup.ObjectSchema = yup
+ .object({
+ name: yup.string().required(),
+ collection_url: yup.string().test(urlTest).required(),
+ instance_url: yup.string().test(urlTest).required(),
+ logo_url: yup.string().test(urlTest).required(),
+ });
+
+const bridgedTokenChainSchema: yup.ObjectSchema = yup
+ .object({
+ id: yup.string().required(),
+ title: yup.string().required(),
+ short_title: yup.string().required(),
+ base_url: yup.string().test(urlTest).required(),
+ });
+
+const tokenBridgeSchema: yup.ObjectSchema = yup
+ .object({
+ type: yup.string().required(),
+ title: yup.string().required(),
+ short_title: yup.string().required(),
+ });
+
+const bridgedTokensSchema = yup
+ .object()
+ .shape({
+ NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(bridgedTokenChainSchema),
+ NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(tokenBridgeSchema)
+ .when('NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', {
+ is: (value: Array) => value && value.length > 0,
+ then: (schema) => schema.required(),
+ otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES cannot not be used without NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS'),
+ }),
+ });
+
+const deFiDropdownItemSchema: yup.ObjectSchema = yup
+ .object({
+ text: yup.string().required(),
+ icon: yup.string().required(),
+ dappId: yup.string(),
+ url: yup.string().test(urlTest),
+ })
+ .test('oneOfRequired', 'NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: Either dappId or url is required', function(value) {
+ return Boolean(value.dappId) || Boolean(value.url);
+ }) as yup.ObjectSchema;
+
+const schema = yup
+ .object()
+ .noUnknown(true, (params) => {
+ return `Unknown ENV variables were provided: ${ params.unknown }`;
+ })
+ .shape({
+ // I. Build-time ENVs
+ // -----------------
+ NEXT_PUBLIC_GIT_TAG: yup.string(),
+ NEXT_PUBLIC_GIT_COMMIT_SHA: yup.string(),
+
+ // II. Run-time ENVs
+ // -----------------
+ // 1. App configuration
+ NEXT_PUBLIC_APP_HOST: yup.string().required(),
+ NEXT_PUBLIC_APP_PROTOCOL: yup.string().oneOf(protocols),
+ NEXT_PUBLIC_APP_PORT: yup.number().positive().integer(),
+
+ // 2. Blockchain parameters
+ NEXT_PUBLIC_NETWORK_NAME: yup.string().required(),
+ NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(),
+ NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(),
+ NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest),
+ NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(),
+ NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(),
+ NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
+ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(),
+ NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(),
+ NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES: yup.boolean(),
+ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string().oneOf([ 'validation', 'mining' ]),
+ NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME: yup.string(),
+ NEXT_PUBLIC_IS_TESTNET: yup.boolean(),
+
+ // 3. API configuration
+ NEXT_PUBLIC_API_PROTOCOL: yup.string().oneOf(protocols),
+ NEXT_PUBLIC_API_HOST: yup.string().required(),
+ NEXT_PUBLIC_API_PORT: yup.number().integer().positive(),
+ NEXT_PUBLIC_API_BASE_PATH: yup.string(),
+ NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL: yup.string().oneOf([ 'ws', 'wss' ]),
+
+ // 4. UI configuration
+ // a. homepage
+ NEXT_PUBLIC_HOMEPAGE_CHARTS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)),
+ NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
+ NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
+ NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
+
+ // b. sidebar
+ NEXT_PUBLIC_FEATURED_NETWORKS: yup
+ .array()
+ .json()
+ .of(featuredNetworkSchema),
+ NEXT_PUBLIC_OTHER_LINKS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(navItemExternalSchema),
+ NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(NAVIGATION_LINK_IDS)),
+ NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string()),
+ NEXT_PUBLIC_NAVIGATION_LAYOUT: yup.string().oneOf([ 'horizontal', 'vertical' ]),
+ NEXT_PUBLIC_NETWORK_LOGO: yup.string().test(urlTest),
+ NEXT_PUBLIC_NETWORK_LOGO_DARK: yup.string().test(urlTest),
+ NEXT_PUBLIC_NETWORK_ICON: yup.string().test(urlTest),
+ NEXT_PUBLIC_NETWORK_ICON_DARK: yup.string().test(urlTest),
+
+ // c. footer
+ NEXT_PUBLIC_FOOTER_LINKS: yup
+ .array()
+ .json()
+ .of(footerLinkGroupSchema),
+
+ // d. views
+ NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(BLOCK_FIELDS_IDS)),
+ NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: yup.string().oneOf(IDENTICON_TYPES),
+ NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(ADDRESS_VIEWS_IDS)),
+ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS: yup
+ .mixed()
+ .test(
+ 'shape',
+ 'Invalid schema were provided for NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS, it should be either array of method ids or "none" string literal',
+ (data) => {
+ const isNoneSchema = yup.string().oneOf([ 'none' ]);
+ const isArrayOfMethodsSchema = yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(SMART_CONTRACT_EXTRA_VERIFICATION_METHODS));
+
+ return isNoneSchema.isValidSync(data) || isArrayOfMethodsSchema.isValidSync(data);
+ }),
+ NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(TX_FIELDS_IDS)),
+ NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(TX_ADDITIONAL_FIELDS_IDS)),
+ NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(nftMarketplaceSchema),
+
+ // e. misc
+ NEXT_PUBLIC_NETWORK_EXPLORERS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(networkExplorerSchema),
+ NEXT_PUBLIC_CONTRACT_CODE_IDES: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(contractCodeIdeSchema),
+ NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: yup.boolean(),
+ NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(),
+ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
+ NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
+ NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS),
+
+ // 5. Features configuration
+ NEXT_PUBLIC_API_SPEC_URL: yup
+ .mixed()
+ .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_API_SPEC_URL, it should be either URL-string or "none" string literal', (data) => {
+ const isNoneSchema = yup.string().oneOf([ 'none' ]);
+ const isUrlStringSchema = yup.string().test(urlTest);
+
+ return isNoneSchema.isValidSync(data) || isUrlStringSchema.isValidSync(data);
+ }),
+ NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest),
+ NEXT_PUBLIC_STATS_API_BASE_PATH: yup.string(),
+ NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest),
+ NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(),
+ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
+ NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
+ NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest),
+ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest),
+ NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup
+ .mixed()
+ .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GRAPHIQL_TRANSACTION, it should be either Hex-string or "none" string literal', (data) => {
+ const isNoneSchema = yup.string().oneOf([ 'none' ]);
+ const isHashStringSchema = yup.string().matches(regexp.HEX_REGEXP);
+
+ return isNoneSchema.isValidSync(data) || isHashStringSchema.isValidSync(data);
+ }),
+ NEXT_PUBLIC_WEB3_WALLETS: yup
+ .mixed()
+ .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_WEB3_WALLETS, it should be either array or "none" string literal', (data) => {
+ const isNoneSchema = yup.string().equals([ 'none' ]);
+ const isArrayOfWalletsSchema = yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(yup.string().oneOf(SUPPORTED_WALLETS));
+
+ return isNoneSchema.isValidSync(data) || isArrayOfWalletsSchema.isValidSync(data);
+ }),
+ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: yup.boolean(),
+ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: yup.string().oneOf(TX_INTERPRETATION_PROVIDERS),
+ NEXT_PUBLIC_AD_TEXT_PROVIDER: yup.string().oneOf(SUPPORTED_AD_TEXT_PROVIDERS),
+ NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(),
+ NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
+ NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
+ NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest),
+ NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
+ NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
+ NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup
+ .mixed()
+ .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG, it should have name and url template', (data) => {
+ const isUndefined = data === undefined;
+ const valueSchema = yup.object().transform(replaceQuotes).json().shape({
+ name: yup.string().required(),
+ url_template: yup.string().required(),
+ logo: yup.string(),
+ dapp_id: yup.string(),
+ });
+
+ return isUndefined || valueSchema.isValidSync(data);
+ }),
+ NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup
+ .mixed()
+ .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG, it should have name and url template', (data) => {
+ const isUndefined = data === undefined;
+ const valueSchema = yup.object().transform(replaceQuotes).json().shape({
+ name: yup.string().required(),
+ url_template: yup.string().required(),
+ logo: yup.string(),
+ dapp_id: yup.string(),
+ });
+
+ return isUndefined || valueSchema.isValidSync(data);
+ }),
+ NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string().oneOf(VALIDATORS_CHAIN_TYPE),
+ NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string().oneOf(GAS_UNITS)),
+ NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: yup.boolean(),
+ NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: yup
+ .array()
+ .transform(replaceQuotes)
+ .json()
+ .of(deFiDropdownItemSchema),
+ NEXT_PUBLIC_FAULT_PROOF_ENABLED: yup.boolean()
+ .when('NEXT_PUBLIC_ROLLUP_TYPE', {
+ is: 'optimistic',
+ then: (schema) => schema,
+ otherwise: (schema) => schema.test(
+ 'not-exist',
+ 'NEXT_PUBLIC_FAULT_PROOF_ENABLED can only be used with NEXT_PUBLIC_ROLLUP_TYPE=optimistic',
+ value => value === undefined,
+ ),
+ }),
+ NEXT_PUBLIC_HAS_MUD_FRAMEWORK: yup.boolean()
+ .when('NEXT_PUBLIC_ROLLUP_TYPE', {
+ is: 'optimistic',
+ then: (schema) => schema,
+ otherwise: (schema) => schema.test(
+ 'not-exist',
+ 'NEXT_PUBLIC_HAS_MUD_FRAMEWORK can only be used with NEXT_PUBLIC_ROLLUP_TYPE=optimistic',
+ value => value === undefined,
+ ),
+ }),
+
+ // 6. External services envs
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
+ NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
+ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
+ NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
+ NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
+
+ // Misc
+ NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
+ })
+ .concat(accountSchema)
+ .concat(adsBannerSchema)
+ .concat(marketplaceSchema)
+ .concat(rollupSchema)
+ .concat(beaconChainSchema)
+ .concat(bridgedTokensSchema)
+ .concat(sentrySchema);
+
+export default schema;
diff --git a/deploy/tools/envs-validator/test.sh b/deploy/tools/envs-validator/test.sh
new file mode 100755
index 0000000000..46d3ea3fca
--- /dev/null
+++ b/deploy/tools/envs-validator/test.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+test_folder="./test"
+common_file="${test_folder}/.env.common"
+
+# Generate ENV registry file
+export NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
+export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0)
+../../scripts/collect_envs.sh ../../../docs/ENVS.md
+
+# Copy test assets
+mkdir -p "./public/assets/configs"
+cp -r ${test_folder}/assets ./public/
+
+# Build validator script
+yarn build
+
+validate_file() {
+ local test_file="$1"
+
+ echo
+ echo "🧿 Validating file '$test_file'..."
+
+ dotenv \
+ -e $test_file \
+ -e $common_file \
+ yarn run validate -- --silent
+
+ if [ $? -eq 0 ]; then
+ echo "👍 All good!"
+ return 0
+ else
+ echo "🛑 The file is invalid. Please fix errors and run script again."
+ echo
+ return 1
+ fi
+}
+
+test_files=($(find "$test_folder" -maxdepth 1 -type f | grep -vE '\/\.env\.common$'))
+
+for file in "${test_files[@]}"; do
+ validate_file "$file"
+ if [ $? -eq 1 ]; then
+ exit 1
+ fi
+done
diff --git a/deploy/tools/envs-validator/test/.env.adbutler b/deploy/tools/envs-validator/test/.env.adbutler
new file mode 100644
index 0000000000..7877a7740c
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.adbutler
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'123456','width':'728','height':'90'}
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'654321','width':'300','height':'100'}
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/.env.adbutler_add b/deploy/tools/envs-validator/test/.env.adbutler_add
new file mode 100644
index 0000000000..7f1968e4bb
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.adbutler_add
@@ -0,0 +1,4 @@
+NEXT_PUBLIC_AD_BANNER_PROVIDER='slise'
+NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER='adbutler'
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'123456','width':'728','height':'90'}
+NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'654321','width':'300','height':'100'}
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/.env.alt b/deploy/tools/envs-validator/test/.env.alt
new file mode 100644
index 0000000000..62183782bf
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.alt
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
+NEXT_PUBLIC_API_SPEC_URL=none
+NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base
new file mode 100644
index 0000000000..607ff38d36
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.base
@@ -0,0 +1,83 @@
+NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
+NEXT_PUBLIC_AUTH_URL=https://example.com
+NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
+NEXT_PUBLIC_LOGOUT_URL=https://example.com
+NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
+NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
+NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
+NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
+NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
+NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
+FAVICON_GENERATOR_API_KEY=xxx
+NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
+NEXT_PUBLIC_AD_TEXT_PROVIDER=coinzilla
+NEXT_PUBLIC_AD_BANNER_PROVIDER=slise
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com
+NEXT_PUBLIC_API_BASE_PATH=/
+NEXT_PUBLIC_API_SPEC_URL=https://example.com
+NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
+NEXT_PUBLIC_APP_ENV=development
+NEXT_PUBLIC_APP_PORT=3000
+NEXT_PUBLIC_APP_PROTOCOL=http
+NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://example.com'}]
+NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}]
+NEXT_PUBLIC_COLOR_THEME_DEFAULT=dim
+NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]
+NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com
+NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
+NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
+NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
+NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
+NEXT_PUBLIC_FOOTER_LINKS=https://example.com
+NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
+NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
+NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
+NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff'
+NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)'
+NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
+NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
+NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
+NEXT_PUBLIC_IS_TESTNET=true
+NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='Hello'
+NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com
+NEXT_PUBLIC_METASUITES_ENABLED=true
+NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=['eth_rpc_api','rpc_api']
+NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
+NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
+NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
+NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
+NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO
+NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true
+NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png
+NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png
+NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png
+NEXT_PUBLIC_NETWORK_LOGO_DARK=https://example.com/logo.png
+NEXT_PUBLIC_NETWORK_RPC_URL=https://example.com
+NEXT_PUBLIC_NETWORK_SHORT_NAME=Test
+NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
+NEXT_PUBLIC_OG_DESCRIPTION='Hello world!'
+NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png
+NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
+NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}]
+NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true
+NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
+NEXT_PUBLIC_STATS_API_HOST=https://example.com
+NEXT_PUBLIC_STATS_API_BASE_PATH=/
+NEXT_PUBLIC_USE_NEXT_JS_PROXY=false
+NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
+NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts']
+NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry']
+NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
+NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'NFT Marketplace','collection_url':'https://example.com/{hash}','instance_url':'https://example.com/{hash}/{id}','logo_url':'https://example.com/logo.png'}]
+NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas']
+NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','tx_fee','gas_fees','burnt_fees']
+NEXT_PUBLIC_VISUALIZE_API_HOST=https://example.com
+NEXT_PUBLIC_VISUALIZE_API_BASE_PATH=https://example.com
+NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false
+NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
+NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
+NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
+NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
+NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
diff --git a/deploy/tools/envs-validator/test/.env.beacon_chain b/deploy/tools/envs-validator/test/.env.beacon_chain
new file mode 100644
index 0000000000..f0800e9af3
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.beacon_chain
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_HAS_BEACON_CHAIN=true
+NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL=aETH
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/.env.common b/deploy/tools/envs-validator/test/.env.common
new file mode 100644
index 0000000000..5788f392d3
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.common
@@ -0,0 +1,4 @@
+NEXT_PUBLIC_API_HOST=blockscout.com
+NEXT_PUBLIC_APP_HOST=localhost
+NEXT_PUBLIC_NETWORK_ID=1
+NEXT_PUBLIC_NETWORK_NAME=Testnet
diff --git a/deploy/tools/envs-validator/test/.env.marketplace b/deploy/tools/envs-validator/test/.env.marketplace
new file mode 100644
index 0000000000..6cc6b1f839
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.marketplace
@@ -0,0 +1,12 @@
+NEXT_PUBLIC_MARKETPLACE_ENABLED=true
+NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com
+NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com
+NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com
+NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com
+NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://example.com
+NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com
+NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave
+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
+NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app
+NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=test
+NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=test
diff --git a/deploy/tools/envs-validator/test/.env.rollup b/deploy/tools/envs-validator/test/.env.rollup
new file mode 100644
index 0000000000..e7cacfb086
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.rollup
@@ -0,0 +1,4 @@
+NEXT_PUBLIC_ROLLUP_TYPE=optimistic
+NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com
+NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com
+NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/.env.sentry b/deploy/tools/envs-validator/test/.env.sentry
new file mode 100644
index 0000000000..c34c4d4f26
--- /dev/null
+++ b/deploy/tools/envs-validator/test/.env.sentry
@@ -0,0 +1,5 @@
+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
diff --git a/deploy/tools/envs-validator/test/assets/configs/featured_networks.json b/deploy/tools/envs-validator/test/assets/configs/featured_networks.json
new file mode 100644
index 0000000000..83b9088843
--- /dev/null
+++ b/deploy/tools/envs-validator/test/assets/configs/featured_networks.json
@@ -0,0 +1,22 @@
+[
+ {
+ "title": "Ethereum",
+ "url": "https://eth.blockscout.com/",
+ "group": "Mainnets",
+ "icon": "https://example.com/logo.svg"
+ },
+ {
+ "title": "Goerli",
+ "url": "https://eth-goerli.blockscout.com/",
+ "group": "Testnets",
+ "isActive": true,
+ "icon": "https://example.com/logo.svg",
+ "invertIconInDarkMode": true
+ },
+ {
+ "title": "POA Sokol",
+ "url": "https://blockscout.com/poa/sokol",
+ "group": "Other",
+ "icon": "https://example.com/logo.svg"
+ }
+]
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/assets/configs/footer_links.json b/deploy/tools/envs-validator/test/assets/configs/footer_links.json
new file mode 100644
index 0000000000..d3f7e7d437
--- /dev/null
+++ b/deploy/tools/envs-validator/test/assets/configs/footer_links.json
@@ -0,0 +1,28 @@
+[
+ {
+ "title": "Foo",
+ "links": [
+ {
+ "text": "Home",
+ "url": "https://example.com"
+ },
+ {
+ "text": "Brand",
+ "url": "https://example.com"
+ }
+ ]
+ },
+ {
+ "title": "Developers",
+ "links": [
+ {
+ "text": "Develop",
+ "url": "https://example.com"
+ },
+ {
+ "text": "Grants",
+ "url": "https://example.com"
+ }
+ ]
+ }
+ ]
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json b/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json
new file mode 100644
index 0000000000..15b31a5557
--- /dev/null
+++ b/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json
@@ -0,0 +1,5 @@
+[
+ "Swaps",
+ "Bridges",
+ "NFT"
+]
diff --git a/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json b/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json
new file mode 100644
index 0000000000..0b142b7a24
--- /dev/null
+++ b/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json
@@ -0,0 +1,25 @@
+[
+ {
+ "author": "Hop",
+ "id": "hop-exchange",
+ "title": "Hop",
+ "logo": "https://example.com/logo.svg",
+ "categories": ["Bridge"],
+ "shortDescription": "Hop is a scalable rollup-to-rollup general token bridge.",
+ "site": "https://example.com",
+ "description": "Hop is a scalable rollup-to-rollup general token bridge.",
+ "external": true,
+ "url": "https://example.com"
+ },
+ {
+ "author": "Blockscout",
+ "id": "token-approval-tracker",
+ "title": "Token Approval Tracker",
+ "logo": "https://example.com/logo.svg",
+ "categories": ["Infra & Dev tooling"],
+ "shortDescription": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.",
+ "site": "https://example.com",
+ "description": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.",
+ "url": "https://example.com"
+ }
+ ]
diff --git a/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json b/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json
new file mode 100644
index 0000000000..cf0f481ae3
--- /dev/null
+++ b/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json
@@ -0,0 +1,1073 @@
+[
+ {
+ "appName": "paraswap",
+ "doc": "https://developers.paraswap.network/smart-contracts",
+ "chainsData": {
+ "1": {
+ "overallInfo": {
+ "verifiedNumber": 4,
+ "totalContractsNumber": 4,
+ "solidityScanContractsNumber": 4,
+ "securityScore": 77.41749999999999,
+ "issueSeverityDistribution": {
+ "critical": 5,
+ "gas": 58,
+ "high": 9,
+ "informational": 27,
+ "low": 41,
+ "medium": 5
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
+ "contractname": "AugustusSwapper",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 8,
+ "high": 4,
+ "informational": 7,
+ "low": 8,
+ "medium": 1
+ },
+ "lines_analyzed_count": 180,
+ "scan_time_taken": 1,
+ "score": "3.61",
+ "score_v2": "72.22",
+ "threat_score": "73.68"
+ }
+ }
+ },
+ {
+ "address": "0x216b4b4ba9f3e719726886d34a177484278bfcae",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x216b4b4ba9f3e719726886d34a177484278bfcae",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x216b4b4ba9f3e719726886d34a177484278bfcae",
+ "contractname": "TokenTransferProxy",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x216b4b4ba9f3e719726886d34a177484278bfcae/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 1,
+ "gas": 29,
+ "high": 5,
+ "informational": 14,
+ "low": 21,
+ "medium": 3
+ },
+ "lines_analyzed_count": 553,
+ "scan_time_taken": 1,
+ "score": "3.92",
+ "score_v2": "78.48",
+ "threat_score": "78.95"
+ }
+ }
+ },
+ {
+ "address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0xa68bEA62Dc4034A689AA0F58A76681433caCa663",
+ "contractname": "AugustusRegistry",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xa68bEA62Dc4034A689AA0F58A76681433caCa663/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 3,
+ "high": 0,
+ "informational": 5,
+ "low": 4,
+ "medium": 0
+ },
+ "lines_analyzed_count": 103,
+ "scan_time_taken": 0,
+ "score": "4.22",
+ "score_v2": "84.47",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7",
+ "contractname": "FeeClaimer",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 18,
+ "high": 0,
+ "informational": 1,
+ "low": 8,
+ "medium": 1
+ },
+ "lines_analyzed_count": 149,
+ "scan_time_taken": 0,
+ "score": "3.72",
+ "score_v2": "74.50",
+ "threat_score": "94.74"
+ }
+ }
+ }
+ ]
+ },
+ "10": {
+ "overallInfo": {
+ "verifiedNumber": 3,
+ "totalContractsNumber": 4,
+ "solidityScanContractsNumber": 3,
+ "securityScore": 75.44333333333333,
+ "issueSeverityDistribution": {
+ "critical": 4,
+ "gas": 29,
+ "high": 4,
+ "informational": 20,
+ "low": 20,
+ "medium": 2
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
+ "contractname": "AugustusSwapper",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 8,
+ "high": 4,
+ "informational": 7,
+ "low": 8,
+ "medium": 1
+ },
+ "lines_analyzed_count": 180,
+ "scan_time_taken": 1,
+ "score": "3.61",
+ "score_v2": "72.22",
+ "threat_score": "73.68"
+ }
+ }
+ },
+ {
+ "address": "0x216B4B4Ba9F3e719726886d34a177484278Bfcae",
+ "isVerified": false,
+ "solidityScanReport": null
+ },
+ {
+ "address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0x6e7bE86000dF697facF4396efD2aE2C322165dC3",
+ "contractname": "AugustusRegistry",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x6e7bE86000dF697facF4396efD2aE2C322165dC3/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 3,
+ "high": 0,
+ "informational": 5,
+ "low": 4,
+ "medium": 0
+ },
+ "lines_analyzed_count": 102,
+ "scan_time_taken": 0,
+ "score": "4.22",
+ "score_v2": "84.31",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0xA7465CCD97899edcf11C56D2d26B49125674e45F",
+ "contractname": "FeeClaimer",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xA7465CCD97899edcf11C56D2d26B49125674e45F/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 18,
+ "high": 0,
+ "informational": 8,
+ "low": 8,
+ "medium": 1
+ },
+ "lines_analyzed_count": 149,
+ "scan_time_taken": 1,
+ "score": "3.49",
+ "score_v2": "69.80",
+ "threat_score": "94.74"
+ }
+ }
+ }
+ ]
+ },
+ "8453": {
+ "overallInfo": {
+ "verifiedNumber": 1,
+ "totalContractsNumber": 4,
+ "solidityScanContractsNumber": 1,
+ "securityScore": 73.33,
+ "issueSeverityDistribution": {
+ "critical": 4,
+ "gas": 8,
+ "high": 4,
+ "informational": 5,
+ "low": 8,
+ "medium": 1
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52",
+ "contract_chain": "base",
+ "contract_platform": "blockscout",
+ "contract_url": "https://base.blockscout.com/address/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52",
+ "contractname": "AugustusSwapper",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52/blockscout/base?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 8,
+ "high": 4,
+ "informational": 5,
+ "low": 8,
+ "medium": 1
+ },
+ "lines_analyzed_count": 180,
+ "scan_time_taken": 1,
+ "score": "3.67",
+ "score_v2": "73.33",
+ "threat_score": "73.68"
+ }
+ }
+ },
+ {
+ "address": "0x93aAAe79a53759cD164340E4C8766E4Db5331cD7",
+ "isVerified": false,
+ "solidityScanReport": null
+ },
+ {
+ "address": "0x7e31b336f9e8ba52ba3c4ac861b033ba90900bb3",
+ "isVerified": false,
+ "solidityScanReport": null
+ },
+ {
+ "address": "0x9aaB4B24541af30fD72784ED98D8756ac0eFb3C7",
+ "isVerified": false,
+ "solidityScanReport": null
+ }
+ ]
+ }
+ }
+ },
+ {
+ "appName": "mean-finance",
+ "doc": "https://docs.mean.finance/guides/smart-contract-registry",
+ "chainsData": {
+ "1": {
+ "overallInfo": {
+ "verifiedNumber": 4,
+ "totalContractsNumber": 6,
+ "solidityScanContractsNumber": 4,
+ "securityScore": 61.36750000000001,
+ "issueSeverityDistribution": {
+ "critical": 6,
+ "gas": 25,
+ "high": 1,
+ "informational": 10,
+ "low": 20,
+ "medium": 3
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "isVerified": false,
+ "solidityScanReport": null
+ },
+ {
+ "address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "contractname": "DCAPermissionsManager",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 2,
+ "gas": 22,
+ "high": 0,
+ "informational": 8,
+ "low": 11,
+ "medium": 3
+ },
+ "lines_analyzed_count": 314,
+ "scan_time_taken": 1,
+ "score": "3.87",
+ "score_v2": "77.39",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "isVerified": false,
+ "solidityScanReport": null
+ },
+ {
+ "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "contractname": "DCAHubPositionDescriptor",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 1,
+ "informational": 2,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 280,
+ "scan_time_taken": 1,
+ "score": "4.77",
+ "score_v2": "95.36",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "contractname": "DCAHubCompanion",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 11,
+ "scan_time_taken": 0,
+ "score": "1.82",
+ "score_v2": "36.36",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
+ "contractname": "DCAHubCompanion",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 11,
+ "scan_time_taken": 0,
+ "score": "1.82",
+ "score_v2": "36.36",
+ "threat_score": "100.00"
+ }
+ }
+ }
+ ]
+ },
+ "10": {
+ "overallInfo": {
+ "verifiedNumber": 5,
+ "totalContractsNumber": 6,
+ "solidityScanContractsNumber": 5,
+ "securityScore": 66.986,
+ "issueSeverityDistribution": {
+ "critical": 6,
+ "gas": 26,
+ "high": 1,
+ "informational": 10,
+ "low": 23,
+ "medium": 3
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "contractname": "DCAHub",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 23,
+ "scan_time_taken": 0,
+ "score": "3.48",
+ "score_v2": "69.57",
+ "threat_score": "94.44"
+ }
+ }
+ },
+ {
+ "address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "contractname": "DCAPermissionsManager",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 2,
+ "gas": 22,
+ "high": 0,
+ "informational": 8,
+ "low": 11,
+ "medium": 3
+ },
+ "lines_analyzed_count": 314,
+ "scan_time_taken": 1,
+ "score": "3.87",
+ "score_v2": "77.39",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "contractname": "DCAHubCompanion",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 16,
+ "scan_time_taken": 0,
+ "score": "2.81",
+ "score_v2": "56.25",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "contractname": "DCAHubPositionDescriptor",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 1,
+ "informational": 2,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 280,
+ "scan_time_taken": 1,
+ "score": "4.77",
+ "score_v2": "95.36",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "contract_chain": "optimism",
+ "contract_platform": "blockscout",
+ "contract_url": "https://optimism.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "contractname": "DCAHubCompanion",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/optimism?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 11,
+ "scan_time_taken": 0,
+ "score": "1.82",
+ "score_v2": "36.36",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
+ "isVerified": false,
+ "solidityScanReport": null
+ }
+ ]
+ },
+ "8453": {
+ "overallInfo": {
+ "verifiedNumber": 4,
+ "totalContractsNumber": 6,
+ "solidityScanContractsNumber": 4,
+ "securityScore": 74.88,
+ "issueSeverityDistribution": {
+ "critical": 6,
+ "gas": 25,
+ "high": 1,
+ "informational": 7,
+ "low": 20,
+ "medium": 3
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "contract_chain": "base",
+ "contract_platform": "blockscout",
+ "contract_url": "https://base.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345",
+ "contractname": "DCAHub",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/base?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 23,
+ "scan_time_taken": 0,
+ "score": "3.48",
+ "score_v2": "69.57",
+ "threat_score": "94.44"
+ }
+ }
+ },
+ {
+ "address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "contract_chain": "base",
+ "contract_platform": "blockscout",
+ "contract_url": "https://base.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923",
+ "contractname": "DCAPermissionsManager",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/base?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 2,
+ "gas": 22,
+ "high": 0,
+ "informational": 5,
+ "low": 11,
+ "medium": 3
+ },
+ "lines_analyzed_count": 314,
+ "scan_time_taken": 1,
+ "score": "3.92",
+ "score_v2": "78.34",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "contract_chain": "base",
+ "contract_platform": "blockscout",
+ "contract_url": "https://base.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE",
+ "contractname": "DCAHubCompanion",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/base?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 0,
+ "informational": 0,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 16,
+ "scan_time_taken": 0,
+ "score": "2.81",
+ "score_v2": "56.25",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b",
+ "contractname": "DCAHubPositionDescriptor",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/base?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 1,
+ "high": 1,
+ "informational": 2,
+ "low": 3,
+ "medium": 0
+ },
+ "lines_analyzed_count": 280,
+ "scan_time_taken": 1,
+ "score": "4.77",
+ "score_v2": "95.36",
+ "threat_score": "100.00"
+ }
+ }
+ },
+ {
+ "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9",
+ "isVerified": false,
+ "solidityScanReport": null
+ },
+ {
+ "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0",
+ "isVerified": false,
+ "solidityScanReport": null
+ }
+ ]
+ }
+ }
+ },
+ {
+ "appName": "cow-swap",
+ "doc": "https://docs.cow.fi/cow-protocol/reference/contracts/core#deployments",
+ "chainsData": {
+ "1": {
+ "overallInfo": {
+ "verifiedNumber": 3,
+ "totalContractsNumber": 3,
+ "solidityScanContractsNumber": 3,
+ "securityScore": 87.60000000000001,
+ "issueSeverityDistribution": {
+ "critical": 4,
+ "gas": 18,
+ "high": 0,
+ "informational": 13,
+ "low": 14,
+ "medium": 3
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
+ "contractname": "GPv2Settlement",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 16,
+ "high": 0,
+ "informational": 7,
+ "low": 5,
+ "medium": 3
+ },
+ "lines_analyzed_count": 493,
+ "scan_time_taken": 1,
+ "score": "4.57",
+ "score_v2": "91.48",
+ "threat_score": "94.74"
+ }
+ }
+ },
+ {
+ "address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
+ "contractname": "EIP173Proxy",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 0,
+ "high": 0,
+ "informational": 4,
+ "low": 5,
+ "medium": 0
+ },
+ "lines_analyzed_count": 94,
+ "scan_time_taken": 0,
+ "score": "4.26",
+ "score_v2": "85.11",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
+ "contractname": "GPv2VaultRelayer",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/eth?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 2,
+ "high": 0,
+ "informational": 2,
+ "low": 4,
+ "medium": 0
+ },
+ "lines_analyzed_count": 87,
+ "scan_time_taken": 0,
+ "score": "4.31",
+ "score_v2": "86.21",
+ "threat_score": "94.74"
+ }
+ }
+ }
+ ]
+ },
+ "100": {
+ "overallInfo": {
+ "verifiedNumber": 3,
+ "totalContractsNumber": 3,
+ "solidityScanContractsNumber": 3,
+ "securityScore": 87.60000000000001,
+ "issueSeverityDistribution": {
+ "critical": 4,
+ "gas": 18,
+ "high": 0,
+ "informational": 13,
+ "low": 14,
+ "medium": 3
+ }
+ },
+ "contractsData": [
+ {
+ "address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
+ "contract_chain": "gnosis",
+ "contract_platform": "blockscout",
+ "contract_url": "https://gnosis.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
+ "contractname": "GPv2Settlement",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/gnosis?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 16,
+ "high": 0,
+ "informational": 7,
+ "low": 5,
+ "medium": 3
+ },
+ "lines_analyzed_count": 493,
+ "scan_time_taken": 1,
+ "score": "4.57",
+ "score_v2": "91.48",
+ "threat_score": "94.74"
+ }
+ }
+ },
+ {
+ "address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE",
+ "contractname": "EIP173Proxy",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/gnosis?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 0,
+ "high": 0,
+ "informational": 4,
+ "low": 5,
+ "medium": 0
+ },
+ "lines_analyzed_count": 94,
+ "scan_time_taken": 0,
+ "score": "4.26",
+ "score_v2": "85.11",
+ "threat_score": "88.89"
+ }
+ }
+ },
+ {
+ "address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
+ "isVerified": true,
+ "solidityScanReport": {
+ "connection_id": "",
+ "contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
+ "contract_chain": "eth",
+ "contract_platform": "blockscout",
+ "contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
+ "contractname": "GPv2VaultRelayer",
+ "is_quick_scan": true,
+ "node_reference_id": null,
+ "request_type": "threat_scan",
+ "scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/gnosis?ref=blockscout",
+ "scan_status": "scan_done",
+ "scan_summary": {
+ "issue_severity_distribution": {
+ "critical": 0,
+ "gas": 2,
+ "high": 0,
+ "informational": 2,
+ "low": 4,
+ "medium": 0
+ },
+ "lines_analyzed_count": 87,
+ "scan_time_taken": 0,
+ "score": "4.31",
+ "score_v2": "86.21",
+ "threat_score": "94.74"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+]
diff --git a/deploy/tools/envs-validator/tsconfig.json b/deploy/tools/envs-validator/tsconfig.json
new file mode 100644
index 0000000000..a169faac5c
--- /dev/null
+++ b/deploy/tools/envs-validator/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "target": "es2016",
+ "module": "CommonJS",
+ "paths": {
+ "nextjs-routes": ["./nextjs/nextjs-routes.d.ts"],
+ }
+ },
+ "include": [
+ "../../../types/**/*.ts",
+ "../../../configs/app/**/*.ts",
+ "../../../global.d.ts",
+ "./index.ts",
+ "./schema.ts"
+ ],
+ "tsc-alias": {
+ "verbose": true,
+ "resolveFullPaths": true,
+ }
+}
+
\ No newline at end of file
diff --git a/deploy/tools/envs-validator/webpack.config.js b/deploy/tools/envs-validator/webpack.config.js
new file mode 100644
index 0000000000..b62c19d076
--- /dev/null
+++ b/deploy/tools/envs-validator/webpack.config.js
@@ -0,0 +1,25 @@
+const path = require('path');
+const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
+
+module.exports = {
+ mode: 'production',
+ target: 'node',
+ entry: path.resolve(__dirname) + '/index.ts',
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ use: 'ts-loader',
+ exclude: /node_modules/,
+ },
+ ],
+ },
+ resolve: {
+ extensions: [ '.tsx', '.ts', '.js' ],
+ plugins: [ new TsconfigPathsPlugin({ configFile: './tsconfig.json' }) ],
+ },
+ output: {
+ filename: 'index.js',
+ path: path.resolve(__dirname),
+ },
+};
diff --git a/deploy/tools/envs-validator/yarn.lock b/deploy/tools/envs-validator/yarn.lock
new file mode 100644
index 0000000000..6ea4de04cf
--- /dev/null
+++ b/deploy/tools/envs-validator/yarn.lock
@@ -0,0 +1,980 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@discoveryjs/json-ext@^0.5.0":
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
+ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+
+"@jridgewell/gen-mapping@^0.3.0":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+ integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+ integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+ integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.3":
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91"
+ integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@1.4.14":
+ version "1.4.14"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+ integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.18"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
+ integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
+ dependencies:
+ "@jridgewell/resolve-uri" "3.1.0"
+ "@jridgewell/sourcemap-codec" "1.4.14"
+
+"@types/eslint-scope@^3.7.3":
+ version "3.7.4"
+ resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
+ integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==
+ dependencies:
+ "@types/eslint" "*"
+ "@types/estree" "*"
+
+"@types/eslint@*":
+ version "8.44.0"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.0.tgz#55818eabb376e2272f77fbf5c96c43137c3c1e53"
+ integrity sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
+ integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+
+"@types/json-schema@*", "@types/json-schema@^7.0.8":
+ version "7.0.12"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
+ integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
+
+"@types/node@*":
+ version "20.4.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"
+ integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==
+
+"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
+ integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==
+ dependencies:
+ "@webassemblyjs/helper-numbers" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431"
+ integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==
+
+"@webassemblyjs/helper-api-error@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
+ integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
+
+"@webassemblyjs/helper-buffer@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093"
+ integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==
+
+"@webassemblyjs/helper-numbers@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5"
+ integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==
+ dependencies:
+ "@webassemblyjs/floating-point-hex-parser" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
+ integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
+
+"@webassemblyjs/helper-wasm-section@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577"
+ integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+
+"@webassemblyjs/ieee754@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a"
+ integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7"
+ integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
+ integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
+
+"@webassemblyjs/wasm-edit@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab"
+ integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/helper-wasm-section" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+ "@webassemblyjs/wasm-opt" "1.11.6"
+ "@webassemblyjs/wasm-parser" "1.11.6"
+ "@webassemblyjs/wast-printer" "1.11.6"
+
+"@webassemblyjs/wasm-gen@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268"
+ integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
+
+"@webassemblyjs/wasm-opt@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2"
+ integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+ "@webassemblyjs/wasm-parser" "1.11.6"
+
+"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1"
+ integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
+
+"@webassemblyjs/wast-printer@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20"
+ integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646"
+ integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==
+
+"@webpack-cli/info@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd"
+ integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==
+
+"@webpack-cli/serve@^2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
+ integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+acorn-import-assertions@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
+ integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
+
+acorn@^8.7.1, acorn@^8.8.2:
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
+ integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+
+ajv-keywords@^3.5.2:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+ integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.12.5:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+braces@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+browserslist@^4.14.5:
+ version "4.21.9"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635"
+ integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==
+ dependencies:
+ caniuse-lite "^1.0.30001503"
+ electron-to-chromium "^1.4.431"
+ node-releases "^2.0.12"
+ update-browserslist-db "^1.0.11"
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+caniuse-lite@^1.0.30001503:
+ version "1.0.30001517"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
+ integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==
+
+chalk@^4.1.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+chrome-trace-event@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+ integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clone-deep@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+ integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ dependencies:
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.2"
+ shallow-clone "^3.0.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colorette@^2.0.14:
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
+
+commander@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
+commander@^2.20.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+cross-spawn@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+dotenv-cli@^7.2.1:
+ version "7.2.1"
+ resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.2.1.tgz#e595afd9ebfb721df9da809a435b9aa966c92062"
+ integrity sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==
+ dependencies:
+ cross-spawn "^7.0.3"
+ dotenv "^16.0.0"
+ dotenv-expand "^10.0.0"
+ minimist "^1.2.6"
+
+dotenv-expand@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37"
+ integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==
+
+dotenv@^16.0.0:
+ version "16.3.1"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
+ integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
+
+electron-to-chromium@^1.4.431:
+ version "1.4.467"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.467.tgz#b0660bf644baff7eedea33b8c742fb53ec60e3c2"
+ integrity sha512-2qI70O+rR4poYeF2grcuS/bCps5KJh6y1jtZMDDEteyKJQrzLOEhFyXCLcHW6DTBjKjWkk26JhWoAi+Ux9A0fg==
+
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0:
+ version "5.15.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
+ integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
+envinfo@^7.7.3:
+ version "7.10.0"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13"
+ integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==
+
+es-module-lexer@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f"
+ integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+eslint-scope@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+events@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+fast-deep-equal@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fastest-levenshtein@^1.0.12:
+ version "1.0.16"
+ resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
+ integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+glob-to-regexp@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+ integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+import-local@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
+ integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==
+ dependencies:
+ pkg-dir "^4.2.0"
+ resolve-cwd "^3.0.0"
+
+interpret@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
+ integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
+
+is-core-module@^2.11.0:
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd"
+ integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
+ dependencies:
+ has "^1.0.3"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+jest-worker@^27.4.5:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+ integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+ dependencies:
+ "@types/node" "*"
+ merge-stream "^2.0.0"
+ supports-color "^8.0.0"
+
+json-parse-even-better-errors@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json5@^2.2.2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+loader-runner@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
+ integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+micromatch@^4.0.0:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.27:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+neo-async@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-releases@^2.0.12:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
+ integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+property-expr@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
+ integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
+
+punycode@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
+ integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+
+randombytes@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+rechoir@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
+ integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
+ dependencies:
+ resolve "^1.20.0"
+
+resolve-cwd@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+ integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+ dependencies:
+ resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.20.0:
+ version "1.22.2"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
+ integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+ dependencies:
+ is-core-module "^2.11.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+safe-buffer@^5.1.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+schema-utils@^3.1.1, schema-utils@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"
+ integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
+ dependencies:
+ "@types/json-schema" "^7.0.8"
+ ajv "^6.12.5"
+ ajv-keywords "^3.5.2"
+
+semver@^7.3.4:
+ version "7.5.4"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
+ integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
+ dependencies:
+ lru-cache "^6.0.0"
+
+serialize-javascript@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
+ integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
+ dependencies:
+ randombytes "^2.1.0"
+
+shallow-clone@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+ integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ dependencies:
+ kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+source-map-support@~0.5.20:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-color@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tapable@^2.1.1, tapable@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+ integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
+terser-webpack-plugin@^5.3.7:
+ version "5.3.9"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
+ integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jest-worker "^27.4.5"
+ schema-utils "^3.1.1"
+ serialize-javascript "^6.0.1"
+ terser "^5.16.8"
+
+terser@^5.16.8:
+ version "5.19.2"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e"
+ integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==
+ dependencies:
+ "@jridgewell/source-map" "^0.3.3"
+ acorn "^8.8.2"
+ commander "^2.20.0"
+ source-map-support "~0.5.20"
+
+tiny-case@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
+ integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+ integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
+
+ts-loader@^9.4.4:
+ version "9.4.4"
+ resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.4.tgz#6ceaf4d58dcc6979f84125335904920884b7cee4"
+ integrity sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==
+ dependencies:
+ chalk "^4.1.0"
+ enhanced-resolve "^5.0.0"
+ micromatch "^4.0.0"
+ semver "^7.3.4"
+
+tsconfig-paths-webpack-plugin@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763"
+ integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==
+ dependencies:
+ chalk "^4.1.0"
+ enhanced-resolve "^5.7.0"
+ tsconfig-paths "^4.1.2"
+
+tsconfig-paths@^4.1.2:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c"
+ integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==
+ dependencies:
+ json5 "^2.2.2"
+ minimist "^1.2.6"
+ strip-bom "^3.0.0"
+
+type-fest@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
+ integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+
+update-browserslist-db@^1.0.11:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
+ integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
+ dependencies:
+ escalade "^3.1.1"
+ picocolors "^1.0.0"
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+watchpack@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+ integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+ dependencies:
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.1.2"
+
+webpack-cli@^5.1.4:
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"
+ integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==
+ dependencies:
+ "@discoveryjs/json-ext" "^0.5.0"
+ "@webpack-cli/configtest" "^2.1.1"
+ "@webpack-cli/info" "^2.0.2"
+ "@webpack-cli/serve" "^2.0.5"
+ colorette "^2.0.14"
+ commander "^10.0.1"
+ cross-spawn "^7.0.3"
+ envinfo "^7.7.3"
+ fastest-levenshtein "^1.0.12"
+ import-local "^3.0.2"
+ interpret "^3.1.1"
+ rechoir "^0.8.0"
+ webpack-merge "^5.7.3"
+
+webpack-merge@^5.7.3:
+ version "5.9.0"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826"
+ integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==
+ dependencies:
+ clone-deep "^4.0.1"
+ wildcard "^2.0.0"
+
+webpack-sources@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+ integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack@^5.88.2:
+ version "5.88.2"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e"
+ integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==
+ dependencies:
+ "@types/eslint-scope" "^3.7.3"
+ "@types/estree" "^1.0.0"
+ "@webassemblyjs/ast" "^1.11.5"
+ "@webassemblyjs/wasm-edit" "^1.11.5"
+ "@webassemblyjs/wasm-parser" "^1.11.5"
+ acorn "^8.7.1"
+ acorn-import-assertions "^1.9.0"
+ browserslist "^4.14.5"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^5.15.0"
+ es-module-lexer "^1.2.1"
+ eslint-scope "5.1.1"
+ events "^3.2.0"
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.2.9"
+ json-parse-even-better-errors "^2.3.1"
+ loader-runner "^4.2.0"
+ mime-types "^2.1.27"
+ neo-async "^2.6.2"
+ schema-utils "^3.2.0"
+ tapable "^2.1.1"
+ terser-webpack-plugin "^5.3.7"
+ watchpack "^2.4.0"
+ webpack-sources "^3.2.3"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wildcard@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
+ integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yup@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-1.2.0.tgz#9e51af0c63bdfc9be0fdc6c10aa0710899d8aff6"
+ integrity sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==
+ dependencies:
+ property-expr "^2.0.5"
+ tiny-case "^1.0.3"
+ toposort "^2.0.2"
+ type-fest "^2.19.0"
diff --git a/deploy/tools/favicon-generator/.gitignore b/deploy/tools/favicon-generator/.gitignore
new file mode 100755
index 0000000000..09af2dfe51
--- /dev/null
+++ b/deploy/tools/favicon-generator/.gitignore
@@ -0,0 +1,4 @@
+/output
+config.json
+response.json
+favicon_package**
\ No newline at end of file
diff --git a/deploy/tools/favicon-generator/config.template.json b/deploy/tools/favicon-generator/config.template.json
new file mode 100755
index 0000000000..4d032e8bb9
--- /dev/null
+++ b/deploy/tools/favicon-generator/config.template.json
@@ -0,0 +1,41 @@
+{
+ "favicon_generation": {
+ "api_key": "",
+ "master_picture": {
+ "type": "url",
+ "url": ""
+ },
+ "files_location": {
+ "type": "path",
+ "path": "/favicons"
+ },
+ "favicon_design": {
+ "desktop_browser": {},
+ "ios": {
+ "picture_aspect": "no_change",
+ "assets": {
+ "ios6_and_prior_icons": false,
+ "ios7_and_later_icons": true,
+ "precomposed_icons": false,
+ "declare_only_default_icon": true
+ }
+ },
+ "safari_pinned_tab": {
+ "picture_aspect": "black_and_white",
+ "threshold": 60
+ }
+ },
+ "settings": {
+ "compression": "3",
+ "scaling_algorithm": "Mitchell",
+ "error_on_image_too_small": true,
+ "readme_file": false,
+ "html_code_file": false,
+ "use_path_as_is": false
+ },
+ "versioning": {
+ "param_name": "ver",
+ "param_value": "15Zd8"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deploy/tools/favicon-generator/script.sh b/deploy/tools/favicon-generator/script.sh
new file mode 100755
index 0000000000..69145399ed
--- /dev/null
+++ b/deploy/tools/favicon-generator/script.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+
+echo "🌀 Generating favicons bundle..."
+
+# Check if MASTER_URL is provided
+if [ -z "$MASTER_URL" ]; then
+ echo "🛑 Error: MASTER_URL variable is not provided."
+ exit 1
+fi
+
+# Check if FAVICON_GENERATOR_API_KEY is provided
+if [ -z "$FAVICON_GENERATOR_API_KEY" ]; then
+ echo "🛑 Error: FAVICON_GENERATOR_API_KEY variable is not provided."
+ exit 1
+fi
+
+# Mask the FAVICON_GENERATOR_API_KEY to display only the first 8 characters
+API_KEY_MASKED="${FAVICON_GENERATOR_API_KEY:0:8}***"
+echo "🆗 The following variables are provided:"
+echo " MASTER_URL: $MASTER_URL"
+echo " FAVICON_GENERATOR_API_KEY: $API_KEY_MASKED"
+echo
+
+# RealFaviconGenerator API endpoint URL
+API_URL="https://realfavicongenerator.net/api/favicon"
+
+# Target folder for the downloaded assets
+TARGET_FOLDER="./output"
+
+# Path to the config JSON template file
+CONFIG_TEMPLATE_FILE="config.template.json"
+
+# Path to the generated config JSON file
+CONFIG_FILE="config.json"
+
+# Replace and placeholders in the JSON template file
+API_KEY_VALUE="$FAVICON_GENERATOR_API_KEY"
+sed -e "s||$API_KEY_VALUE|" -e "s||$MASTER_URL|" "$CONFIG_TEMPLATE_FILE" > "$CONFIG_FILE"
+
+# Make the API POST request with JSON data from the config file
+echo "⏳ Making request to API..."
+API_RESPONSE=$(curl -s -X POST -H "Content-Type: application/json" -d @"$CONFIG_FILE" "$API_URL")
+
+# Create the response.json file with the API response
+echo "$API_RESPONSE" > response.json
+
+# Check if the API response is valid JSON and contains success status
+if ! jq -e '.favicon_generation_result.result.status == "success"' <<< "$API_RESPONSE" >/dev/null; then
+ echo "🛑 Error: API response does not contain the expected structure or has an error status."
+ ERROR_MESSAGE=$(echo "$API_RESPONSE" | jq -r '.favicon_generation_result.result.error_message' | tr -d '\\')
+ if [ -n "$ERROR_MESSAGE" ]; then
+ echo "🛑 $ERROR_MESSAGE"
+ fi
+ exit 1
+fi
+echo "🆗 API responded with success status."
+
+# Parse the JSON response to extract the file URL and remove backslashes
+FILE_URL=$(echo "$API_RESPONSE" | jq -r '.favicon_generation_result.favicon.package_url' | tr -d '\\')
+PREVIEW_URL=$(echo "$API_RESPONSE" | jq -r '.favicon_generation_result.preview_picture_url' | tr -d '\\')
+
+# Check if FILE_URL is empty
+if [ -z "$FILE_URL" ]; then
+ echo "🛑 File URL not found in JSON response."
+ exit 1
+fi
+
+echo "🆗 Found following file URL in the response: $FILE_URL"
+echo "🆗 Favicon preview URL: $PREVIEW_URL"
+echo
+
+# Generate a filename based on the URL
+FILE_NAME=$(basename "$FILE_URL")
+
+# Check if the target folder exists and clear its contents if it does
+if [ -d "$TARGET_FOLDER" ]; then
+ rm -r "$TARGET_FOLDER"
+fi
+mkdir -p "$TARGET_FOLDER"
+
+# Download the file
+echo "⏳ Trying to download the file..."
+curl -s -L "$FILE_URL" -o "$FILE_NAME"
+
+# Check if the download was successful
+if [ $? -eq 0 ]; then
+ echo "🆗 File downloaded successfully."
+ echo
+else
+ echo "🛑 Error: Failed to download the file."
+ exit 1
+fi
+
+# Unzip the downloaded file to the target folder
+echo "⏳ Unzipping the file..."
+unzip -q "$FILE_NAME" -d "$TARGET_FOLDER"
+
+# Check if the unzip operation was successful
+if [ $? -eq 0 ]; then
+ echo "🆗 File unzipped successfully."
+ echo
+else
+ echo "🛑 Failed to unzip the file."
+ exit 1
+fi
+
+# Clean up - remove the JSON response file and temporary JSON config file
+rm response.json "$CONFIG_FILE"
+
+echo "✅ Done."
\ No newline at end of file
diff --git a/deploy/tools/feature-reporter/.gitignore b/deploy/tools/feature-reporter/.gitignore
new file mode 100644
index 0000000000..cefc90f67f
--- /dev/null
+++ b/deploy/tools/feature-reporter/.gitignore
@@ -0,0 +1,3 @@
+/node_modules
+/build
+index.js
\ No newline at end of file
diff --git a/deploy/tools/feature-reporter/dev.sh b/deploy/tools/feature-reporter/dev.sh
new file mode 100755
index 0000000000..183698a810
--- /dev/null
+++ b/deploy/tools/feature-reporter/dev.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+rm -rf ./build
+yarn compile_config
+yarn build
+dotenv -e ../../../configs/envs/.env.main -e ../../../configs/envs/.env.secrets yarn print_report
\ No newline at end of file
diff --git a/deploy/tools/feature-reporter/entry.js b/deploy/tools/feature-reporter/entry.js
new file mode 100644
index 0000000000..01a2989035
--- /dev/null
+++ b/deploy/tools/feature-reporter/entry.js
@@ -0,0 +1,23 @@
+/* eslint-disable no-console */
+const config = require('./build/configs/app').default;
+
+run();
+
+async function run() {
+ console.log();
+ try {
+ console.log(`📋 Here is the list of the features enabled for the running instance.
+To adjust their configuration, please refer to the documentation - https://github.com/blockscout/frontend/blob/main/docs/ENVS.md#app-features
+ `);
+ Object.entries(config.features)
+ .forEach(([ , feature ]) => {
+ const mark = feature.isEnabled ? 'v' : ' ';
+ console.log(` [${ mark }] ${ feature.title }`);
+ });
+
+ } catch (error) {
+ console.log('🚨 An error occurred while generating the feature report.');
+ process.exit(1);
+ }
+ console.log();
+}
diff --git a/deploy/tools/feature-reporter/package.json b/deploy/tools/feature-reporter/package.json
new file mode 100644
index 0000000000..1368868318
--- /dev/null
+++ b/deploy/tools/feature-reporter/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "feature-reporter",
+ "version": "1.0.0",
+ "main": "index.js",
+ "license": "MIT",
+ "scripts": {
+ "compile_config": "yarn tsc -p ./tsconfig.json && yarn tsc-alias -p ./tsconfig.json",
+ "build": "yarn webpack-cli -c ./webpack.config.js",
+ "print_report": "node ./index.js",
+ "dev": "./dev.sh"
+ },
+ "dependencies": {
+ "tsc": "^2.0.4",
+ "tsc-alias": "^1.8.7",
+ "typescript": "5.1",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "devDependencies": {
+ "dotenv-cli": "^7.2.1"
+ }
+}
diff --git a/deploy/tools/feature-reporter/tsconfig.json b/deploy/tools/feature-reporter/tsconfig.json
new file mode 100644
index 0000000000..1fefb24f4c
--- /dev/null
+++ b/deploy/tools/feature-reporter/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "module": "CommonJS",
+ "outDir": "./build",
+ "paths": {
+ "nextjs-routes": ["./nextjs/nextjs-routes.d.ts"],
+ }
+ },
+ "include": [ "../../../configs/app/index.ts", "../../../global.d.ts" ],
+ "tsc-alias": {
+ "verbose": true,
+ "resolveFullPaths": true,
+ }
+}
diff --git a/deploy/tools/feature-reporter/webpack.config.js b/deploy/tools/feature-reporter/webpack.config.js
new file mode 100644
index 0000000000..41363dfeef
--- /dev/null
+++ b/deploy/tools/feature-reporter/webpack.config.js
@@ -0,0 +1,13 @@
+const path = require('path');
+module.exports = {
+ mode: 'production',
+ target: 'node',
+ entry: path.resolve(__dirname, '/entry.js'),
+ resolve: {
+ extensions: [ '.js' ],
+ },
+ output: {
+ filename: 'index.js',
+ path: path.resolve(__dirname),
+ },
+};
diff --git a/deploy/tools/feature-reporter/yarn.lock b/deploy/tools/feature-reporter/yarn.lock
new file mode 100644
index 0000000000..502e97507e
--- /dev/null
+++ b/deploy/tools/feature-reporter/yarn.lock
@@ -0,0 +1,1069 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@discoveryjs/json-ext@^0.5.0":
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
+ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+
+"@jridgewell/gen-mapping@^0.3.0":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+ integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
+"@jridgewell/set-array@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+ integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.3":
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91"
+ integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
+ integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@types/eslint-scope@^3.7.3":
+ version "3.7.4"
+ resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
+ integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==
+ dependencies:
+ "@types/eslint" "*"
+ "@types/estree" "*"
+
+"@types/eslint@*":
+ version "8.44.2"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.2.tgz#0d21c505f98a89b8dd4d37fa162b09da6089199a"
+ integrity sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
+ integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+
+"@types/json-schema@*", "@types/json-schema@^7.0.8":
+ version "7.0.12"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
+ integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
+
+"@types/node@*":
+ version "20.4.8"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85"
+ integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==
+
+"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
+ integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==
+ dependencies:
+ "@webassemblyjs/helper-numbers" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431"
+ integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==
+
+"@webassemblyjs/helper-api-error@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
+ integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
+
+"@webassemblyjs/helper-buffer@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093"
+ integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==
+
+"@webassemblyjs/helper-numbers@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5"
+ integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==
+ dependencies:
+ "@webassemblyjs/floating-point-hex-parser" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
+ integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
+
+"@webassemblyjs/helper-wasm-section@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577"
+ integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+
+"@webassemblyjs/ieee754@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a"
+ integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7"
+ integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
+ integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
+
+"@webassemblyjs/wasm-edit@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab"
+ integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/helper-wasm-section" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+ "@webassemblyjs/wasm-opt" "1.11.6"
+ "@webassemblyjs/wasm-parser" "1.11.6"
+ "@webassemblyjs/wast-printer" "1.11.6"
+
+"@webassemblyjs/wasm-gen@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268"
+ integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
+
+"@webassemblyjs/wasm-opt@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2"
+ integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+ "@webassemblyjs/wasm-parser" "1.11.6"
+
+"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1"
+ integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
+
+"@webassemblyjs/wast-printer@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20"
+ integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.6"
+ "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646"
+ integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==
+
+"@webpack-cli/info@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd"
+ integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==
+
+"@webpack-cli/serve@^2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
+ integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+acorn-import-assertions@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
+ integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
+
+acorn@^8.7.1, acorn@^8.8.2:
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
+ integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+
+ajv-keywords@^3.5.2:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+ integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.12.5:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+braces@^3.0.2, braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+browserslist@^4.14.5:
+ version "4.21.10"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
+ integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==
+ dependencies:
+ caniuse-lite "^1.0.30001517"
+ electron-to-chromium "^1.4.477"
+ node-releases "^2.0.13"
+ update-browserslist-db "^1.0.11"
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+caniuse-lite@^1.0.30001517:
+ version "1.0.30001519"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601"
+ integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==
+
+chokidar@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chrome-trace-event@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+ integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clone-deep@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+ integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ dependencies:
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.2"
+ shallow-clone "^3.0.0"
+
+colorette@^2.0.14:
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
+
+commander@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
+commander@^2.20.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^9.0.0:
+ version "9.5.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
+ integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
+
+cross-spawn@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
+dotenv-cli@^7.2.1:
+ version "7.2.1"
+ resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.2.1.tgz#e595afd9ebfb721df9da809a435b9aa966c92062"
+ integrity sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==
+ dependencies:
+ cross-spawn "^7.0.3"
+ dotenv "^16.0.0"
+ dotenv-expand "^10.0.0"
+ minimist "^1.2.6"
+
+dotenv-expand@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37"
+ integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==
+
+dotenv@^16.0.0:
+ version "16.3.1"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
+ integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
+
+electron-to-chromium@^1.4.477:
+ version "1.4.487"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz#e2ef8b15f2791bf68fa6f38f2656f1a551d360ae"
+ integrity sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg==
+
+enhanced-resolve@^5.15.0:
+ version "5.15.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
+ integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
+envinfo@^7.7.3:
+ version "7.10.0"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13"
+ integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==
+
+es-module-lexer@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f"
+ integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+eslint-scope@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+events@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+fast-deep-equal@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-glob@^3.2.9:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
+ integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fastest-levenshtein@^1.0.12:
+ version "1.0.16"
+ resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
+ integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
+
+fastq@^1.6.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
+ integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+ dependencies:
+ reusify "^1.0.4"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-to-regexp@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+ integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+globby@^11.0.4:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+ integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.2.9"
+ ignore "^5.2.0"
+ merge2 "^1.4.1"
+ slash "^3.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+ignore@^5.2.0:
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
+ integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+
+import-local@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
+ integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==
+ dependencies:
+ pkg-dir "^4.2.0"
+ resolve-cwd "^3.0.0"
+
+interpret@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
+ integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-core-module@^2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
+ integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
+ dependencies:
+ has "^1.0.3"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+jest-worker@^27.4.5:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+ integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+ dependencies:
+ "@types/node" "*"
+ merge-stream "^2.0.0"
+ supports-color "^8.0.0"
+
+json-parse-even-better-errors@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+loader-runner@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
+ integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+merge2@^1.3.0, merge2@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.27:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+mylas@^2.1.9:
+ version "2.1.13"
+ resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4"
+ integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==
+
+neo-async@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-releases@^2.0.13:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
+ integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+plimit-lit@^1.2.6:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-1.5.0.tgz#f66df8a7041de1e965c4f1c0697ab486968a92a5"
+ integrity sha512-Eb/MqCb1Iv/ok4m1FqIXqvUKPISufcjZ605hl3KM/n8GaX8zfhtgdLwZU3vKjuHGh2O9Rjog/bHTq8ofIShdng==
+ dependencies:
+ queue-lit "^1.5.0"
+
+punycode@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
+ integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+
+queue-lit@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/queue-lit/-/queue-lit-1.5.0.tgz#8197fdafda1edd615c8a0fc14c48353626e5160a"
+ integrity sha512-IslToJ4eiCEE9xwMzq3viOO5nH8sUWUCwoElrhNMozzr9IIt2qqvB4I+uHu/zJTQVqc9R5DFwok4ijNK1pU3fA==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+randombytes@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+rechoir@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
+ integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
+ dependencies:
+ resolve "^1.20.0"
+
+resolve-cwd@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+ integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+ dependencies:
+ resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.20.0:
+ version "1.22.4"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34"
+ integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==
+ dependencies:
+ is-core-module "^2.13.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+safe-buffer@^5.1.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+schema-utils@^3.1.1, schema-utils@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"
+ integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
+ dependencies:
+ "@types/json-schema" "^7.0.8"
+ ajv "^6.12.5"
+ ajv-keywords "^3.5.2"
+
+serialize-javascript@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
+ integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
+ dependencies:
+ randombytes "^2.1.0"
+
+shallow-clone@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+ integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ dependencies:
+ kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+source-map-support@~0.5.20:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+supports-color@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tapable@^2.1.1, tapable@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+ integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
+terser-webpack-plugin@^5.3.7:
+ version "5.3.9"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
+ integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jest-worker "^27.4.5"
+ schema-utils "^3.1.1"
+ serialize-javascript "^6.0.1"
+ terser "^5.16.8"
+
+terser@^5.16.8:
+ version "5.19.2"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e"
+ integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==
+ dependencies:
+ "@jridgewell/source-map" "^0.3.3"
+ acorn "^8.8.2"
+ commander "^2.20.0"
+ source-map-support "~0.5.20"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+tsc-alias@^1.8.7:
+ version "1.8.7"
+ resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.7.tgz#4f8721b031a31345fa9f1fa8d3cf209d925abb88"
+ integrity sha512-59Q/zUQa3miTf99mLbSqaW0hi1jt4WoG8Uhe5hSZJHQpSoFW9eEwvW7jlKMHXWvT+zrzy3SN9PE/YBhQ+WVydA==
+ dependencies:
+ chokidar "^3.5.3"
+ commander "^9.0.0"
+ globby "^11.0.4"
+ mylas "^2.1.9"
+ normalize-path "^3.0.0"
+ plimit-lit "^1.2.6"
+
+tsc@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/tsc/-/tsc-2.0.4.tgz#5f6499146abea5dca4420b451fa4f2f9345238f5"
+ integrity sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==
+
+typescript@5.1:
+ version "5.1.6"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
+ integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
+
+update-browserslist-db@^1.0.11:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
+ integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
+ dependencies:
+ escalade "^3.1.1"
+ picocolors "^1.0.0"
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+watchpack@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+ integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+ dependencies:
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.1.2"
+
+webpack-cli@^5.1.4:
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"
+ integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==
+ dependencies:
+ "@discoveryjs/json-ext" "^0.5.0"
+ "@webpack-cli/configtest" "^2.1.1"
+ "@webpack-cli/info" "^2.0.2"
+ "@webpack-cli/serve" "^2.0.5"
+ colorette "^2.0.14"
+ commander "^10.0.1"
+ cross-spawn "^7.0.3"
+ envinfo "^7.7.3"
+ fastest-levenshtein "^1.0.12"
+ import-local "^3.0.2"
+ interpret "^3.1.1"
+ rechoir "^0.8.0"
+ webpack-merge "^5.7.3"
+
+webpack-merge@^5.7.3:
+ version "5.9.0"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826"
+ integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==
+ dependencies:
+ clone-deep "^4.0.1"
+ wildcard "^2.0.0"
+
+webpack-sources@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+ integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack@^5.88.2:
+ version "5.88.2"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e"
+ integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==
+ dependencies:
+ "@types/eslint-scope" "^3.7.3"
+ "@types/estree" "^1.0.0"
+ "@webassemblyjs/ast" "^1.11.5"
+ "@webassemblyjs/wasm-edit" "^1.11.5"
+ "@webassemblyjs/wasm-parser" "^1.11.5"
+ acorn "^8.7.1"
+ acorn-import-assertions "^1.9.0"
+ browserslist "^4.14.5"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^5.15.0"
+ es-module-lexer "^1.2.1"
+ eslint-scope "5.1.1"
+ events "^3.2.0"
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.2.9"
+ json-parse-even-better-errors "^2.3.1"
+ loader-runner "^4.2.0"
+ mime-types "^2.1.27"
+ neo-async "^2.6.2"
+ schema-utils "^3.2.0"
+ tapable "^2.1.1"
+ terser-webpack-plugin "^5.3.7"
+ watchpack "^2.4.0"
+ webpack-sources "^3.2.3"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wildcard@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
+ integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl
new file mode 100644
index 0000000000..a30300e8a1
--- /dev/null
+++ b/deploy/values/review-l2/values.yaml.gotmpl
@@ -0,0 +1,83 @@
+fullNameOverride: bs-stack
+nameOverride: bs-stack
+imagePullSecrets:
+ - name: regcred
+config:
+ network:
+ id: 420
+ name: "Base"
+ shortname: Base
+ currency:
+ name: Ether
+ symbol: ETH
+ decimals: 18
+ account:
+ enabled: true
+ testnet: true
+blockscout:
+ enabled: false
+stats:
+ enabled: false
+frontend:
+ enabled: true
+ replicaCount: 1
+ image:
+ tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ pullPolicy: Always
+ ingress:
+ enabled: true
+ annotations:
+ kubernetes.io/ingress.class: internal-and-public
+ nginx.ingress.kubernetes.io/proxy-body-size: 500m
+ nginx.ingress.kubernetes.io/client-max-body-size: "500M"
+ nginx.ingress.kubernetes.io/proxy-buffering: "on"
+ nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "15m"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "15m"
+ nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
+ nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
+ cert-manager.io/cluster-issuer: "zerossl-prod"
+ hostname: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
+
+ resources:
+ limits:
+ memory: 768Mi
+ cpu: "1"
+ requests:
+ memory: 384Mi
+ cpu: 250m
+ env:
+ NEXT_PUBLIC_APP_ENV: development
+ NEXT_PUBLIC_APP_INSTANCE: review_L2
+ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
+ NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
+ NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
+ NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json
+ NEXT_PUBLIC_API_HOST: base.blockscout.com
+ NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
+ NEXT_PUBLIC_STATS_API_HOST: https://stats-l2-base-mainnet.k8s-prod-1.blockscout.com
+ NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
+ NEXT_PUBLIC_NETWORK_RPC_URL: https://mainnet.base.org
+ NEXT_PUBLIC_WEB3_WALLETS: "['coinbase']"
+ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: true
+ NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs']"
+ NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer.services.blockscout.com
+ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info.services.blockscout.com
+ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs.services.blockscout.com
+ NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens.services.blockscout.com
+ NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata.services.blockscout.com
+ NEXT_PUBLIC_ROLLUP_TYPE: optimistic
+ NEXT_PUBLIC_ROLLUP_L1_BASE_URL: https://eth.blockscout.com
+ NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
+ NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
+ NEXT_PUBLIC_NAVIGATION_LAYOUT: horizontal
+ NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/blocks','/name-domains']"
+ envFromSecret:
+ NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
+ SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
+ NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
+ NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
+ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
+ FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
+ NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl
new file mode 100644
index 0000000000..c37f4ee6c6
--- /dev/null
+++ b/deploy/values/review/values.yaml.gotmpl
@@ -0,0 +1,99 @@
+fullNameOverride: bs-stack
+nameOverride: bs-stack
+imagePullSecrets:
+ - name: regcred
+config:
+ network:
+ id: 11155111
+ name: Blockscout
+ shortname: Blockscout
+ currency:
+ name: Ether
+ symbol: ETH
+ decimals: 18
+ account:
+ enabled: true
+ testnet: true
+blockscout:
+ enabled: false
+stats:
+ enabled: false
+frontend:
+ enabled: true
+ replicaCount: 1
+ image:
+ tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}
+ pullPolicy: Always
+ ingress:
+ enabled: true
+ annotations:
+ kubernetes.io/ingress.class: internal-and-public
+ nginx.ingress.kubernetes.io/proxy-body-size: 500m
+ nginx.ingress.kubernetes.io/client-max-body-size: "500M"
+ nginx.ingress.kubernetes.io/proxy-buffering: "on"
+ nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "15m"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "15m"
+ nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
+ nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
+ cert-manager.io/cluster-issuer: "zerossl-prod"
+ hostname: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com
+
+ resources:
+ limits:
+ memory: 768Mi
+ cpu: "1"
+ requests:
+ memory: 384Mi
+ cpu: 250m
+ env:
+ NEXT_PUBLIC_APP_ENV: development
+ NEXT_PUBLIC_APP_INSTANCE: review
+ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
+ NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-sepolia.json
+ NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
+ NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
+ NEXT_PUBLIC_API_HOST: eth-sepolia.k8s-dev.blockscout.com
+ NEXT_PUBLIC_STATS_API_HOST: https://stats-sepolia.k8s-dev.blockscout.com/
+ NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
+ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
+ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
+ NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
+ NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata-test.k8s-dev.blockscout.com
+ NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
+ NEXT_PUBLIC_MARKETPLACE_ENABLED: true
+ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
+ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
+ NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json
+ NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
+ NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
+ NEXT_PUBLIC_NETWORK_RPC_URL: https://eth-sepolia.public.blastapi.io
+ 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','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
+ NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
+ NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: zkbob-wallet
+ NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
+ NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: https://www.basename.app
+ NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
+ NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
+ NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar
+ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: true
+ NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
+ NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
+ NEXT_PUBLIC_HAS_USER_OPS: true
+ NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
+ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: blockscout
+ NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true
+ NEXT_PUBLIC_AD_BANNER_PROVIDER: slise
+ NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
+ NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']"
+ envFromSecret:
+ NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
+ SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
+ NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
+ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
+ NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
+ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
+ FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
+ NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
+ NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
+ NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: ref+vault://deployment-values/blockscout/dev/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY
diff --git a/docs/BUILD-TIME_ENVS.md b/docs/BUILD-TIME_ENVS.md
new file mode 100644
index 0000000000..7f0db07fc2
--- /dev/null
+++ b/docs/BUILD-TIME_ENVS.md
@@ -0,0 +1,9 @@
+# Build-time environment variables
+
+These variables are passed to the app during the image build process. They cannot be re-defined during the run-time.
+
+| Variable | Type | Description | Optional | Example value |
+| --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_GIT_COMMIT_SHA | `string` | SHA of the latest commit in the branch from which image is built | false | `29d0613e` |
+| NEXT_PUBLIC_GIT_TAG | `string` | Git tag on the latest commit in the branch from which image is built | true | `v1.0.0` |
+| NEXT_OPEN_TELEMETRY_ENABLED | `boolean` | Enables OpenTelemetry SDK | true | `true` |
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000000..0284e6e3cf
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,207 @@
+# Contribution guide
+
+Thanks for showing interest to contribute to Blockscout. The following steps will get you up and running.
+
+
+
+## Project setup
+
+1. Fork the repo by clicking Fork button at the top of the repo main page and name it appropriately
+
+2. Clone your fork locally
+ ```sh
+ git clone https://github.com//.git
+ cd
+ ```
+
+3. Make sure you're running Node.js 20+ and NPM 10+; if not, upgrade it accordingly, for example using [nvm](https://github.com/nvm-sh/nvm).
+ ```sh
+ node -v
+ npm -v
+ ```
+
+4. Install dependencies
+ ```sh
+ yarn
+ ```
+
+
+
+## Toolkit
+
+We are using following technology stack in the project
+- [Yarn](https://yarnpkg.com/) as package manager
+- [ReactJS](https://reactjs.org/) as UI library
+- [Next.js](https://nextjs.org/) as application framework
+- [Chakra](https://chakra-ui.com/) as component library; our theme customization can be found in `/theme` folder
+- [TanStack Query](https://tanstack.com/query/v4/docs/react/overview/) for fetching, caching and updating data from the API
+- [Jest](https://jestjs.io/) as JavaScript testing framework
+- [Playwright](https://playwright.dev/) as a tool for components visual testing
+
+And of course our premier language is [Typescript](https://www.typescriptlang.org/).
+
+
+
+## Local development
+
+To develop locally, follow one of the two paths outlined below:
+
+A. Custom configuration:
+
+1. Create `.env.local` file in the root folder and include all required environment variables from the [list](./ENVS.md)
+2. Optionally, clone `.env.example` and name it `.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need.
+3. Use `yarn dev` command to start the dev server.
+4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`).
+
+B. Pre-defined configuration:
+
+1. Optionally, clone `.env.example` file into `configs/envs/.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need.
+2. Choose one of the predefined configurations located in the `/configs/envs` folder.
+3. Start your local dev server using the `yarn dev:preset ` command.
+4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`).
+
+
+
+
+## Adding new dependencies
+For all types of dependencies:
+- **Do not add** a dependency if the desired functionality is easily implementable
+- If adding a dependency is necessary, please be sure that is is well-maintained and trustworthy
+
+
+
+## Adding new ENV variable
+
+*Note*, if the variable should be exposed to the browser don't forget to add prefix `NEXT_PUBLIC_` to its name.
+
+These are the steps that you have to follow to make everything work:
+1. First and foremost, document variable in the [/docs/ENVS.md](./ENVS.md) file; provide short description, its expected type, requirement flag, default and example value; **do not skip this step** otherwise the app will not receive variable value at run-time
+2. Make sure that you have added a property to React app config (`configs/app/index.ts`) in appropriate section that is associated with this variable; do not use ENV variable values directly in the application code; decide where this variable belongs to and place it under the certain section:
+ - `app` - the front-end app itself
+ - `api` - the main API configuration
+ - `chain` - the Blockchain parameters
+ - `UI` - the app UI customization
+ - `meta` - SEO and meta-tags customization
+ - `features` - the particular feature of the app
+ - `services` - some 3rd party service integration which is not related to one particular feature
+3. If a new variable is meant to store the URL of an external API service, remember to include its value in the Content-Security-Policy document header. Refer to `nextjs/csp/policies/app.ts` for details.
+4. For local development purposes add the variable with its appropriate values to pre-defined ENV configs `configs/envs` where it is needed
+5. Add the variable to CI configs where it is needed
+ - `deploy/values/review/values.yaml.gotmpl` - review development environment
+ - `deploy/values/review-l2/values.yaml.gotmpl` - review development environment for L2 networks
+6. If your variable is meant to receive a link to some external resource (image or JSON-config file), extend the array `ASSETS_ENVS` in `deploy/scripts/download_assets.sh` with your variable name
+7. Add validation schema for the new variable into the file `deploy/tools/envs-validator/schema.ts`
+8. Check if modified validation schema is valid by doing the following steps:
+ - change your current directory to `deploy/tools/envs-validator`
+ - install deps with `yarn` command
+ - add your variable into `./test/.env.base` test preset or create a new test preset if needed
+ - if your variable contains a link to the external JSON config file:
+ - add example of file content into `./test/assets` directory; the file name should be constructed by stripping away prefix `NEXT_PUBLIC_` and postfix `_URL` if any, and converting the remaining string to lowercase (for example, `NEXT_PUBLIC_MARKETPLACE_CONFIG_URL` will become `marketplace_config.json`)
+ - in the main script `index.ts` extend array `envsWithJsonConfig` with your variable name
+ - run `yarn test` command to see the validation result
+9. Don't forget to mention in the PR notes that new ENV variable was added
+
+
+
+## Writing & Running Tests
+
+Every feature or bugfix should be accompanied by tests, either unit tests or component visual tests, or both, except from trivial fixes (for example, typo fix). All commands for running tests you can find [below](./CONTRIBUTING.md#command-list).
+
+### Jest unit tests
+
+If your changes only related to the logic of the app and not to its visual presentation, then try to write unit tests using [Jest](https://jestjs.io/) framework and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). In general these tests are "cheaper" and faster than Playwright ones. Use them for testing your utilities and React hooks, as well as the whole components logic.
+
+Place your test suites in `.test.ts` or `.test.tsx` files. You can find or add some mocks or other helpful utilities for these tests purposes in the `/jest` folder.
+
+*Note*, that we are using custom renderer and wrapper in all test for React components, so please do not import package `@testing-library/react` directly in your test suites, instead use imports from `jest/lib` utility.
+
+### Playwright components tests
+
+For changes associated with the UI itself write components visual tests using [Playwright](https://playwright.dev/) framework and its *experimental* [Components test library](https://playwright.dev/docs/test-components). Please be aware of known [issues and limitations](https://playwright.dev/docs/test-components#known-issues-and-limitations) of this library.
+
+Your tests files should have `.pw.tsx` extension. All configs, mocks, fixtures and other utilities for these tests live in `/playwright` folder.
+
+We have 3 pre-configured projects. You can run your test with the desired project by simply adding its [tag](https://playwright.dev/docs/test-annotations#tag-tests) to the test name:
+- `default` - default project for all test, uses desktop Chrome desktop device; don't need to specify its tag, instead use `-@default` tag to skip test run with this project
+- `mobile` - project for testing on mobile devices, uses Safari mobile browser; add tag `+@mobile` to run test with this project
+- `dark-color-mode` - project for testing app in the dark color mode, uses desktop Chrome desktop device with forced dark color mode; add tag `+@dark-mode` to run test with this project.
+
+*Note* that, since we are developing not on the same operating system as our CI system, we have to use Docker to generate or update the screenshots. In order to do that use `yarn test:pw:docker --update-snapshots` command. Please **do not commit** any screenshots generated via `yarn test:pw:local` command, their associated tests will fail in the CI run.
+
+
+
+## Making a Pull Request
+
+### Steps to PR
+
+1. Make sure that you fork and clone repo; check if the main branch has all recent changes from the original repo
+
+ > Tip: Keep your `main` branch pointing at the original repository and make pull
+ > requests from branches on your fork. To do this, run:
+ >
+ > ```
+ > git remote add upstream https://github.com/blockscout/frontend.git
+ > git fetch upstream
+ > git branch --set-upstream-to=upstream/main main
+ > ```
+ >
+ > This will add the original repository as a "remote" called "upstream," Then
+ > fetch the git information from that remote, then set your local `main` branch
+ > to use the upstream main branch whenever you run `git pull`. Then you can make
+ > all of your pull request branches based on this `main` branch. Whenever you
+ > want to update your version of `main`, do a regular `git pull`.
+
+2. Create a branch for your PR with `git checkout -b `; we do not follow any branch name convention just yet
+3. Commit your changes. Commits should be one logical change that still allows all tests to pass. Prefer smaller commits if there could be two levels of logic grouping. The goal is to allow contributors in the future (including your future self) to determine your reasoning for making changes and to allow them to cherry-pick, patch or port those changes in isolation to other branches or forks. Again, there is no strict commit message convention, but make sure that it clear and fully describes all changes that were made
+4. If during your PR you reveal a pre-existing bug, try to isolate the bug and fix it on an independent branch and PR it first
+5. Where possible, please provide unit tests that demonstrate new functionality or bug fix is working
+
+### Opening PR and getting it accepted
+
+1. Push your changes and create a Pull Request. If you are still working on the task, please use "Draft Pull Request" option, so we know that it is not ready yet. In addition, you can add label "skip checks" to your PR, so all CI checks will not be triggered.
+2. Once you finish your work, remove label "skip checks" from PR, if it was added before, and publish PR if it was in the draft state
+3. Make sure that all code checks and tests are successfully passed
+4. Add description to your Pull Request and link an existing issue(s) that it is fixing
+5. Request review from one or all core team members: @tom2drum, @isstuev. Our core team is committed to reviewing patches in a timely manner.
+6. After code review is done, we merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface.
+
+*Note*, if you Pull Request contains any changes that are not backwards compatible with the previous versions of the app, please specify them in PR description and add label ["breaking changes"](https://github.com/blockscout/frontend/labels/breaking%20changes) to it.
+
+
+
+## Commands list
+
+| Command | Description |
+| --- | --- |
+| **Running and building** |
+| `yarn dev` | run local dev server with user's configuration |
+| `yarn dev:preset ` | run local dev server with predefined configuration |
+| `yarn build:docker` | build a docker image locally |
+| `yarn start:docker:local` | start an application from previously built local docker image with user's configuration |
+| `yarn start:docker:preset ` | start an application from previously built local docker image with predefined configuration |
+| **Linting and formatting** |
+| `yarn lint:eslint` | lint project files with ESLint |
+| `yarn lint:eslint:fix` | lint project files with ESLint and automatically fix problems |
+| `yarn lint:tsc` | compile project typescript files using TypeScript Compiler |
+| `yarn svg:format` | format and optimize SVG icons in the `/icons` folder using SVGO tool |
+| `yarn svg:build-sprite` | build SVG icons sprite |
+| **Testing** |
+| `yarn test:jest` | run all Jest unit tests |
+| `yarn test:jest:watch` | run all Jest unit tests in watch mode |
+| `yarn test:pw:local` | run Playwright component tests locally |
+| `yarn test:pw:docker` | run Playwright component tests in docker container |
+| `yarn test:pw:ci` | run Playwright component tests in CI |
+
+
+
+## Tips & Tricks
+
+### Code Editor
+
+#### VSCode
+
+There are some predefined tasks for all commands described above. You can see its full list by pressing cmd + shift + P and using command `Task: Run task`
+
+Also there is a Jest test launch configuration for debugging and running current test file in the watch mode.
+
+And you may find the Dev Container setup useful too.
diff --git a/docs/CUSTOM_BUILD.md b/docs/CUSTOM_BUILD.md
new file mode 100644
index 0000000000..9142e12846
--- /dev/null
+++ b/docs/CUSTOM_BUILD.md
@@ -0,0 +1,10 @@
+# Building and running your own docker image
+
+You are free to clone the repo and make any changes to the application code that you want, adding your own customization and features. After that you can build a docker image by running `yarn build:docker` or alternatively run `docker build` and pass your own args that is necessary.
+
+For running app container from freshly built image do
+```sh
+docker run -p 3000:3000 --env-file
+```
+
+*Disclaimer* Do not try to generate production build of the app on your local machine (outside the docker). The app will not work as you would expect.
diff --git a/docs/DEPRECATED_ENVS.md b/docs/DEPRECATED_ENVS.md
new file mode 100644
index 0000000000..0cf27ae106
--- /dev/null
+++ b/docs/DEPRECATED_ENVS.md
@@ -0,0 +1,12 @@
+# Deprecated environment variables
+
+| Variable | Type | Description | Compulsoriness | Default value | Example value | Introduced in version | Deprecated in version | Comment |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY | `string` | RealFaviconGenerator [API key](https://realfavicongenerator.net/api/) | Required | - | `` | v1.11.0 | v1.16.0 | Replaced FAVICON_GENERATOR_API_KEY |
+| NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK | `boolean` | Set to true for optimistic L2 solutions | Required | - | `true` | v1.17.0 | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE |
+| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.17.0 | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE |
+| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.17.0 | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL |
+| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | - | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL |
+| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | - | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED |
+| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
+| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
diff --git a/docs/ENVS.md b/docs/ENVS.md
new file mode 100644
index 0000000000..830252673c
--- /dev/null
+++ b/docs/ENVS.md
@@ -0,0 +1,745 @@
+# Run-time environment variables
+
+The app instance can be customized by passing the following variables to the Node.js environment at runtime. Some of these variables have been deprecated, and their full list can be found in the [file](./DEPRECATED_ENVS.md).
+
+**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted. If it contains a hash (`#`) or a dollar-sign (`$`) the whole value should be wrapped in single quotes as well (see `dotenv` [readme](https://github.com/bkeepers/dotenv#variable-substitution) for the reference)
+
+## Disclaimer about using variables
+
+Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will be exposed to the browser. So any user can obtain its values. Make sure that for all 3rd-party services keys (e.g., Sentri, Auth0, WalletConnect, etc.) in the services administration panel you have created a whitelist of allowed origins and have added your app domain into it. That will help you prevent using your key by unauthorized app, if someone gets its value.
+
+
+
+## Table of contents
+- [App configuration](ENVS.md#app-configuration)
+- [Blockchain parameters](ENVS.md#blockchain-parameters)
+- [API configuration](ENVS.md#api-configuration)
+- [UI configuration](ENVS.md#ui-configuration)
+ - [Homepage](ENVS.md#homepage)
+ - [Navigation](ENVS.md#navigation)
+ - [Footer](ENVS.md#footer)
+ - [Favicon](ENVS.md#favicon)
+ - [Meta](ENVS.md#meta)
+ - [Views](ENVS.md#views)
+ - [Block](ENVS.md#block-views)
+ - [Address](ENVS.md#address-views)
+ - [Transaction](ENVS.md#transaction-views)
+ - [NFT](ENVS.md#nft-views)
+ - [Misc](ENVS.md#misc)
+- [App features](ENVS.md#app-features)
+ - [My account](ENVS.md#my-account)
+ - [Gas tracker](ENVS.md#gas-tracker)
+ - [Address verification](ENVS.md#address-verification-in-my-account) in "My account"
+ - [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.)
+ - [Banner ads](ENVS.md#banner-ads)
+ - [Text ads](ENVS.md#text-ads)
+ - [Beacon chain](ENVS.md#beacon-chain)
+ - [User operations](ENVS.md#user-operations-erc-4337)
+ - [Rollup chain](ENVS.md#rollup-chain)
+ - [Export data to CSV file](ENVS.md#export-data-to-csv-file)
+ - [Google analytics](ENVS.md#google-analytics)
+ - [Mixpanel analytics](ENVS.md#mixpanel-analytics)
+ - [GrowthBook feature flagging and A/B testing](ENVS.md#growthbook-feature-flagging-and-ab-testing)
+ - [GraphQL API documentation](ENVS.md#graphql-api-documentation)
+ - [REST API documentation](ENVS.md#rest-api-documentation)
+ - [Marketplace](ENVS.md#marketplace)
+ - [Solidity to UML diagrams](ENVS.md#solidity-to-uml-diagrams)
+ - [Blockchain statistics](ENVS.md#blockchain-statistics)
+ - [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet)
+ - [Transaction interpretation](ENVS.md#transaction-interpretation)
+ - [Verified tokens info](ENVS.md#verified-tokens-info)
+ - [Name service integration](ENVS.md#name-service-integration)
+ - [Metadata service integration](ENVS.md#metadata-service-integration)
+ - [Public tag submission](ENVS.md#public-tag-submission)
+ - [Data availability](ENVS.md#data-availability)
+ - [Bridged tokens](ENVS.md#bridged-tokens)
+ - [Safe{Core} address tags](ENVS.md#safecore-address-tags)
+ - [SUAVE chain](ENVS.md#suave-chain)
+ - [MetaSuites extension](ENVS.md#metasuites-extension)
+ - [Validators list](ENVS.md#validators-list)
+ - [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
+ - [OpenTelemetry](ENVS.md#opentelemetry)
+ - [Swap button](ENVS.md#defi-dropdown)
+ - [Multichain balance button](ENVS.md#multichain-balance-button)
+ - [Get gas button](ENVS.md#get-gas-button)
+- [3rd party services configuration](ENVS.md#external-services-configuration)
+
+
+
+## App configuration
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` | App url schema | - | `https` | `http` | v1.0.x+ |
+| NEXT_PUBLIC_APP_HOST | `string` | App host | Required | - | `blockscout.com` | v1.0.x+ |
+| NEXT_PUBLIC_APP_PORT | `number` | Port where app is running | - | `3000` | `3001` | v1.0.x+ |
+| NEXT_PUBLIC_USE_NEXT_JS_PROXY | `boolean` | Tells the app to proxy all APIs request through the NextJS app. **We strongly advise not to use it in the production environment**, since it can lead to performance issues of the NodeJS server | - | `false` | `true` | v1.8.0+ |
+
+
+
+## Blockchain parameters
+
+*Note!* The `NEXT_PUBLIC_NETWORK_CURRENCY` variables represent the blockchain's native token used for paying transaction fees. `NEXT_PUBLIC_NETWORK_SECONDARY_COIN` variables refer to tokens like protocol-specific tokens (e.g., OP token on Optimism chain) or governance tokens (e.g., GNO on Gnosis chain).
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` | Used for SEO attributes (e.g, page description) | - | - | `OoG` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference | - | - | `https://core.poa.network` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | v1.23.0+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | v1.29.0+ |
+| NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES | `boolean` | Set to `true` for networks where users can pay transaction fees in either the native coin or ERC-20 tokens. | - | `false` | `true` | v1.33.0+ |
+| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME | `string` | Name of the standard for creating tokens | - | `ERC` | `BEP` | v1.31.0+ |
+| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | v1.0.x+ |
+
+
+
+## API configuration
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` | v1.0.x+ |
+| NEXT_PUBLIC_API_HOST | `string` | Main API host | Required | - | `blockscout.com` | v1.0.x+ |
+| NEXT_PUBLIC_API_PORT | `number` | Port where API is running on the host | - | - | `3001` | v1.0.x+ |
+| NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` | v1.0.x+ |
+| NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` | v1.0.x+ |
+
+
+
+## UI configuration
+
+### Homepage
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ |
+| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | v1.0.x+ |
+| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ |
+| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ |
+
+
+
+### Navigation
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_NETWORK_LOGO | `string` | Network logo; if not provided, placeholder will be shown; *Note* the logo height should be 24px and width less than 120px | - | - | `https://placekitten.com/240/40` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_LOGO_DARK | `string` | Network logo for dark color mode; if not provided, **inverted** regular logo will be used instead | - | - | `https://placekitten.com/240/40` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_ICON | `string` | Network icon; used as a replacement for regular network logo when nav bar is collapsed; if not provided, placeholder will be shown; *Note* the icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_ICON_DARK | `string` | Network icon for dark color mode; if not provided, **inverted** regular icon will be used instead | - | - | `https://placekitten.com/60/60` | v1.0.x+ |
+| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` | v1.0.x+ |
+| NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | v1.0.x+ |
+| NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` | v1.16.0+ |
+| NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES | `Array` | List of menu item routes that should have a lightning label | - | - | `['/accounts']` | v1.31.0+ |
+| NEXT_PUBLIC_NAVIGATION_LAYOUT | `vertical \| horizontal` | Navigation menu layout type | - | `vertical` | `horizontal` | v1.32.0+ |
+
+#### Featured network configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| title | `string` | Displayed name of the network | Required | - | `Gnosis Chain` |
+| url | `string` | Network explorer main page url | Required | - | `https://blockscout.com/xdai/mainnet` |
+| group | `Mainnets \| Testnets \| Other` | Indicates in which tab network appears in the menu | Required | - | `Mainnets` |
+| icon | `string` | Network icon; if not provided, the common placeholder will be shown; *Note* that icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` |
+| isActive | `boolean` | Pass `true` if item should be shown as active in the menu | - | - | `true` |
+| invertIconInDarkMode | `boolean` | Pass `true` if icon colors should be inverted in dark mode | - | - | `true` |
+
+
+
+### Footer
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_FOOTER_LINKS | `string` | URL of configuration file (`.json` format only) which contains list of link groups to be displayed in the footer. See [below](#footer-links-configuration-properties) list of available properties for particular group | - | - | `https://example.com/footer_links_config.json` | v1.1.1+ |
+
+The app version shown in the footer is derived from build-time ENV variables `NEXT_PUBLIC_GIT_TAG` and `NEXT_PUBLIC_GIT_COMMIT_SHA` and cannot be overwritten at run-time.
+
+#### Footer links configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| title | `string` | Title of link group | Required | - | `Company` |
+| links | `Array<{'text':string;'url':string;}>` | list of links | Required | - | `[{'text':'Homepage','url':'https://www.blockscout.com'}]` |
+
+
+
+### Favicon
+
+By default, the app has generic favicon. You can override this behavior by providing the following variables. Hence, the favicon assets bundle will be generated at the container start time and will be used instead of default one.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| FAVICON_GENERATOR_API_KEY | `string` | RealFaviconGenerator [API key](https://realfavicongenerator.net/api/) | Required | - | `` | v1.16.0+ |
+| FAVICON_MASTER_URL | `string` | - | - | `NEXT_PUBLIC_NETWORK_ICON` | `https://placekitten.com/180/180` | v1.11.0+ |
+
+
+
+### Meta
+
+Settings for meta tags, OG tags and SEO
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE | `boolean` | Set to `true` to promote Blockscout in meta and OG titles | - | `true` | `true` | v1.12.0+ |
+| NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` | v1.12.0+ |
+| NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` | v1.12.0+ |
+| NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` | v1.29.0+ |
+| NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to pre-render page titles (e.g Token page) on the server side and inject page h1-tag to the markup before it is sent to the browser. | - | `false` | `true` | v1.30.0+ |
+
+
+
+### Views
+
+#### Block views
+
+| Variable | Type | Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS | `Array` | Array of the block fields ids that should be hidden. See below the list of the possible id values. | - | - | `'["burnt_fees","total_reward"]'` | v1.10.0+ |
+
+
+##### Block fields list
+| Id | Description |
+| --- | --- |
+| `burnt_fees` | Burnt fees |
+| `total_reward` | Total block reward |
+| `nonce` | Block nonce |
+| `miner` | Address of block's miner or validator |
+| `L1_status` | Short interpretation of the batch lifecycle (applicable for Rollup chains) |
+| `batch` | Batch index (applicable for Rollup chains) |
+
+
+
+#### Address views
+
+| Variable | Type | Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Default style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | v1.12.0+ |
+| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | v1.15.0+ |
+| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | v1.19.0+ |
+| NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS | `Array<'solidity-hardhat' \| 'solidity-foundry'>` | Pass an array of additional methods from which users can choose while verifying a smart contract. Both methods are available by default, pass `'none'` string to disable them all. | - | - | `['solidity-hardhat']` | v1.33.0+ |
+
+##### Address views list
+| Id | Description |
+| --- | --- |
+| `top_accounts` | Top accounts |
+
+
+
+#### Transaction views
+
+| Variable | Type | Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS | `Array` | Array of the transaction fields ids that should be hidden. See below the list of the possible id values. | - | - | `'["value","tx_fee"]'` | v1.15.0+ |
+| NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS | `Array` | Array of the additional fields ids that should be added to the transaction details. See below the list of the possible id values. | - | - | `'["fee_per_gas"]'` | v1.15.0+ |
+
+##### Transaction fields list
+| Id | Description |
+| --- | --- |
+| `value` | Sent value |
+| `fee_currency` | Fee currency |
+| `gas_price` | Price per unit of gas |
+| `tx_fee` | Total transaction fee |
+| `gas_fees` | Gas fees breakdown |
+| `burnt_fees` | Amount of native coin burnt for transaction |
+| `L1_status` | Short interpretation of the batch lifecycle (applicable for Rollup chains) |
+| `batch` | Batch index (applicable for Rollup chains) |
+
+##### Transaction additional fields list
+| Id | Description |
+| --- | --- |
+| `fee_per_gas` | Amount of total fee divided by total amount of gas used by transaction |
+
+
+
+#### NFT views
+
+| Variable | Type | Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES | `Array` where `NftMarketplace` can have following [properties](#nft-marketplace-properties) | Used to build up links to NFT collections and NFT instances in external marketplaces. | - | - | `[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'}]` | v1.15.0+ |
+
+
+##### NFT marketplace properties
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| name | `string` | Displayed name of the marketplace | Required | - | `OpenSea` |
+| collection_url | `string` | URL template for NFT collection | Required | - | `https://opensea.io/assets/ethereum/{hash}` |
+| instance_url | `string` | URL template for NFT instance | Required | - | `https://opensea.io/assets/ethereum/{hash}/{id}` |
+| logo_url | `string` | URL of marketplace logo | Required | - | `https://opensea.io/static/images/logos/opensea-logo.svg` |
+
+*Note* URL templates should contain placeholders of NFT hash (`{hash}`) and NFT id (`{id}`). This placeholders will be substituted with particular values for every collection or instance.
+
+
+
+### Misc
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | v1.0.x+ |
+| NEXT_PUBLIC_CONTRACT_CODE_IDES | `Array` where `ContractCodeIde` can have following [properties](#contract-code-ide-configuration-properties) | Used to build up links to IDEs with contract source code. | - | - | `[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]` | v1.23.0+ |
+| NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS | `boolean` | Set to `true` to enable Submit Audit form on the contract page | - | `false` | `true` | v1.25.0+ |
+| NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` | v1.17.0+ |
+| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ |
+| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ |
+| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ |
+
+#### Network explorer configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| logo | `string` | URL to explorer logo file. Should be at least 40x40. | - | - | `'https://foo.app/icon.png'` |
+| title | `string` | Displayed name of the explorer | Required | - | `Anyblock` |
+| baseUrl | `string` | Base url of the explorer | Required | - | `https://explorer.anyblock.tools` |
+| paths | `Record<'tx' \| 'block' \| 'address' \| 'token', string>` | Map of explorer entities and their paths | Required | - | `{'tx':'/ethereum/poa/core/tx'}` |
+
+*Note* The url of an entity will be constructed as `]>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/`
+
+#### Contract code IDE configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| title | `string` | Displayed name of the IDE | Required | - | `Remix IDE` |
+| url | `string` | URL of the IDE with placeholders for contract hash (`{hash}`) and current domain (`{domain}`) | Required | - | `https://remix.blockscout.com/?address={hash}&blockscout={domain}` |
+| icon_url | `string` | URL of the IDE icon | Required | - | `https://example.com/icon.svg` |
+
+
+
+## App features
+
+*Note* The variables which are marked as required should be passed as described in order to enable the particular feature, but they are not required in the whole app context.
+
+### My account
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ |
+| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | Required | - | `` | v1.0.x+ |
+| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | Required | - | `https://blockscout.com` | v1.0.x+ |
+| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | Required | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ |
+
+
+
+### Gas tracker
+
+This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_TRACKER_ENABLED=false`.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_GAS_TRACKER_ENABLED | `boolean` | Set to true to enable "Gas tracker" in the app | Required | `true` | `false` | v1.25.0+ |
+| NEXT_PUBLIC_GAS_TRACKER_UNITS | Array<`usd` \| `gwei`> | Array of units for displaying gas prices on the Gas Tracker page, in the stats snippet on the Home page, and in the top bar. The first value in the array will take priority over the second one in all mentioned views. If only one value is provided, gas prices will be displayed only in that unit. | - | `[ 'usd', 'gwei' ]` | `[ 'gwei' ]` | v1.25.0+ |
+
+
+
+### Address verification in "My account"
+
+*Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones:
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | Required | - | `https://contracts-info.services.blockscout.com` | v1.1.0+ |
+| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ |
+
+
+
+### Blockchain interaction (writing to contract, etc.)
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://cloud.walletconnect.com/) integration | Required | - | `` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Gnosis Chain` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_ID | `number` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `99` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Ether` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `ETH` | v1.0.x+ |
+| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | - | `18` | `6` | v1.0.x+ |
+
+
+
+### Banner ads
+
+This feature is **enabled by default** with the `slise` ads provider. To switch it off pass `NEXT_PUBLIC_AD_BANNER_PROVIDER=none`.
+*Note* that the `getit` ad provider is temporary disabled.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `hype` \| `getit` \| `none` | Ads provider | - | `slise` | `coinzilla` | v1.0.x+ |
+| NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER | `adbutler` | Additional ads provider to mix with the main one | - | - | `adbutler` | v1.28.0+ |
+| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` | v1.3.0+ |
+| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` | v1.3.0+ |
+
+
+
+### Text ads
+
+This feature is **enabled by default** with the `coinzilla` ads provider. To switch it off pass `NEXT_PUBLIC_AD_TEXT_PROVIDER=none`.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_AD_TEXT_PROVIDER | `coinzilla` \| `none` | Ads provider | - | `coinzilla` | `none` | v1.0.x+ |
+
+
+
+### Beacon chain
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_HAS_BEACON_CHAIN | `boolean` | Set to true for networks with the beacon chain | Required | - | `true` | v1.0.x+ |
+| NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL | `string` | Beacon network currency symbol | - | `NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL` | `ETH` | v1.0.x+ |
+
+
+
+### User operations (ERC-4337)
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_HAS_USER_OPS | `boolean` | Set to true to show user operations related data and pages | - | - | `true` | v1.23.0+ |
+
+
+
+### Rollup chain
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ |
+| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
+| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
+| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
+| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | - |
+
+
+
+### Export data to CSV file
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `` | v1.0.x+ |
+
+
+
+### Google analytics
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | true | - | `UA-XXXXXX-X` | v1.0.x+ |
+
+
+
+### Mixpanel analytics
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN | `string` | Project token for [Mixpanel](https://mixpanel.com/) analytics service | true | - | `` | v1.1.0+ |
+
+
+
+### GrowthBook feature flagging and A/B testing
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY | `string` | Client SDK key for [GrowthBook](https://www.growthbook.io/) service | true | - | `` | v1.22.0+ |
+
+
+
+### GraphQL API documentation
+
+This feature is **always enabled**, but you can disable it by passing `none` value to `NEXT_PUBLIC_GRAPHIQL_TRANSACTION` variable.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl playground page. Pass `none` to disable the feature. | - | - | `0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62` | v1.0.x+ |
+
+
+
+### REST API documentation
+
+This feature is **always enabled**, but you can disable it by passing `none` value to `NEXT_PUBLIC_API_SPEC_URL` variable.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec to be displayed on `/api-docs` page. Pass `none` to disable the feature. | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | v1.0.x+ |
+
+
+
+### Marketplace
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_MARKETPLACE_ENABLED | `boolean` | `true` means that the marketplace page will be enabled | Required | - | `true` | v1.24.1+ |
+| NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app. Can be replaced with NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | Required | - | `https://example.com/marketplace_config.json` | v1.0.x+ |
+| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ |
+| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | v1.0.x+ |
+| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | v1.24.0+ |
+| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ |
+| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | v1.23.0+ |
+| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | v1.28.0+ |
+| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | v1.29.0+ |
+| NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL | `string` | URL of the banner HTML content | - | - | `https://example.com/banner` | v1.29.0+ |
+| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
+| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ |
+| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ |
+
+#### Marketplace app configuration properties
+
+| Property | Type | Description | Compulsoriness | Example value |
+| --- | --- | --- | --- | --- |
+| id | `string` | Used as slug for the app. Must be unique in the app list. | Required | `'app'` |
+| external | `boolean` | `true` means that the application opens in a new window, but not in an iframe. | - | `true` |
+| title | `string` | Displayed title of the app. | Required | `'The App'` |
+| logo | `string` | URL to logo file. Should be at least 288x288. | Required | `'https://foo.app/icon.png'` |
+| shortDescription | `string` | Displayed only in the app list. | Required | `'Awesome app'` |
+| categories | `Array` | Displayed category. | Required | `['Security', 'Tools']` |
+| author | `string` | Displayed author of the app | Required | `'Bob'` |
+| url | `string` | URL of the app which will be launched in the iframe. | Required | `'https://foo.app/launch'` |
+| description | `string` | Displayed only in the modal dialog with additional info about the app. | Required | `'The best app'` |
+| site | `string` | Displayed site link | - | `'https://blockscout.com'` |
+| twitter | `string` | Displayed twitter link | - | `'https://twitter.com/blockscoutcom'` |
+| telegram | `string` | Displayed telegram link | - | `'https://t.me/poa_network'` |
+| github | `string` | Displayed github link | - | `'https://github.com/blockscout'` |
+| internalWallet | `boolean` | `true` means that the application can automatically connect to the Blockscout wallet. | - | `true` |
+| priority | `number` | The higher the priority, the higher the app will appear in the list on the Marketplace page. | - | `7` |
+
+
+
+### Solidity to UML diagrams
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | Required | - | `https://visualizer.services.blockscout.com` | v1.0.x+ |
+| NEXT_PUBLIC_VISUALIZE_API_BASE_PATH | `string` | Base path for Visualize API endpoint url | - | - | `/poa/core` | v1.29.0+ |
+
+
+
+### Blockchain statistics
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | Required | - | `https://stats.services.blockscout.com` | v1.0.x+ |
+| NEXT_PUBLIC_STATS_API_BASE_PATH | `string` | Base path for Stats API endpoint url | - | - | `/poa/core` | v1.29.0+ |
+
+
+
+### Web3 wallet integration (add token or network to the wallet)
+
+This feature is **enabled by default** with the `['metamask']` value. To switch it off pass `NEXT_PUBLIC_WEB3_WALLETS=none`.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_WEB3_WALLETS | `Array<'metamask' \| 'coinbase' \| 'token_pocket'>` | Array of Web3 wallets which will be used to add tokens or chain to. The first wallet which is enabled in user's browser will be shown. | - | `[ 'metamask' ]` | `[ 'coinbase' ]` | v1.10.0+ |
+| NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET | `boolean`| Set to `true` to hide icon "Add to your wallet" next to token addresses | - | - | `true` | v1.0.x+ |
+
+
+
+### Transaction interpretation
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER | `blockscout` \| `noves` \| `none` | Transaction interpretation provider that displays human readable transaction description | - | `none` | `blockscout` | v1.21.0+ |
+
+
+
+### Verified tokens info
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | Required | - | `https://contracts-info.services.blockscout.com` | v1.0.x+ |
+
+
+
+### Name service integration
+
+This feature allows resolving blockchain addresses using human-readable domain names.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_NAME_SERVICE_API_HOST | `string` | Name Service API endpoint url | Required | - | `https://bens.services.blockscout.com` | v1.22.0+ |
+
+
+
+### Metadata service integration
+
+This feature allows name tags and other public tags for addresses.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ |
+
+
+
+### Public tag submission
+
+This feature allows you to submit an application with a public address tag.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ |
+| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ |
+
+
+
+### Data Availability
+
+This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED | `boolean` | Set to true to enable blob transactions views. | Required | - | `true` | v1.28.0+ |
+
+
+
+### Bridged tokens
+
+This feature allows users to view tokens that have been bridged from other EVM chains. Additional tab "Bridged" will be added to the tokens page and the link to original token will be displayed on the token page.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS | `Array` where `BridgedTokenChain` can have following [properties](#bridged-token-chain-configuration-properties) | Used for displaying filter by the chain from which token where bridged. Also, used for creating links to original tokens in other explorers. | Required | - | `[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://eth.blockscout.com/token'}]` | v1.14.0+ |
+| NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES | `Array` where `TokenBridge` can have following [properties](#token-bridge-configuration-properties) | Used for displaying text about bridges types on the tokens page. | Required | - | `[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}]` | v1.14.0+ |
+
+#### Bridged token chain configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| id | `string` | Base chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `1` |
+| title | `string` | Displayed name of the chain | Required | - | `Ethereum` |
+| short_title | `string` | Used for displaying chain name in the list view as tag | Required | - | `ETH` |
+| base_url | `string` | Base url to original token in base chain explorer | Required | - | `https://eth.blockscout.com/token` |
+
+*Note* The url to original token will be constructed as `/`, e.g `https://eth.blockscout.com/token/`
+
+#### Token bridge configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| type | `string` | Bridge type; should be matched to `bridge_type` field in API response | Required | - | `omni` |
+| title | `string` | Bridge title | Required | - | `OmniBridge` |
+| short_title | `string` | Bridge short title for displaying in the tags | Required | - | `OMNI` |
+
+
+
+### Safe{Core} address tags
+
+For the smart contract addresses which are [Safe{Core} accounts](https://safe.global/) public tag "Multisig: Safe" will be displayed in the address page header alongside to Safe logo.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_SAFE_TX_SERVICE_URL | `string` | The Safe transaction service URL. See full list of supported networks [here](https://docs.safe.global/api-supported-networks). | - | - | `uniswap` | v1.26.0+ |
+
+
+
+### SUAVE chain
+
+For blockchains that implement SUAVE architecture additional fields will be shown on the transaction page ("Allowed peekers", "Kettle"). Users also will be able to see the list of all transactions for a particular Kettle in the separate view.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_IS_SUAVE_CHAIN | `boolean` | Set to true for blockchains with [SUAVE architecture](https://writings.flashbots.net/mevm-suave-centauri-and-beyond) | Required | - | `true` | v1.14.0+ |
+
+
+
+### MetaSuites extension
+
+Enables [MetaSuites browser extension](https://github.com/blocksecteam/metasuites) to integrate with the app views.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_METASUITES_ENABLED | `boolean` | Set to true to enable integration | Required | - | `true` | v1.26.0+ |
+
+
+
+### Validators list
+
+The feature enables the Validators page which provides detailed information about the validators of the PoS chains.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability'` | Chain type | Required | - | `'stability'` | v1.25.0+ |
+
+
+
+### Sentry error monitoring
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | Required | - | `` | v1.0.x+ |
+| SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `` | v1.0.x+ |
+| NEXT_PUBLIC_SENTRY_ENABLE_TRACING | `boolean` | Enables tracing and performance monitoring in Sentry.io | - | `false` | `true` | v1.17.0+ |
+| NEXT_PUBLIC_APP_ENV | `string` | App env (e.g development, review or production). Passed as `environment` property to Sentry config | - | `production` | `production` | v1.0.x+ |
+| 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` | v1.0.x+ |
+
+
+
+### OpenTelemetry
+
+OpenTelemetry SDK for Node.js app could be enabled by passing `OTEL_SDK_ENABLED=true` variable. Configure the OpenTelemetry Protocol Exporter by using the generic environment variables described in the [OT docs](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options). Note that this Next.js feature is currently experimental. The Docker image should be built with the `NEXT_OPEN_TELEMETRY_ENABLED=true` argument to enable it.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| OTEL_SDK_ENABLED | `boolean` | Run-time flag to enable the feature | Required | `false` | `true` | v1.18.0+ |
+
+
+
+### DeFi dropdown
+
+If the feature is enabled, a single button or a dropdown (if more than 1 item is provided) will be displayed at the top of the explorer page, which will take a user to the specified application in the marketplace or to an external site.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon: string; dappId?: string, url?: string }]` | An array of dropdown items containing the button text, icon name and dappId in DAppscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |
+
+
+
+### Multichain balance button
+
+If the feature is enabled, a Multichain balance button will be displayed on the address page, which will take you to the portfolio application in the marketplace or to an external site.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url_template: 'https://app.zerion.io/{address}/overview', logo: 'https://example.com/icon.svg'` | v1.31.0+ |
+
+
+
+#### Multichain button configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| name | `string` | Multichain portfolio application name | Required | - | `zerion` |
+| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` |
+| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` |
+| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |
+
+
+
+### Get gas button
+
+If the feature is enabled, a Get gas button will be displayed in the top bar, which will take you to the gas refuel application in the marketplace or to an external site.
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Get gas button config. See [below](#get-gas-button-configuration-properties) | - | - | `{ name: 'Need gas?', dapp_id: 'smol-refuel', url_template: 'https://smolrefuel.com/?outboundChain={chainId}', logo: 'https://example.com/icon.png' }` | v1.33.0+ |
+
+
+
+#### Get gas button configuration properties
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value |
+| --- | --- | --- | --- | --- | --- |
+| name | `string` | Text on the button | Required | - | `Need gas?` |
+| url_template | `string` | Url template, may contain `{chainId}` variable | Required | - | `https://smolrefuel.com/?outboundChain={chainId}` |
+| dapp_id | `string` | Set for open a Blockscout dapp page instead of opening external app page | - | - | `smol-refuel` |
+| logo | `string` | Gas refuel application logo url | - | - | `https://example.com/icon.png` |
+
+
+
+## External services configuration
+
+### Google ReCaptcha
+
+For obtaining the variables values please refer to [reCAPTCHA documentation](https://developers.google.com/recaptcha).
+
+| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
+| --- | --- | --- | --- | --- | --- | --- |
+| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key | - | - | `` | v1.0.x+ |
diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..d4378762bf
--- /dev/null
+++ b/docs/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,21 @@
+## Description and Related Issue(s)
+
+*[Provide a brief description of the changes or enhancements introduced by this pull request and explain motivation behind them. Cite any related issue(s) or bug(s) that it addresses using the [format](https://blog.github.com/2013-05-14-closing-issues-via-pull-requests/) `Fixes #123` or `Resolves #456`.]*
+
+### Proposed Changes
+*[Specify the changes or additions made in this pull request. Please mention if any changes were made to the ENV variables]*
+
+### Breaking or Incompatible Changes
+*[Describe any breaking or incompatible changes introduced by this pull request. Specify how users might need to modify their code or configurations to accommodate these changes.]*
+
+### Additional Information
+*[Include any additional information, context, or screenshots that may be helpful for reviewers.]*
+
+## Checklist for PR author
+- [ ] I have tested these changes locally.
+- [ ] I added tests to cover any new functionality, following this [guide](./CONTRIBUTING.md#writing--running-tests)
+- [ ] Whenever I fix a bug, I include a regression test to ensure that the bug does not reappear silently.
+- [ ] If I have added, changed, renamed, or removed an environment variable
+ - I updated the list of environment variables in the [documentation](ENVS.md)
+ - I made the necessary changes to the validator script according to the [guide](./CONTRIBUTING.md#adding-new-env-variable)
+ - I added "ENVs" label to this pull request
diff --git a/global.d.ts b/global.d.ts
new file mode 100644
index 0000000000..1632505b52
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,31 @@
+import type { WalletProvider } from 'types/web3';
+
+type CPreferences = {
+ zone: string;
+ width: string;
+ height: string;
+}
+
+declare global {
+ export interface Window {
+ ethereum?: WalletProvider | undefined;
+ coinzilla_display: Array;
+ ga?: {
+ getAll: () => Array<{ get: (prop: string) => string }>;
+ };
+ AdButler: {
+ ads: Array;
+ register: (...args: unknown) => void;
+ };
+ abkw: string;
+ __envs: Record;
+ }
+
+ namespace NodeJS {
+ interface ProcessEnv {
+ NODE_ENV: 'development' | 'production';
+ }
+ }
+}
+
+export {};
diff --git a/icons/ABI.svg b/icons/ABI.svg
new file mode 100644
index 0000000000..9e5e27982d
--- /dev/null
+++ b/icons/ABI.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/ABI_slim.svg b/icons/ABI_slim.svg
new file mode 100644
index 0000000000..89532207b1
--- /dev/null
+++ b/icons/ABI_slim.svg
@@ -0,0 +1,13 @@
+
diff --git a/icons/API.svg b/icons/API.svg
new file mode 100644
index 0000000000..2fae957e81
--- /dev/null
+++ b/icons/API.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/ENS.svg b/icons/ENS.svg
new file mode 100644
index 0000000000..9832944dab
--- /dev/null
+++ b/icons/ENS.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/ENS_slim.svg b/icons/ENS_slim.svg
new file mode 100644
index 0000000000..cd999b523a
--- /dev/null
+++ b/icons/ENS_slim.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/MUD.svg b/icons/MUD.svg
new file mode 100644
index 0000000000..8ab1229a71
--- /dev/null
+++ b/icons/MUD.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/MUD_menu.svg b/icons/MUD_menu.svg
new file mode 100644
index 0000000000..c30c571c47
--- /dev/null
+++ b/icons/MUD_menu.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/RPC.svg b/icons/RPC.svg
new file mode 100644
index 0000000000..7df2ba2195
--- /dev/null
+++ b/icons/RPC.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/apps.svg b/icons/apps.svg
new file mode 100644
index 0000000000..c0cdc1c4eb
--- /dev/null
+++ b/icons/apps.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/apps_list.svg b/icons/apps_list.svg
new file mode 100644
index 0000000000..62cb5020d6
--- /dev/null
+++ b/icons/apps_list.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/apps_slim.svg b/icons/apps_slim.svg
new file mode 100644
index 0000000000..59e2f2d818
--- /dev/null
+++ b/icons/apps_slim.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/down-right.svg b/icons/arrows/down-right.svg
new file mode 100644
index 0000000000..e02aa2f9a5
--- /dev/null
+++ b/icons/arrows/down-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/east-mini.svg b/icons/arrows/east-mini.svg
new file mode 100644
index 0000000000..1a9d93e401
--- /dev/null
+++ b/icons/arrows/east-mini.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/east.svg b/icons/arrows/east.svg
new file mode 100644
index 0000000000..c152a2f673
--- /dev/null
+++ b/icons/arrows/east.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/north-east.svg b/icons/arrows/north-east.svg
new file mode 100644
index 0000000000..59aa6a4b23
--- /dev/null
+++ b/icons/arrows/north-east.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/south-east.svg b/icons/arrows/south-east.svg
new file mode 100644
index 0000000000..16a7590f56
--- /dev/null
+++ b/icons/arrows/south-east.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/up-down.svg b/icons/arrows/up-down.svg
new file mode 100644
index 0000000000..8f45bb257d
--- /dev/null
+++ b/icons/arrows/up-down.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/arrows/up-head.svg b/icons/arrows/up-head.svg
new file mode 100644
index 0000000000..375381a790
--- /dev/null
+++ b/icons/arrows/up-head.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/beta.svg b/icons/beta.svg
new file mode 100644
index 0000000000..bba1309f3a
--- /dev/null
+++ b/icons/beta.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/beta_xs.svg b/icons/beta_xs.svg
new file mode 100644
index 0000000000..a6dc48ee4e
--- /dev/null
+++ b/icons/beta_xs.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/blob.svg b/icons/blob.svg
new file mode 100644
index 0000000000..9b40d72ecb
--- /dev/null
+++ b/icons/blob.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/blobs/image.svg b/icons/blobs/image.svg
new file mode 100644
index 0000000000..be08dd269c
--- /dev/null
+++ b/icons/blobs/image.svg
@@ -0,0 +1,11 @@
+
diff --git a/icons/blobs/raw.svg b/icons/blobs/raw.svg
new file mode 100644
index 0000000000..8a97401ff5
--- /dev/null
+++ b/icons/blobs/raw.svg
@@ -0,0 +1,11 @@
+
diff --git a/icons/blobs/text.svg b/icons/blobs/text.svg
new file mode 100644
index 0000000000..08ec8801bf
--- /dev/null
+++ b/icons/blobs/text.svg
@@ -0,0 +1,7 @@
+
diff --git a/icons/block.svg b/icons/block.svg
new file mode 100644
index 0000000000..85c88a6bdf
--- /dev/null
+++ b/icons/block.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/block_countdown.svg b/icons/block_countdown.svg
new file mode 100644
index 0000000000..0024e52ce9
--- /dev/null
+++ b/icons/block_countdown.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/block_slim.svg b/icons/block_slim.svg
new file mode 100644
index 0000000000..63302e1d83
--- /dev/null
+++ b/icons/block_slim.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/brands/blockscout.svg b/icons/brands/blockscout.svg
new file mode 100644
index 0000000000..0e3279de01
--- /dev/null
+++ b/icons/brands/blockscout.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/brands/safe.svg b/icons/brands/safe.svg
new file mode 100644
index 0000000000..8369513837
--- /dev/null
+++ b/icons/brands/safe.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/brands/solidity_scan.svg b/icons/brands/solidity_scan.svg
new file mode 100644
index 0000000000..ac5747c69a
--- /dev/null
+++ b/icons/brands/solidity_scan.svg
@@ -0,0 +1,9 @@
+
diff --git a/icons/burger.svg b/icons/burger.svg
new file mode 100644
index 0000000000..c6a84a911e
--- /dev/null
+++ b/icons/burger.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/certified.svg b/icons/certified.svg
new file mode 100644
index 0000000000..088a866f8a
--- /dev/null
+++ b/icons/certified.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/check.svg b/icons/check.svg
new file mode 100644
index 0000000000..563e6e5a9a
--- /dev/null
+++ b/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/clock-light.svg b/icons/clock-light.svg
new file mode 100644
index 0000000000..110cd4b797
--- /dev/null
+++ b/icons/clock-light.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/clock.svg b/icons/clock.svg
new file mode 100644
index 0000000000..14a8c94053
--- /dev/null
+++ b/icons/clock.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/coins/bitcoin.svg b/icons/coins/bitcoin.svg
new file mode 100644
index 0000000000..7f22b3139f
--- /dev/null
+++ b/icons/coins/bitcoin.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/collection.svg b/icons/collection.svg
new file mode 100644
index 0000000000..981040af5a
--- /dev/null
+++ b/icons/collection.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/contracts/proxy.svg b/icons/contracts/proxy.svg
new file mode 100644
index 0000000000..1b75cb210f
--- /dev/null
+++ b/icons/contracts/proxy.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/contracts/regular.svg b/icons/contracts/regular.svg
new file mode 100644
index 0000000000..1bc8e0d3d9
--- /dev/null
+++ b/icons/contracts/regular.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/contracts/regular_many.svg b/icons/contracts/regular_many.svg
new file mode 100644
index 0000000000..1f0b62afd2
--- /dev/null
+++ b/icons/contracts/regular_many.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/contracts/verified.svg b/icons/contracts/verified.svg
new file mode 100644
index 0000000000..6dbb058433
--- /dev/null
+++ b/icons/contracts/verified.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/contracts/verified_many.svg b/icons/contracts/verified_many.svg
new file mode 100644
index 0000000000..2a004f596d
--- /dev/null
+++ b/icons/contracts/verified_many.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/copy.svg b/icons/copy.svg
new file mode 100644
index 0000000000..778c59dcf1
--- /dev/null
+++ b/icons/copy.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/cross.svg b/icons/cross.svg
new file mode 100644
index 0000000000..44a66fcc75
--- /dev/null
+++ b/icons/cross.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/delete.svg b/icons/delete.svg
new file mode 100644
index 0000000000..13b71cdffe
--- /dev/null
+++ b/icons/delete.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/docs.svg b/icons/docs.svg
new file mode 100644
index 0000000000..71a628d93c
--- /dev/null
+++ b/icons/docs.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/donate.svg b/icons/donate.svg
new file mode 100644
index 0000000000..9ed7def2bf
--- /dev/null
+++ b/icons/donate.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/dots.svg b/icons/dots.svg
new file mode 100644
index 0000000000..1ea165f92f
--- /dev/null
+++ b/icons/dots.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/edit.svg b/icons/edit.svg
new file mode 100644
index 0000000000..02a2737da2
--- /dev/null
+++ b/icons/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/email-sent.svg b/icons/email-sent.svg
new file mode 100644
index 0000000000..d31e30f244
--- /dev/null
+++ b/icons/email-sent.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/email.svg b/icons/email.svg
new file mode 100644
index 0000000000..4e184be0e5
--- /dev/null
+++ b/icons/email.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/empty_search_result.svg b/icons/empty_search_result.svg
new file mode 100644
index 0000000000..f4d62eff0e
--- /dev/null
+++ b/icons/empty_search_result.svg
@@ -0,0 +1,16 @@
+
diff --git a/icons/error-pages/404.svg b/icons/error-pages/404.svg
new file mode 100644
index 0000000000..296ff701c8
--- /dev/null
+++ b/icons/error-pages/404.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/error-pages/422.svg b/icons/error-pages/422.svg
new file mode 100644
index 0000000000..99225c6433
--- /dev/null
+++ b/icons/error-pages/422.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/error-pages/429.svg b/icons/error-pages/429.svg
new file mode 100644
index 0000000000..9ae110e192
--- /dev/null
+++ b/icons/error-pages/429.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/error-pages/500.svg b/icons/error-pages/500.svg
new file mode 100644
index 0000000000..43cd887fd8
--- /dev/null
+++ b/icons/error-pages/500.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/explorer.svg b/icons/explorer.svg
new file mode 100644
index 0000000000..dd9e6b57af
--- /dev/null
+++ b/icons/explorer.svg
@@ -0,0 +1,11 @@
+
diff --git a/icons/files/csv.svg b/icons/files/csv.svg
new file mode 100644
index 0000000000..119d58cdf0
--- /dev/null
+++ b/icons/files/csv.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/files/image.svg b/icons/files/image.svg
new file mode 100644
index 0000000000..9d2da4ee82
--- /dev/null
+++ b/icons/files/image.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/files/json.svg b/icons/files/json.svg
new file mode 100644
index 0000000000..06c437efbf
--- /dev/null
+++ b/icons/files/json.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/files/placeholder.svg b/icons/files/placeholder.svg
new file mode 100644
index 0000000000..129e36a3bc
--- /dev/null
+++ b/icons/files/placeholder.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/files/sol.svg b/icons/files/sol.svg
new file mode 100644
index 0000000000..0efa5d0b39
--- /dev/null
+++ b/icons/files/sol.svg
@@ -0,0 +1,8 @@
+
diff --git a/icons/files/yul.svg b/icons/files/yul.svg
new file mode 100644
index 0000000000..03b88fbb3d
--- /dev/null
+++ b/icons/files/yul.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/filter.svg b/icons/filter.svg
new file mode 100644
index 0000000000..c9c284e81b
--- /dev/null
+++ b/icons/filter.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/flame.svg b/icons/flame.svg
new file mode 100644
index 0000000000..ddc5f161e6
--- /dev/null
+++ b/icons/flame.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/games.svg b/icons/games.svg
new file mode 100644
index 0000000000..39c01ca56d
--- /dev/null
+++ b/icons/games.svg
@@ -0,0 +1,8 @@
+
diff --git a/icons/gas.svg b/icons/gas.svg
new file mode 100644
index 0000000000..4334fe93f5
--- /dev/null
+++ b/icons/gas.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/gas_xl.svg b/icons/gas_xl.svg
new file mode 100644
index 0000000000..5a3913ac16
--- /dev/null
+++ b/icons/gas_xl.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/gear.svg b/icons/gear.svg
new file mode 100644
index 0000000000..32ec463e17
--- /dev/null
+++ b/icons/gear.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/gear_slim.svg b/icons/gear_slim.svg
new file mode 100644
index 0000000000..abc14e6a78
--- /dev/null
+++ b/icons/gear_slim.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/globe-b.svg b/icons/globe-b.svg
new file mode 100644
index 0000000000..0b185e8551
--- /dev/null
+++ b/icons/globe-b.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/globe.svg b/icons/globe.svg
new file mode 100644
index 0000000000..e3cea4b639
--- /dev/null
+++ b/icons/globe.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/graphQL.svg b/icons/graphQL.svg
new file mode 100644
index 0000000000..9332276ac2
--- /dev/null
+++ b/icons/graphQL.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/heart_filled.svg b/icons/heart_filled.svg
new file mode 100644
index 0000000000..80926b1668
--- /dev/null
+++ b/icons/heart_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/heart_outline.svg b/icons/heart_outline.svg
new file mode 100644
index 0000000000..8bf7ce3e36
--- /dev/null
+++ b/icons/heart_outline.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/hourglass.svg b/icons/hourglass.svg
new file mode 100644
index 0000000000..7ebd6d78b2
--- /dev/null
+++ b/icons/hourglass.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/info.svg b/icons/info.svg
new file mode 100644
index 0000000000..0401005742
--- /dev/null
+++ b/icons/info.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/integration/full.svg b/icons/integration/full.svg
new file mode 100644
index 0000000000..0ac50a345b
--- /dev/null
+++ b/icons/integration/full.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/integration/partial.svg b/icons/integration/partial.svg
new file mode 100644
index 0000000000..2f7a76969f
--- /dev/null
+++ b/icons/integration/partial.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/key.svg b/icons/key.svg
new file mode 100644
index 0000000000..ae0834f529
--- /dev/null
+++ b/icons/key.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/lightning.svg b/icons/lightning.svg
new file mode 100644
index 0000000000..03fea73d75
--- /dev/null
+++ b/icons/lightning.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/lightning_navbar.svg b/icons/lightning_navbar.svg
new file mode 100644
index 0000000000..9587a9c7a2
--- /dev/null
+++ b/icons/lightning_navbar.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/link.svg b/icons/link.svg
new file mode 100644
index 0000000000..5d072572aa
--- /dev/null
+++ b/icons/link.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/link_external.svg b/icons/link_external.svg
new file mode 100644
index 0000000000..dbddf710bc
--- /dev/null
+++ b/icons/link_external.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/lock.svg b/icons/lock.svg
new file mode 100644
index 0000000000..763d128cb7
--- /dev/null
+++ b/icons/lock.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/minus.svg b/icons/minus.svg
new file mode 100644
index 0000000000..ba1cfe2409
--- /dev/null
+++ b/icons/minus.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/monaco/file.svg b/icons/monaco/file.svg
new file mode 100644
index 0000000000..38b4a10fc4
--- /dev/null
+++ b/icons/monaco/file.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/monaco/folder-open.svg b/icons/monaco/folder-open.svg
new file mode 100644
index 0000000000..02989af8d2
--- /dev/null
+++ b/icons/monaco/folder-open.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/monaco/folder.svg b/icons/monaco/folder.svg
new file mode 100644
index 0000000000..70e98efa46
--- /dev/null
+++ b/icons/monaco/folder.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/monaco/solidity.svg b/icons/monaco/solidity.svg
new file mode 100644
index 0000000000..f1c51a4c69
--- /dev/null
+++ b/icons/monaco/solidity.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/monaco/vyper.svg b/icons/monaco/vyper.svg
new file mode 100644
index 0000000000..cd7b34ad9a
--- /dev/null
+++ b/icons/monaco/vyper.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/moon-with-star.svg b/icons/moon-with-star.svg
new file mode 100644
index 0000000000..24c2085873
--- /dev/null
+++ b/icons/moon-with-star.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/moon.svg b/icons/moon.svg
new file mode 100644
index 0000000000..57c4ee6564
--- /dev/null
+++ b/icons/moon.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/networks.svg b/icons/networks.svg
new file mode 100644
index 0000000000..cc62a35798
--- /dev/null
+++ b/icons/networks.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/networks/icon-placeholder.svg b/icons/networks/icon-placeholder.svg
new file mode 100644
index 0000000000..77414f1df4
--- /dev/null
+++ b/icons/networks/icon-placeholder.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/networks/logo-placeholder.svg b/icons/networks/logo-placeholder.svg
new file mode 100644
index 0000000000..6c7891fdbf
--- /dev/null
+++ b/icons/networks/logo-placeholder.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/nft_shield.svg b/icons/nft_shield.svg
new file mode 100644
index 0000000000..0d2a83e9a3
--- /dev/null
+++ b/icons/nft_shield.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/open-link.svg b/icons/open-link.svg
new file mode 100644
index 0000000000..d0fcc28ab4
--- /dev/null
+++ b/icons/open-link.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/output_roots.svg b/icons/output_roots.svg
new file mode 100644
index 0000000000..447e65743b
--- /dev/null
+++ b/icons/output_roots.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/payment_link.svg b/icons/payment_link.svg
new file mode 100644
index 0000000000..f97128fff6
--- /dev/null
+++ b/icons/payment_link.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/plus.svg b/icons/plus.svg
new file mode 100644
index 0000000000..b86a621188
--- /dev/null
+++ b/icons/plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/privattags.svg b/icons/privattags.svg
new file mode 100644
index 0000000000..7e0cacaa07
--- /dev/null
+++ b/icons/privattags.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/profile.svg b/icons/profile.svg
new file mode 100644
index 0000000000..177eea9650
--- /dev/null
+++ b/icons/profile.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/publictags.svg b/icons/publictags.svg
new file mode 100644
index 0000000000..4c55542fc6
--- /dev/null
+++ b/icons/publictags.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/publictags_slim.svg b/icons/publictags_slim.svg
new file mode 100644
index 0000000000..4de98a2d1f
--- /dev/null
+++ b/icons/publictags_slim.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/qr_code.svg b/icons/qr_code.svg
new file mode 100644
index 0000000000..f31b981dae
--- /dev/null
+++ b/icons/qr_code.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/refresh.svg b/icons/refresh.svg
new file mode 100644
index 0000000000..fef0346a50
--- /dev/null
+++ b/icons/refresh.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/repeat.svg b/icons/repeat.svg
new file mode 100644
index 0000000000..dcd2c7a374
--- /dev/null
+++ b/icons/repeat.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/restAPI.svg b/icons/restAPI.svg
new file mode 100644
index 0000000000..8a6faf3d5f
--- /dev/null
+++ b/icons/restAPI.svg
@@ -0,0 +1,12 @@
+
diff --git a/icons/rocket.svg b/icons/rocket.svg
new file mode 100644
index 0000000000..46523e1b05
--- /dev/null
+++ b/icons/rocket.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/rocket_xl.svg b/icons/rocket_xl.svg
new file mode 100644
index 0000000000..8b3f4ccdbf
--- /dev/null
+++ b/icons/rocket_xl.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/scope.svg b/icons/scope.svg
new file mode 100644
index 0000000000..6337932af8
--- /dev/null
+++ b/icons/scope.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/score/score-not-ok.svg b/icons/score/score-not-ok.svg
new file mode 100644
index 0000000000..9b4533f6ac
--- /dev/null
+++ b/icons/score/score-not-ok.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/score/score-ok.svg b/icons/score/score-ok.svg
new file mode 100644
index 0000000000..dbe11836fd
--- /dev/null
+++ b/icons/score/score-ok.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/search.svg b/icons/search.svg
new file mode 100644
index 0000000000..1d7814fa05
--- /dev/null
+++ b/icons/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/canny.svg b/icons/social/canny.svg
new file mode 100644
index 0000000000..8041cd2bf3
--- /dev/null
+++ b/icons/social/canny.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/coingecko.svg b/icons/social/coingecko.svg
new file mode 100644
index 0000000000..baf56eef3a
--- /dev/null
+++ b/icons/social/coingecko.svg
@@ -0,0 +1,8 @@
+
diff --git a/icons/social/coinmarketcap.svg b/icons/social/coinmarketcap.svg
new file mode 100644
index 0000000000..0341e03c42
--- /dev/null
+++ b/icons/social/coinmarketcap.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/defi_llama.svg b/icons/social/defi_llama.svg
new file mode 100644
index 0000000000..f7d2cc51a2
--- /dev/null
+++ b/icons/social/defi_llama.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/social/discord.svg b/icons/social/discord.svg
new file mode 100644
index 0000000000..133a54e97a
--- /dev/null
+++ b/icons/social/discord.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/discord_filled.svg b/icons/social/discord_filled.svg
new file mode 100644
index 0000000000..691efc8a4e
--- /dev/null
+++ b/icons/social/discord_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/facebook_filled.svg b/icons/social/facebook_filled.svg
new file mode 100644
index 0000000000..12afda1977
--- /dev/null
+++ b/icons/social/facebook_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/git.svg b/icons/social/git.svg
new file mode 100644
index 0000000000..caf39a4c95
--- /dev/null
+++ b/icons/social/git.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/github_filled.svg b/icons/social/github_filled.svg
new file mode 100644
index 0000000000..e134fcaf9a
--- /dev/null
+++ b/icons/social/github_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/linkedin_filled.svg b/icons/social/linkedin_filled.svg
new file mode 100644
index 0000000000..6fcd9bedf9
--- /dev/null
+++ b/icons/social/linkedin_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/medium_filled.svg b/icons/social/medium_filled.svg
new file mode 100644
index 0000000000..60f2d6303c
--- /dev/null
+++ b/icons/social/medium_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/opensea_filled.svg b/icons/social/opensea_filled.svg
new file mode 100644
index 0000000000..a1369318e9
--- /dev/null
+++ b/icons/social/opensea_filled.svg
@@ -0,0 +1,8 @@
+
diff --git a/icons/social/reddit_filled.svg b/icons/social/reddit_filled.svg
new file mode 100644
index 0000000000..a57d9f7979
--- /dev/null
+++ b/icons/social/reddit_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/slack_filled.svg b/icons/social/slack_filled.svg
new file mode 100644
index 0000000000..177f971226
--- /dev/null
+++ b/icons/social/slack_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/stats.svg b/icons/social/stats.svg
new file mode 100644
index 0000000000..cfc239f889
--- /dev/null
+++ b/icons/social/stats.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/social/telega.svg b/icons/social/telega.svg
new file mode 100644
index 0000000000..432bc5c763
--- /dev/null
+++ b/icons/social/telega.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/social/telegram_filled.svg b/icons/social/telegram_filled.svg
new file mode 100644
index 0000000000..87a2ebf52b
--- /dev/null
+++ b/icons/social/telegram_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/twitter.svg b/icons/social/twitter.svg
new file mode 100644
index 0000000000..21e9812ff7
--- /dev/null
+++ b/icons/social/twitter.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/social/twitter_filled.svg b/icons/social/twitter_filled.svg
new file mode 100644
index 0000000000..0d73b850a0
--- /dev/null
+++ b/icons/social/twitter_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/star_filled.svg b/icons/star_filled.svg
new file mode 100644
index 0000000000..7b6312c876
--- /dev/null
+++ b/icons/star_filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/star_outline.svg b/icons/star_outline.svg
new file mode 100644
index 0000000000..05286fa1d5
--- /dev/null
+++ b/icons/star_outline.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/stats.svg b/icons/stats.svg
new file mode 100644
index 0000000000..127477d180
--- /dev/null
+++ b/icons/stats.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/status/error.svg b/icons/status/error.svg
new file mode 100644
index 0000000000..3f7dff92c3
--- /dev/null
+++ b/icons/status/error.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/status/pending.svg b/icons/status/pending.svg
new file mode 100644
index 0000000000..f9e5a88d53
--- /dev/null
+++ b/icons/status/pending.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/status/success.svg b/icons/status/success.svg
new file mode 100644
index 0000000000..ce76c56a49
--- /dev/null
+++ b/icons/status/success.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/status/warning.svg b/icons/status/warning.svg
new file mode 100644
index 0000000000..c177aea5a5
--- /dev/null
+++ b/icons/status/warning.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/sun.svg b/icons/sun.svg
new file mode 100644
index 0000000000..f90bf90eab
--- /dev/null
+++ b/icons/sun.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/swap.svg b/icons/swap.svg
new file mode 100644
index 0000000000..c1566be5fc
--- /dev/null
+++ b/icons/swap.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/testnet.svg b/icons/testnet.svg
new file mode 100644
index 0000000000..ff01ccf7ad
--- /dev/null
+++ b/icons/testnet.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/token-placeholder.svg b/icons/token-placeholder.svg
new file mode 100644
index 0000000000..92307c47e4
--- /dev/null
+++ b/icons/token-placeholder.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/token.svg b/icons/token.svg
new file mode 100644
index 0000000000..933d69406f
--- /dev/null
+++ b/icons/token.svg
@@ -0,0 +1,9 @@
+
diff --git a/icons/tokens.svg b/icons/tokens.svg
new file mode 100644
index 0000000000..40c10466d6
--- /dev/null
+++ b/icons/tokens.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/tokens/xdai.svg b/icons/tokens/xdai.svg
new file mode 100644
index 0000000000..17028c76bf
--- /dev/null
+++ b/icons/tokens/xdai.svg
@@ -0,0 +1,9 @@
+
diff --git a/icons/top-accounts.svg b/icons/top-accounts.svg
new file mode 100644
index 0000000000..b74d07e854
--- /dev/null
+++ b/icons/top-accounts.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/transactions.svg b/icons/transactions.svg
new file mode 100644
index 0000000000..3cefc2141e
--- /dev/null
+++ b/icons/transactions.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/transactions_slim.svg b/icons/transactions_slim.svg
new file mode 100644
index 0000000000..412f46aa31
--- /dev/null
+++ b/icons/transactions_slim.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/txn_batches.svg b/icons/txn_batches.svg
new file mode 100644
index 0000000000..00dd273c27
--- /dev/null
+++ b/icons/txn_batches.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/txn_batches_slim.svg b/icons/txn_batches_slim.svg
new file mode 100644
index 0000000000..b10a430734
--- /dev/null
+++ b/icons/txn_batches_slim.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/uniswap.svg b/icons/uniswap.svg
new file mode 100644
index 0000000000..7abbc79f08
--- /dev/null
+++ b/icons/uniswap.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/user_op.svg b/icons/user_op.svg
new file mode 100644
index 0000000000..02bc701401
--- /dev/null
+++ b/icons/user_op.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/user_op_slim.svg b/icons/user_op_slim.svg
new file mode 100644
index 0000000000..d8c64b52b8
--- /dev/null
+++ b/icons/user_op_slim.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/validator.svg b/icons/validator.svg
new file mode 100644
index 0000000000..e77bb0ba5d
--- /dev/null
+++ b/icons/validator.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/verification-steps/finalized.svg b/icons/verification-steps/finalized.svg
new file mode 100644
index 0000000000..fbae66d631
--- /dev/null
+++ b/icons/verification-steps/finalized.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/verification-steps/unfinalized.svg b/icons/verification-steps/unfinalized.svg
new file mode 100644
index 0000000000..49fdbc7ddf
--- /dev/null
+++ b/icons/verification-steps/unfinalized.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/verified.svg b/icons/verified.svg
new file mode 100644
index 0000000000..b92152ab44
--- /dev/null
+++ b/icons/verified.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/wallet.svg b/icons/wallet.svg
new file mode 100644
index 0000000000..f1765c246d
--- /dev/null
+++ b/icons/wallet.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/wallets/coinbase.svg b/icons/wallets/coinbase.svg
new file mode 100644
index 0000000000..9a75b44e6d
--- /dev/null
+++ b/icons/wallets/coinbase.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/wallets/metamask.svg b/icons/wallets/metamask.svg
new file mode 100644
index 0000000000..ea6a6382a5
--- /dev/null
+++ b/icons/wallets/metamask.svg
@@ -0,0 +1,14 @@
+
diff --git a/icons/wallets/token-pocket.svg b/icons/wallets/token-pocket.svg
new file mode 100644
index 0000000000..e9566f816d
--- /dev/null
+++ b/icons/wallets/token-pocket.svg
@@ -0,0 +1,17 @@
+
diff --git a/icons/watchlist.svg b/icons/watchlist.svg
new file mode 100644
index 0000000000..6570c04066
--- /dev/null
+++ b/icons/watchlist.svg
@@ -0,0 +1,3 @@
+
diff --git a/instrumentation.node.ts b/instrumentation.node.ts
new file mode 100644
index 0000000000..5cae612a92
--- /dev/null
+++ b/instrumentation.node.ts
@@ -0,0 +1,71 @@
+/* eslint-disable no-console */
+import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
+import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
+import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import { Resource } from '@opentelemetry/resources';
+import {
+ PeriodicExportingMetricReader,
+ ConsoleMetricExporter,
+} from '@opentelemetry/sdk-metrics';
+import { NodeSDK } from '@opentelemetry/sdk-node';
+import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
+import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
+
+diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
+
+const traceExporter = new OTLPTraceExporter();
+
+const sdk = new NodeSDK({
+ resource: new Resource({
+ [SemanticResourceAttributes.SERVICE_NAME]: 'blockscout_frontend',
+ [SemanticResourceAttributes.SERVICE_VERSION]: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || 'unknown_version',
+ [SemanticResourceAttributes.SERVICE_INSTANCE_ID]:
+ process.env.NEXT_PUBLIC_APP_INSTANCE ||
+ process.env.NEXT_PUBLIC_APP_HOST?.replace('.blockscout.com', '').replaceAll('-', '_') ||
+ 'unknown_app',
+ }),
+ spanProcessor: new SimpleSpanProcessor(traceExporter),
+ traceExporter,
+ metricReader: new PeriodicExportingMetricReader({
+ exporter:
+ process.env.NODE_ENV === 'production' ?
+ new OTLPMetricExporter() :
+ new ConsoleMetricExporter(),
+ }),
+ instrumentations: [
+ getNodeAutoInstrumentations({
+ '@opentelemetry/instrumentation-http': {
+ ignoreIncomingRequestHook: (request) => {
+ try {
+ if (!request.url) {
+ return false;
+ }
+ const url = new URL(request.url, `http://${ request.headers.host }`);
+ if (
+ url.pathname.startsWith('/_next/static/') ||
+ url.pathname.startsWith('/_next/data/') ||
+ url.pathname.startsWith('/assets/') ||
+ url.pathname.startsWith('/static/')
+ ) {
+ return true;
+ }
+ } catch (error) {}
+ return false;
+ },
+ },
+ }),
+ ],
+});
+
+if (process.env.OTEL_SDK_ENABLED) {
+ sdk.start();
+
+ process.on('SIGTERM', () => {
+ sdk
+ .shutdown()
+ .then(() => console.log('Tracing terminated'))
+ .catch((error) => console.log('Error terminating tracing', error))
+ .finally(() => process.exit(0));
+ });
+}
diff --git a/instrumentation.ts b/instrumentation.ts
new file mode 100644
index 0000000000..c5384e36a7
--- /dev/null
+++ b/instrumentation.ts
@@ -0,0 +1,5 @@
+export async function register() {
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ await import('./instrumentation.node');
+ }
+}
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 0000000000..d87d585507
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,41 @@
+/* eslint-disable max-len */
+import type { JestConfigWithTsJest } from 'ts-jest';
+
+/*
+ * For a detailed explanation regarding each configuration property and type check, visit:
+ * https://jestjs.io/docs/configuration
+ */
+
+const config: JestConfigWithTsJest = {
+ clearMocks: true,
+ coverageProvider: 'v8',
+ globalSetup: '/jest/global-setup.ts',
+ moduleDirectories: [
+ 'node_modules',
+ __dirname,
+ ],
+ moduleNameMapper: {
+ '^jest/(.*)': '/jest/$1',
+ },
+ modulePathIgnorePatterns: [
+ 'node_modules_linux',
+ ],
+ preset: 'ts-jest',
+ reporters: [ 'default', 'github-actions' ],
+ setupFiles: [
+ '/jest/setup.ts',
+ ],
+ testEnvironment: 'jsdom',
+ transform: {
+ // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
+ // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ tsconfig: 'tsconfig.jest.json',
+ },
+ ],
+ },
+};
+
+export default config;
diff --git a/jest/global-setup.ts b/jest/global-setup.ts
new file mode 100644
index 0000000000..1d17469be1
--- /dev/null
+++ b/jest/global-setup.ts
@@ -0,0 +1,5 @@
+import dotenv from 'dotenv';
+
+export default async function globalSetup() {
+ dotenv.config({ path: './configs/envs/.env.jest' });
+}
diff --git a/jest/lib.tsx b/jest/lib.tsx
new file mode 100644
index 0000000000..deafc637c6
--- /dev/null
+++ b/jest/lib.tsx
@@ -0,0 +1,57 @@
+import { ChakraProvider } from '@chakra-ui/react';
+import { GrowthBookProvider } from '@growthbook/growthbook-react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import type { RenderOptions } from '@testing-library/react';
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { AppContextProvider } from 'lib/contexts/app';
+import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
+import { SocketProvider } from 'lib/socket/context';
+import theme from 'theme';
+
+import 'lib/setLocale';
+
+const PAGE_PROPS = {
+ cookies: '',
+ referrer: '',
+ query: {},
+ adBannerProvider: null,
+ apiData: null,
+};
+
+const TestApp = ({ children }: {children: React.ReactNode}) => {
+ const [ queryClient ] = React.useState(() => new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ retry: 0,
+ },
+ },
+ }));
+
+ return (
+
+
+
+
+
+
+ { children }
+
+
+
+
+
+
+ );
+};
+
+const customRender = (
+ ui: React.ReactElement,
+ options?: Omit,
+) => render(ui, { wrapper: TestApp, ...options });
+
+export * from '@testing-library/react';
+export { customRender as render };
+export { TestApp as wrapper };
diff --git a/jest/mocks/next-router.ts b/jest/mocks/next-router.ts
new file mode 100644
index 0000000000..4213a7ab96
--- /dev/null
+++ b/jest/mocks/next-router.ts
@@ -0,0 +1,17 @@
+import type { NextRouter } from 'next/router';
+
+export const router = {
+ query: {},
+ push: jest.fn(() => Promise.resolve()),
+};
+
+export const useRouter = jest.fn>>(() => (router));
+
+export const mockUseRouter = (params?: Partial) => {
+ return {
+ useRouter: jest.fn(() => ({
+ ...router,
+ ...params,
+ })),
+ };
+};
diff --git a/jest/setup.ts b/jest/setup.ts
new file mode 100644
index 0000000000..6ccc67ad4c
--- /dev/null
+++ b/jest/setup.ts
@@ -0,0 +1,43 @@
+import dotenv from 'dotenv';
+import { TextEncoder, TextDecoder } from 'util';
+
+import fetchMock from 'jest-fetch-mock';
+
+fetchMock.enableMocks();
+
+const envs = dotenv.config({ path: './configs/envs/.env.jest' });
+
+Object.assign(global, { TextDecoder, TextEncoder });
+
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: jest.fn().mockImplementation(query => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ })),
+});
+
+Object.defineProperty(window, '__envs', {
+ writable: true,
+ value: envs.parsed || {},
+});
+
+// eslint-disable-next-line no-console
+const consoleError = console.error;
+
+global.console = {
+ ...console,
+ error: (...args) => {
+ // silence some irrelevant errors
+ if (args.some((arg) => typeof arg === 'string' && arg.includes('Using kebab-case for css properties'))) {
+ return;
+ }
+ consoleError(...args);
+ },
+};
diff --git a/jest/utils/flushPromises.ts b/jest/utils/flushPromises.ts
new file mode 100644
index 0000000000..2910c7deba
--- /dev/null
+++ b/jest/utils/flushPromises.ts
@@ -0,0 +1,7 @@
+const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout;
+
+export default function flushPromises() {
+ return new Promise(function(resolve) {
+ scheduler(resolve);
+ });
+}
diff --git a/lib/address/parseMetaPayload.ts b/lib/address/parseMetaPayload.ts
new file mode 100644
index 0000000000..e8b067b148
--- /dev/null
+++ b/lib/address/parseMetaPayload.ts
@@ -0,0 +1,42 @@
+import type { AddressMetadataTag } from 'types/api/addressMetadata';
+import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
+
+type MetaParsed = NonNullable;
+
+export default function parseMetaPayload(meta: AddressMetadataTag['meta']): AddressMetadataTagFormatted['meta'] {
+ try {
+ const parsedMeta = JSON.parse(meta || '');
+
+ if (typeof parsedMeta !== 'object' || parsedMeta === null || Array.isArray(parsedMeta)) {
+ throw new Error('Invalid JSON');
+ }
+
+ const result: AddressMetadataTagFormatted['meta'] = {};
+
+ const stringFields: Array = [
+ 'textColor',
+ 'bgColor',
+ 'tagIcon',
+ 'tagUrl',
+ 'tooltipIcon',
+ 'tooltipTitle',
+ 'tooltipDescription',
+ 'tooltipUrl',
+ 'appID',
+ 'appMarketplaceURL',
+ 'appLogoURL',
+ 'appActionButtonText',
+ 'warpcastHandle',
+ ];
+
+ for (const stringField of stringFields) {
+ if (stringField in parsedMeta && typeof parsedMeta[stringField as keyof typeof parsedMeta] === 'string') {
+ result[stringField] = parsedMeta[stringField as keyof typeof parsedMeta];
+ }
+ }
+
+ return result;
+ } catch (error) {
+ return null;
+ }
+}
diff --git a/lib/address/useAddressMetadataInfoQuery.ts b/lib/address/useAddressMetadataInfoQuery.ts
new file mode 100644
index 0000000000..14c22ab676
--- /dev/null
+++ b/lib/address/useAddressMetadataInfoQuery.ts
@@ -0,0 +1,35 @@
+import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata';
+
+import config from 'configs/app';
+import useApiQuery from 'lib/api/useApiQuery';
+
+import parseMetaPayload from './parseMetaPayload';
+
+export default function useAddressMetadataInfoQuery(addresses: Array, isEnabled = true) {
+
+ const resource = 'address_metadata_info';
+
+ return useApiQuery(resource, {
+ queryParams: {
+ addresses,
+ chainId: config.chain.id,
+ tagsLimit: '20',
+ },
+ queryOptions: {
+ enabled: isEnabled && addresses.length > 0 && config.features.addressMetadata.isEnabled,
+ select: (data) => {
+ const addresses = Object.entries(data.addresses)
+ .map(([ address, { tags, reputation } ]) => {
+ const formattedTags: Array = tags.map((tag) => ({ ...tag, meta: parseMetaPayload(tag.meta) }));
+ return [ address.toLowerCase(), { tags: formattedTags, reputation } ] as const;
+ })
+ .reduce((result, item) => {
+ result[item[0]] = item[1];
+ return result;
+ }, {} as AddressMetadataInfoFormatted['addresses']);
+
+ return { addresses };
+ },
+ },
+ });
+}
diff --git a/lib/api/buildUrl.test.ts b/lib/api/buildUrl.test.ts
new file mode 100644
index 0000000000..29ea283a95
--- /dev/null
+++ b/lib/api/buildUrl.test.ts
@@ -0,0 +1,43 @@
+import buildUrl from './buildUrl';
+
+test('builds URL for resource without path params', () => {
+ const url = buildUrl('config_backend_version');
+ expect(url).toBe('https://localhost:3003/api/v2/config/backend-version');
+});
+
+test('builds URL for resource with path params', () => {
+ const url = buildUrl('block', { height_or_hash: '42' });
+ expect(url).toBe('https://localhost:3003/api/v2/blocks/42');
+});
+
+describe('falsy query parameters', () => {
+ test('leaves "false" as query parameter', () => {
+ const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: false });
+ expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=false');
+ });
+
+ test('leaves "null" as query parameter', () => {
+ const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null });
+ expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null');
+ });
+
+ test('strips out empty string as query parameter', () => {
+ const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null, sort: '' });
+ expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null');
+ });
+
+ test('strips out "undefined" as query parameter', () => {
+ const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null, sort: undefined });
+ expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null');
+ });
+});
+
+test('builds URL with array-like query parameters', () => {
+ const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: [ '0x11', '0x22' ], sort: 'asc' });
+ expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=0x11%2C0x22&sort=asc');
+});
+
+test('builds URL for resource with custom API endpoint', () => {
+ const url = buildUrl('token_verified_info', { chainId: '42', hash: '0x11' });
+ expect(url).toBe('https://localhost:3005/api/v1/chains/42/token-infos/0x11');
+});
diff --git a/lib/api/buildUrl.ts b/lib/api/buildUrl.ts
new file mode 100644
index 0000000000..07805dbd9c
--- /dev/null
+++ b/lib/api/buildUrl.ts
@@ -0,0 +1,26 @@
+import { compile } from 'path-to-regexp';
+
+import config from 'configs/app';
+
+import isNeedProxy from './isNeedProxy';
+import { RESOURCES } from './resources';
+import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
+
+export default function buildUrl(
+ resourceName: R,
+ pathParams?: ResourcePathParams,
+ queryParams?: Record | number | boolean | null | undefined>,
+): string {
+ const resource: ApiResource = RESOURCES[resourceName];
+ const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
+ const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath;
+ const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
+ const url = new URL(compile(path)(pathParams), baseUrl);
+
+ queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
+ // there are some pagination params that can be null or false for the next page
+ value !== undefined && value !== '' && url.searchParams.append(key, String(value));
+ });
+
+ return url.toString();
+}
diff --git a/lib/api/isBodyAllowed.ts b/lib/api/isBodyAllowed.ts
new file mode 100644
index 0000000000..aad52fedb7
--- /dev/null
+++ b/lib/api/isBodyAllowed.ts
@@ -0,0 +1,3 @@
+export default function isBodyAllowed(method: string | undefined | null) {
+ return method && ![ 'GET', 'HEAD' ].includes(method);
+}
diff --git a/lib/api/isNeedProxy.ts b/lib/api/isNeedProxy.ts
new file mode 100644
index 0000000000..a7705091e4
--- /dev/null
+++ b/lib/api/isNeedProxy.ts
@@ -0,0 +1,13 @@
+import config from 'configs/app';
+
+// FIXME
+// I was not able to figure out how to send CORS with credentials from localhost
+// unsuccessfully tried different ways, even custom local dev domain
+// so for local development we have to use next.js api as proxy server
+export default function isNeedProxy() {
+ if (config.app.useProxy) {
+ return true;
+ }
+
+ return config.app.host === 'localhost' && config.app.host !== config.api.host;
+}
diff --git a/lib/api/resources.ts b/lib/api/resources.ts
new file mode 100644
index 0000000000..3f1d6cf6f1
--- /dev/null
+++ b/lib/api/resources.ts
@@ -0,0 +1,1147 @@
+import type * as bens from '@blockscout/bens-types';
+import type * as stats from '@blockscout/stats-types';
+import type * as visualizer from '@blockscout/visualizer-types';
+import { getFeaturePayload } from 'configs/app/features/types';
+import type {
+ UserInfo,
+ CustomAbis,
+ ApiKeys,
+ VerifiedAddressResponse,
+ TokenInfoApplicationConfig,
+ TokenInfoApplications,
+ WatchlistResponse,
+ TransactionTagsResponse,
+ AddressTagsResponse,
+} from 'types/api/account';
+import type {
+ Address,
+ AddressCounters,
+ AddressTabsCounters,
+ AddressTransactionsResponse,
+ AddressTokenTransferResponse,
+ AddressCoinBalanceHistoryResponse,
+ AddressCoinBalanceHistoryChart,
+ AddressBlocksValidatedResponse,
+ AddressInternalTxsResponse,
+ AddressTxsFilters,
+ AddressTokenTransferFilters,
+ AddressTokensFilter,
+ AddressTokensResponse,
+ AddressWithdrawalsResponse,
+ AddressNFTsResponse,
+ AddressCollectionsResponse,
+ AddressNFTTokensFilter,
+ AddressCoinBalanceHistoryChartOld,
+ AddressMudTables,
+ AddressMudTablesFilter,
+ AddressMudRecords,
+ AddressMudRecordsFilter,
+ AddressMudRecordsSorting,
+ AddressMudRecord,
+} from 'types/api/address';
+import type { AddressesResponse } from 'types/api/addresses';
+import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
+import type {
+ ArbitrumL2MessagesResponse,
+ ArbitrumL2TxnBatch,
+ ArbitrumL2TxnBatchesResponse,
+ ArbitrumL2BatchTxs,
+ ArbitrumL2BatchBlocks,
+} from 'types/api/arbitrumL2';
+import type { TxBlobs, Blob } from 'types/api/blobs';
+import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block';
+import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
+import type { BackendVersionConfig } from 'types/api/configs';
+import type {
+ SmartContract,
+ SmartContractVerificationConfigRaw,
+ SolidityscanReport,
+ SmartContractSecurityAudits,
+} from 'types/api/contract';
+import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
+import type {
+ EnsAddressLookupFilters,
+ EnsDomainLookupFilters,
+ EnsLookupSorting,
+} from 'types/api/ens';
+import type { IndexingStatus } from 'types/api/indexingStatus';
+import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
+import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
+import type { MudWorldsResponse } from 'types/api/mudWorlds';
+import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves';
+import type {
+ OptimisticL2DepositsResponse,
+ OptimisticL2DepositsItem,
+ OptimisticL2OutputRootsResponse,
+ OptimisticL2TxnBatchesResponse,
+ OptimisticL2WithdrawalsResponse,
+ OptimisticL2DisputeGamesResponse,
+} from 'types/api/optimisticL2';
+import type { RawTracesResponse } from 'types/api/rawTrace';
+import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
+import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium';
+import type { HomeStats } from 'types/api/stats';
+import type {
+ TokenCounters,
+ TokenInfo,
+ TokenHolders,
+ TokenInventoryResponse,
+ TokenInstance,
+ TokenInstanceTransfersCount,
+ TokenVerifiedInfo,
+ TokenInventoryFilters,
+} from 'types/api/token';
+import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
+import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
+import type {
+ TransactionsResponseValidated,
+ TransactionsResponsePending,
+ Transaction,
+ TransactionsResponseWatchlist,
+ TransactionsSorting,
+ TransactionsResponseWithBlobs,
+ TransactionsStats,
+} from 'types/api/transaction';
+import type { TxInterpretationResponse } from 'types/api/txInterpretation';
+import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters';
+import type { TxStateChanges } from 'types/api/txStateChanges';
+import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps';
+import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ValidatorsSorting } from 'types/api/validators';
+import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
+import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
+import type {
+ ZkEvmL2DepositsResponse,
+ ZkEvmL2TxnBatch,
+ ZkEvmL2TxnBatchesItem,
+ ZkEvmL2TxnBatchesResponse,
+ ZkEvmL2TxnBatchTxs,
+ ZkEvmL2WithdrawalsResponse,
+} from 'types/api/zkEvmL2';
+import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2';
+import type { MarketplaceAppOverview } from 'types/client/marketplace';
+import type { ArrayElement } from 'types/utils';
+
+import config from 'configs/app';
+
+const marketplaceFeature = getFeaturePayload(config.features.marketplace);
+const marketplaceApi = marketplaceFeature && 'api' in marketplaceFeature ? marketplaceFeature.api : undefined;
+
+export interface ApiResource {
+ path: ResourcePath;
+ endpoint?: string;
+ basePath?: string;
+ pathParams?: Array;
+ needAuth?: boolean; // for external APIs which require authentication
+ headers?: RequestInit['headers'];
+}
+
+export const SORTING_FIELDS = [ 'sort', 'order' ];
+
+export const RESOURCES = {
+ // ACCOUNT
+ csrf: {
+ path: '/api/account/v2/get_csrf',
+ },
+ user_info: {
+ path: '/api/account/v2/user/info',
+ },
+ email_resend: {
+ path: '/api/account/v2/email/resend',
+ },
+ custom_abi: {
+ path: '/api/account/v2/user/custom_abis/:id?',
+ pathParams: [ 'id' as const ],
+ },
+ watchlist: {
+ path: '/api/account/v2/user/watchlist/:id?',
+ pathParams: [ 'id' as const ],
+ filterFields: [ ],
+ },
+ private_tags_address: {
+ path: '/api/account/v2/user/tags/address/:id?',
+ pathParams: [ 'id' as const ],
+ filterFields: [ ],
+ },
+ private_tags_tx: {
+ path: '/api/account/v2/user/tags/transaction/:id?',
+ pathParams: [ 'id' as const ],
+ filterFields: [ ],
+ },
+ api_keys: {
+ path: '/api/account/v2/user/api_keys/:id?',
+ pathParams: [ 'id' as const ],
+ },
+
+ // ACCOUNT: ADDRESS VERIFICATION & TOKEN INFO
+ address_verification: {
+ path: '/api/v1/chains/:chainId/verified-addresses:type',
+ pathParams: [ 'chainId' as const, 'type' as const ],
+ endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
+ needAuth: true,
+ },
+
+ verified_addresses: {
+ path: '/api/v1/chains/:chainId/verified-addresses',
+ pathParams: [ 'chainId' as const ],
+ endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
+ needAuth: true,
+ },
+
+ token_info_applications_config: {
+ path: '/api/v1/chains/:chainId/token-info-submissions/selectors',
+ pathParams: [ 'chainId' as const ],
+ endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
+ needAuth: true,
+ },
+
+ token_info_applications: {
+ path: '/api/v1/chains/:chainId/token-info-submissions/:id?',
+ pathParams: [ 'chainId' as const, 'id' as const ],
+ endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
+ needAuth: true,
+ },
+
+ // STATS MICROSERVICE API
+ stats_counters: {
+ path: '/api/v1/counters',
+ endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.stats)?.api.basePath,
+ },
+ stats_lines: {
+ path: '/api/v1/lines',
+ endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.stats)?.api.basePath,
+ },
+ stats_line: {
+ path: '/api/v1/lines/:id',
+ pathParams: [ 'id' as const ],
+ endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.stats)?.api.basePath,
+ },
+
+ // NAME SERVICE
+ addresses_lookup: {
+ path: '/api/v1/:chainId/addresses\\:lookup',
+ pathParams: [ 'chainId' as const ],
+ endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
+ filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const, 'protocols' as const ],
+ },
+ domain_info: {
+ path: '/api/v1/:chainId/domains/:name',
+ pathParams: [ 'chainId' as const, 'name' as const ],
+ endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
+ },
+ domain_events: {
+ path: '/api/v1/:chainId/domains/:name/events',
+ pathParams: [ 'chainId' as const, 'name' as const ],
+ endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
+ },
+ domains_lookup: {
+ path: '/api/v1/:chainId/domains\\:lookup',
+ pathParams: [ 'chainId' as const ],
+ endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
+ filterFields: [ 'name' as const, 'only_active' as const, 'protocols' as const ],
+ },
+ domain_protocols: {
+ path: '/api/v1/:chainId/protocols',
+ pathParams: [ 'chainId' as const ],
+ endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
+ },
+
+ // METADATA SERVICE & PUBLIC TAGS
+ address_metadata_info: {
+ path: '/api/v1/metadata',
+ endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
+ },
+ address_metadata_tag_search: {
+ path: '/api/v1/tags:search',
+ endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
+ },
+ address_metadata_tag_types: {
+ path: '/api/v1/public-tag-types',
+ endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
+ },
+ public_tag_application: {
+ path: '/api/v1/chains/:chainId/metadata-submissions/tag',
+ pathParams: [ 'chainId' as const ],
+ endpoint: getFeaturePayload(config.features.publicTagsSubmission)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.publicTagsSubmission)?.api.basePath,
+ },
+
+ // VISUALIZATION
+ visualize_sol2uml: {
+ path: '/api/v1/solidity\\:visualize-contracts',
+ endpoint: getFeaturePayload(config.features.sol2uml)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.sol2uml)?.api.basePath,
+ },
+
+ // MARKETPLACE
+ marketplace_dapps: {
+ path: '/api/v1/chains/:chainId/marketplace/dapps',
+ pathParams: [ 'chainId' as const ],
+ endpoint: marketplaceApi?.endpoint,
+ basePath: marketplaceApi?.basePath,
+ },
+ marketplace_dapp: {
+ path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId',
+ pathParams: [ 'chainId' as const, 'dappId' as const ],
+ endpoint: marketplaceApi?.endpoint,
+ basePath: marketplaceApi?.basePath,
+ },
+
+ // BLOCKS, TXS
+ blocks: {
+ path: '/api/v2/blocks',
+ filterFields: [ 'type' as const ],
+ },
+ block: {
+ path: '/api/v2/blocks/:height_or_hash',
+ pathParams: [ 'height_or_hash' as const ],
+ },
+ block_txs: {
+ path: '/api/v2/blocks/:height_or_hash/transactions',
+ pathParams: [ 'height_or_hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
+ block_withdrawals: {
+ path: '/api/v2/blocks/:height_or_hash/withdrawals',
+ pathParams: [ 'height_or_hash' as const ],
+ filterFields: [],
+ },
+ txs_stats: {
+ path: '/api/v2/transactions/stats',
+ },
+ txs_validated: {
+ path: '/api/v2/transactions',
+ filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
+ },
+ txs_pending: {
+ path: '/api/v2/transactions',
+ filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
+ },
+ txs_with_blobs: {
+ path: '/api/v2/transactions',
+ filterFields: [ 'type' as const ],
+ },
+ txs_watchlist: {
+ path: '/api/v2/transactions/watchlist',
+ filterFields: [ ],
+ },
+ txs_execution_node: {
+ path: '/api/v2/transactions/execution-node/:hash',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ ],
+ },
+ tx: {
+ path: '/api/v2/transactions/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ tx_internal_txs: {
+ path: '/api/v2/transactions/:hash/internal-transactions',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ ],
+ },
+ tx_logs: {
+ path: '/api/v2/transactions/:hash/logs',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ ],
+ },
+ tx_token_transfers: {
+ path: '/api/v2/transactions/:hash/token-transfers',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
+ tx_raw_trace: {
+ path: '/api/v2/transactions/:hash/raw-trace',
+ pathParams: [ 'hash' as const ],
+ },
+ tx_state_changes: {
+ path: '/api/v2/transactions/:hash/state-changes',
+ pathParams: [ 'hash' as const ],
+ filterFields: [],
+ },
+ tx_blobs: {
+ path: '/api/v2/transactions/:hash/blobs',
+ pathParams: [ 'hash' as const ],
+ },
+ tx_interpretation: {
+ path: '/api/v2/transactions/:hash/summary',
+ pathParams: [ 'hash' as const ],
+ },
+ withdrawals: {
+ path: '/api/v2/withdrawals',
+ filterFields: [],
+ },
+ withdrawals_counters: {
+ path: '/api/v2/withdrawals/counters',
+ },
+
+ // ADDRESSES
+ addresses: {
+ path: '/api/v2/addresses/',
+ filterFields: [ ],
+ },
+
+ // ADDRESS
+ address: {
+ path: '/api/v2/addresses/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ address_counters: {
+ path: '/api/v2/addresses/:hash/counters',
+ pathParams: [ 'hash' as const ],
+ },
+ address_tabs_counters: {
+ path: '/api/v2/addresses/:hash/tabs-counters',
+ pathParams: [ 'hash' as const ],
+ },
+ // this resource doesn't have pagination, so causing huge problems on some addresses page
+ // address_token_balances: {
+ // path: '/api/v2/addresses/:hash/token-balances',
+ // },
+ address_txs: {
+ path: '/api/v2/addresses/:hash/transactions',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'filter' as const ],
+ },
+ address_internal_txs: {
+ path: '/api/v2/addresses/:hash/internal-transactions',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'filter' as const ],
+ },
+ address_token_transfers: {
+ path: '/api/v2/addresses/:hash/token-transfers',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
+ },
+ address_blocks_validated: {
+ path: '/api/v2/addresses/:hash/blocks-validated',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ ],
+ },
+ address_coin_balance: {
+ path: '/api/v2/addresses/:hash/coin-balance-history',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ ],
+ },
+ address_coin_balance_chart: {
+ path: '/api/v2/addresses/:hash/coin-balance-history-by-day',
+ pathParams: [ 'hash' as const ],
+ },
+ address_logs: {
+ path: '/api/v2/addresses/:hash/logs',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ ],
+ },
+ address_tokens: {
+ path: '/api/v2/addresses/:hash/tokens',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
+ address_nfts: {
+ path: '/api/v2/addresses/:hash/nft',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
+ address_collections: {
+ path: '/api/v2/addresses/:hash/nft/collections',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
+ address_withdrawals: {
+ path: '/api/v2/addresses/:hash/withdrawals',
+ pathParams: [ 'hash' as const ],
+ filterFields: [],
+ },
+
+ // CONTRACT
+ contract: {
+ path: '/api/v2/smart-contracts/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ contract_verification_config: {
+ path: '/api/v2/smart-contracts/verification/config',
+ },
+ contract_verification_via: {
+ path: '/api/v2/smart-contracts/:hash/verification/via/:method',
+ pathParams: [ 'hash' as const, 'method' as const ],
+ },
+ contract_solidityscan_report: {
+ path: '/api/v2/smart-contracts/:hash/solidityscan-report',
+ pathParams: [ 'hash' as const ],
+ },
+ contract_security_audits: {
+ path: '/api/v2/smart-contracts/:hash/audit-reports',
+ pathParams: [ 'hash' as const ],
+ },
+
+ verified_contracts: {
+ path: '/api/v2/smart-contracts',
+ filterFields: [ 'q' as const, 'filter' as const ],
+ },
+ verified_contracts_counters: {
+ path: '/api/v2/smart-contracts/counters',
+ },
+
+ // TOKEN
+ token: {
+ path: '/api/v2/tokens/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ token_verified_info: {
+ path: '/api/v1/chains/:chainId/token-infos/:hash',
+ pathParams: [ 'chainId' as const, 'hash' as const ],
+ endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
+ basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
+ },
+ token_counters: {
+ path: '/api/v2/tokens/:hash/counters',
+ pathParams: [ 'hash' as const ],
+ },
+ token_holders: {
+ path: '/api/v2/tokens/:hash/holders',
+ pathParams: [ 'hash' as const ],
+ filterFields: [],
+ },
+ token_transfers: {
+ path: '/api/v2/tokens/:hash/transfers',
+ pathParams: [ 'hash' as const ],
+ filterFields: [],
+ },
+ token_inventory: {
+ path: '/api/v2/tokens/:hash/instances',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'holder_address_hash' as const ],
+ },
+ tokens: {
+ path: '/api/v2/tokens',
+ filterFields: [ 'q' as const, 'type' as const ],
+ },
+ tokens_bridged: {
+ path: '/api/v2/tokens/bridged',
+ filterFields: [ 'q' as const, 'chain_ids' as const ],
+ },
+
+ // TOKEN INSTANCE
+ token_instance: {
+ path: '/api/v2/tokens/:hash/instances/:id',
+ pathParams: [ 'hash' as const, 'id' as const ],
+ },
+ token_instance_transfers_count: {
+ path: '/api/v2/tokens/:hash/instances/:id/transfers-count',
+ pathParams: [ 'hash' as const, 'id' as const ],
+ },
+ token_instance_transfers: {
+ path: '/api/v2/tokens/:hash/instances/:id/transfers',
+ pathParams: [ 'hash' as const, 'id' as const ],
+ filterFields: [],
+ },
+ token_instance_holders: {
+ path: '/api/v2/tokens/:hash/instances/:id/holders',
+ pathParams: [ 'hash' as const, 'id' as const ],
+ filterFields: [],
+ },
+ token_instance_refresh_metadata: {
+ path: '/api/v2/tokens/:hash/instances/:id/refetch-metadata',
+ pathParams: [ 'hash' as const, 'id' as const ],
+ filterFields: [],
+ },
+
+ // APP STATS
+ stats: {
+ path: '/api/v2/stats',
+ headers: {
+ 'updated-gas-oracle': 'true',
+ },
+ },
+ stats_charts_txs: {
+ path: '/api/v2/stats/charts/transactions',
+ },
+ stats_charts_market: {
+ path: '/api/v2/stats/charts/market',
+ },
+ stats_charts_secondary_coin_price: {
+ path: '/api/v2/stats/charts/secondary-coin-market',
+ },
+
+ // HOMEPAGE
+ homepage_blocks: {
+ path: '/api/v2/main-page/blocks',
+ },
+ homepage_deposits: {
+ path: '/api/v2/main-page/optimism-deposits',
+ },
+ homepage_txs: {
+ path: '/api/v2/main-page/transactions',
+ },
+ homepage_zkevm_l2_batches: {
+ path: '/api/v2/main-page/zkevm/batches/confirmed',
+ },
+ homepage_txs_watchlist: {
+ path: '/api/v2/main-page/transactions/watchlist',
+ },
+ homepage_indexing_status: {
+ path: '/api/v2/main-page/indexing-status',
+ },
+ homepage_zkevm_latest_batch: {
+ path: '/api/v2/main-page/zkevm/batches/latest-number',
+ },
+ homepage_zksync_latest_batch: {
+ path: '/api/v2/main-page/zksync/batches/latest-number',
+ },
+
+ // SEARCH
+ quick_search: {
+ path: '/api/v2/search/quick',
+ filterFields: [ 'q' ],
+ },
+ search: {
+ path: '/api/v2/search',
+ filterFields: [ 'q' ],
+ },
+ search_check_redirect: {
+ path: '/api/v2/search/check-redirect',
+ },
+
+ // optimistic L2
+ optimistic_l2_deposits: {
+ path: '/api/v2/optimism/deposits',
+ filterFields: [],
+ },
+
+ optimistic_l2_deposits_count: {
+ path: '/api/v2/optimism/deposits/count',
+ },
+
+ optimistic_l2_withdrawals: {
+ path: '/api/v2/optimism/withdrawals',
+ filterFields: [],
+ },
+
+ optimistic_l2_withdrawals_count: {
+ path: '/api/v2/optimism/withdrawals/count',
+ },
+
+ optimistic_l2_output_roots: {
+ path: '/api/v2/optimism/output-roots',
+ filterFields: [],
+ },
+
+ optimistic_l2_output_roots_count: {
+ path: '/api/v2/optimism/output-roots/count',
+ },
+
+ optimistic_l2_txn_batches: {
+ path: '/api/v2/optimism/txn-batches',
+ filterFields: [],
+ },
+
+ optimistic_l2_txn_batches_count: {
+ path: '/api/v2/optimism/txn-batches/count',
+ },
+
+ optimistic_l2_dispute_games: {
+ path: '/api/v2/optimism/games',
+ filterFields: [],
+ },
+
+ optimistic_l2_dispute_games_count: {
+ path: '/api/v2/optimism/games/count',
+ },
+
+ // MUD worlds on optimism
+ mud_worlds: {
+ path: '/api/v2/mud/worlds',
+ filterFields: [],
+ },
+
+ address_mud_tables: {
+ path: '/api/v2/mud/worlds/:hash/tables',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'q' as const ],
+ },
+
+ address_mud_tables_count: {
+ path: '/api/v2/mud/worlds/:hash/tables/count',
+ pathParams: [ 'hash' as const ],
+ },
+
+ address_mud_records: {
+ path: '/api/v2/mud/worlds/:hash/tables/:table_id/records',
+ pathParams: [ 'hash' as const, 'table_id' as const ],
+ filterFields: [ 'filter_key0' as const, 'filter_key1' as const ],
+ },
+
+ address_mud_record: {
+ path: '/api/v2/mud/worlds/:hash/tables/:table_id/records/:record_id',
+ pathParams: [ 'hash' as const, 'table_id' as const, 'record_id' as const ],
+ },
+
+ // arbitrum L2
+ arbitrum_l2_messages: {
+ path: '/api/v2/arbitrum/messages/:direction',
+ pathParams: [ 'direction' as const ],
+ filterFields: [],
+ },
+
+ arbitrum_l2_messages_count: {
+ path: '/api/v2/arbitrum/messages/:direction/count',
+ pathParams: [ 'direction' as const ],
+ },
+
+ arbitrum_l2_txn_batches: {
+ path: '/api/v2/arbitrum/batches',
+ filterFields: [],
+ },
+
+ arbitrum_l2_txn_batches_count: {
+ path: '/api/v2/arbitrum/batches/count',
+ },
+
+ arbitrum_l2_txn_batch: {
+ path: '/api/v2/arbitrum/batches/:number',
+ pathParams: [ 'number' as const ],
+ },
+
+ arbitrum_l2_txn_batch_txs: {
+ path: '/api/v2/transactions/arbitrum-batch/:number',
+ pathParams: [ 'number' as const ],
+ filterFields: [],
+ },
+
+ arbitrum_l2_txn_batch_blocks: {
+ path: '/api/v2/blocks/arbitrum-batch/:number',
+ pathParams: [ 'number' as const ],
+ filterFields: [],
+ },
+
+ // zkEvm L2
+ zkevm_l2_deposits: {
+ path: '/api/v2/zkevm/deposits',
+ filterFields: [],
+ },
+
+ zkevm_l2_deposits_count: {
+ path: '/api/v2/zkevm/deposits/count',
+ },
+
+ zkevm_l2_withdrawals: {
+ path: '/api/v2/zkevm/withdrawals',
+ filterFields: [],
+ },
+
+ zkevm_l2_withdrawals_count: {
+ path: '/api/v2/zkevm/withdrawals/count',
+ },
+
+ zkevm_l2_txn_batches: {
+ path: '/api/v2/zkevm/batches',
+ filterFields: [],
+ },
+
+ zkevm_l2_txn_batches_count: {
+ path: '/api/v2/zkevm/batches/count',
+ },
+
+ zkevm_l2_txn_batch: {
+ path: '/api/v2/zkevm/batches/:number',
+ pathParams: [ 'number' as const ],
+ },
+
+ zkevm_l2_txn_batch_txs: {
+ path: '/api/v2/transactions/zkevm-batch/:number',
+ pathParams: [ 'number' as const ],
+ filterFields: [],
+ },
+
+ // zkSync L2
+ zksync_l2_txn_batches: {
+ path: '/api/v2/zksync/batches',
+ filterFields: [],
+ },
+
+ zksync_l2_txn_batches_count: {
+ path: '/api/v2/zksync/batches/count',
+ },
+
+ zksync_l2_txn_batch: {
+ path: '/api/v2/zksync/batches/:number',
+ pathParams: [ 'number' as const ],
+ },
+
+ zksync_l2_txn_batch_txs: {
+ path: '/api/v2/transactions/zksync-batch/:number',
+ pathParams: [ 'number' as const ],
+ filterFields: [],
+ },
+
+ // SHIBARIUM L2
+ shibarium_deposits: {
+ path: '/api/v2/shibarium/deposits',
+ filterFields: [],
+ },
+
+ shibarium_deposits_count: {
+ path: '/api/v2/shibarium/deposits/count',
+ },
+
+ shibarium_withdrawals: {
+ path: '/api/v2/shibarium/withdrawals',
+ filterFields: [],
+ },
+
+ shibarium_withdrawals_count: {
+ path: '/api/v2/shibarium/withdrawals/count',
+ },
+
+ // NOVES-FI
+ noves_transaction: {
+ path: '/api/v2/proxy/noves-fi/transactions/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ noves_address_history: {
+ path: '/api/v2/proxy/noves-fi/addresses/:address/transactions',
+ pathParams: [ 'address' as const ],
+ filterFields: [],
+ },
+ noves_describe_txs: {
+ path: '/api/v2/proxy/noves-fi/transaction-descriptions',
+ },
+
+ // USER OPS
+ user_ops: {
+ path: '/api/v2/proxy/account-abstraction/operations',
+ filterFields: [ 'transaction_hash' as const, 'sender' as const ],
+ },
+ user_op: {
+ path: '/api/v2/proxy/account-abstraction/operations/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ user_ops_account: {
+ path: '/api/v2/proxy/account-abstraction/accounts/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+ user_op_interpretation: {
+ path: '/api/v2/proxy/account-abstraction/operations/:hash/summary',
+ pathParams: [ 'hash' as const ],
+ },
+
+ // VALIDATORS
+ validators: {
+ path: '/api/v2/validators/:chainType',
+ pathParams: [ 'chainType' as const ],
+ filterFields: [ 'address_hash' as const, 'state_filter' as const ],
+ },
+ validators_counters: {
+ path: '/api/v2/validators/:chainType/counters',
+ pathParams: [ 'chainType' as const ],
+ },
+
+ // BLOBS
+ blob: {
+ path: '/api/v2/blobs/:hash',
+ pathParams: [ 'hash' as const ],
+ },
+
+ // CONFIGS
+ config_backend_version: {
+ path: '/api/v2/config/backend-version',
+ },
+
+ // CSV EXPORT
+ csv_export_token_holders: {
+ path: '/api/v2/tokens/:hash/holders/csv',
+ pathParams: [ 'hash' as const ],
+ },
+
+ // OTHER
+ api_v2_key: {
+ path: '/api/v2/key',
+ },
+
+ // API V1
+ csv_export_txs: {
+ path: '/api/v1/transactions-csv',
+ },
+ csv_export_internal_txs: {
+ path: '/api/v1/internal-transactions-csv',
+ },
+ csv_export_token_transfers: {
+ path: '/api/v1/token-transfers-csv',
+ },
+ csv_export_logs: {
+ path: '/api/v1/logs-csv',
+ },
+ graphql: {
+ path: '/api/v1/graphql',
+ },
+ block_countdown: {
+ path: '/api',
+ },
+};
+
+export type ResourceName = keyof typeof RESOURCES;
+
+type ResourcePathMap = {
+ [K in ResourceName]: typeof RESOURCES[K]['path']
+}
+export type ResourcePath = ResourcePathMap[keyof ResourcePathMap]
+
+export type ResourceFiltersKey = typeof RESOURCES[R] extends {filterFields: Array} ?
+ ArrayElement :
+ never;
+
+export const resourceKey = (x: keyof typeof RESOURCES) => x;
+
+type ResourcePathParamName =
+ typeof RESOURCES[Resource] extends { pathParams: Array } ?
+ ArrayElement :
+ string;
+
+export type ResourcePathParams = typeof RESOURCES[Resource] extends { pathParams: Array } ?
+ Record, string | undefined> :
+ never;
+
+export interface ResourceError {
+ payload?: T;
+ status: Response['status'];
+ statusText: Response['statusText'];
+}
+
+export type ResourceErrorAccount = ResourceError<{ errors: T }>
+
+export type PaginatedResources = 'blocks' | 'block_txs' |
+'txs_validated' | 'txs_pending' | 'txs_with_blobs' | 'txs_watchlist' | 'txs_execution_node' |
+'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | 'tx_blobs' |
+'addresses' |
+'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
+'search' |
+'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
+'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
+'token_instance_transfers' | 'token_instance_holders' |
+'verified_contracts' |
+'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
+'optimistic_l2_dispute_games' |
+'mud_worlds'| 'address_mud_tables' | 'address_mud_records' |
+'shibarium_deposits' | 'shibarium_withdrawals' |
+'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' |
+'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
+'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
+'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
+'watchlist' | 'private_tags_address' | 'private_tags_tx' |
+'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history';
+
+export type PaginatedResponse = ResourcePayload;
+
+/* eslint-disable @typescript-eslint/indent */
+// !!! IMPORTANT !!!
+// Don't add any new types here because TypeScript cannot handle it properly
+// use ResourcePayloadB instead
+export type ResourcePayloadA =
+Q extends 'user_info' ? UserInfo :
+Q extends 'custom_abi' ? CustomAbis :
+Q extends 'private_tags_address' ? AddressTagsResponse :
+Q extends 'private_tags_tx' ? TransactionTagsResponse :
+Q extends 'api_keys' ? ApiKeys :
+Q extends 'watchlist' ? WatchlistResponse :
+Q extends 'verified_addresses' ? VerifiedAddressResponse :
+Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
+Q extends 'token_info_applications' ? TokenInfoApplications :
+Q extends 'stats' ? HomeStats :
+Q extends 'stats_charts_txs' ? ChartTransactionResponse :
+Q extends 'stats_charts_market' ? ChartMarketResponse :
+Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse :
+Q extends 'homepage_blocks' ? Array :
+Q extends 'homepage_txs' ? Array :
+Q extends 'homepage_txs_watchlist' ? Array :
+Q extends 'homepage_deposits' ? Array :
+Q extends 'homepage_zkevm_l2_batches' ? { items: Array } :
+Q extends 'homepage_indexing_status' ? IndexingStatus :
+Q extends 'homepage_zkevm_latest_batch' ? number :
+Q extends 'homepage_zksync_latest_batch' ? number :
+Q extends 'stats_counters' ? stats.Counters :
+Q extends 'stats_lines' ? stats.LineCharts :
+Q extends 'stats_line' ? stats.LineChart :
+Q extends 'blocks' ? BlocksResponse :
+Q extends 'block' ? Block :
+Q extends 'block_countdown' ? BlockCountdownResponse :
+Q extends 'block_txs' ? BlockTransactionsResponse :
+Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
+Q extends 'txs_stats' ? TransactionsStats :
+Q extends 'txs_validated' ? TransactionsResponseValidated :
+Q extends 'txs_pending' ? TransactionsResponsePending :
+Q extends 'txs_with_blobs' ? TransactionsResponseWithBlobs :
+Q extends 'txs_watchlist' ? TransactionsResponseWatchlist :
+Q extends 'txs_execution_node' ? TransactionsResponseValidated :
+Q extends 'tx' ? Transaction :
+Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
+Q extends 'tx_logs' ? LogsResponseTx :
+Q extends 'tx_token_transfers' ? TokenTransferResponse :
+Q extends 'tx_raw_trace' ? RawTracesResponse :
+Q extends 'tx_state_changes' ? TxStateChanges :
+Q extends 'tx_blobs' ? TxBlobs :
+Q extends 'tx_interpretation' ? TxInterpretationResponse :
+Q extends 'addresses' ? AddressesResponse :
+Q extends 'address' ? Address :
+Q extends 'address_counters' ? AddressCounters :
+Q extends 'address_tabs_counters' ? AddressTabsCounters :
+Q extends 'address_txs' ? AddressTransactionsResponse :
+Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
+Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
+Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
+Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
+Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChartOld | AddressCoinBalanceHistoryChart :
+Q extends 'address_logs' ? LogsResponseAddress :
+Q extends 'address_tokens' ? AddressTokensResponse :
+Q extends 'address_nfts' ? AddressNFTsResponse :
+Q extends 'address_collections' ? AddressCollectionsResponse :
+Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
+Q extends 'token' ? TokenInfo :
+Q extends 'token_verified_info' ? TokenVerifiedInfo :
+Q extends 'token_counters' ? TokenCounters :
+Q extends 'token_transfers' ? TokenTransferResponse :
+Q extends 'token_holders' ? TokenHolders :
+Q extends 'token_instance' ? TokenInstance :
+Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
+Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
+Q extends 'token_instance_holders' ? TokenHolders :
+Q extends 'token_inventory' ? TokenInventoryResponse :
+Q extends 'tokens' ? TokensResponse :
+Q extends 'tokens_bridged' ? TokensResponse :
+Q extends 'quick_search' ? Array :
+Q extends 'search' ? SearchResult :
+Q extends 'search_check_redirect' ? SearchRedirectResult :
+Q extends 'contract' ? SmartContract :
+Q extends 'contract_solidityscan_report' ? SolidityscanReport :
+Q extends 'verified_contracts' ? VerifiedContractsResponse :
+Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
+Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse :
+Q extends 'contract_verification_config' ? SmartContractVerificationConfigRaw :
+Q extends 'withdrawals' ? WithdrawalsResponse :
+Q extends 'withdrawals_counters' ? WithdrawalsCounters :
+Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
+Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
+Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
+Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
+Q extends 'optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse :
+Q extends 'optimistic_l2_output_roots_count' ? number :
+Q extends 'optimistic_l2_withdrawals_count' ? number :
+Q extends 'optimistic_l2_deposits_count' ? number :
+Q extends 'optimistic_l2_txn_batches_count' ? number :
+Q extends 'optimistic_l2_dispute_games_count' ? number :
+never;
+// !!! IMPORTANT !!!
+// See comment above
+/* eslint-enable @typescript-eslint/indent */
+
+/* eslint-disable @typescript-eslint/indent */
+export type ResourcePayloadB =
+Q extends 'config_backend_version' ? BackendVersionConfig :
+Q extends 'address_metadata_info' ? AddressMetadataInfo :
+Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse :
+Q extends 'blob' ? Blob :
+Q extends 'marketplace_dapps' ? Array :
+Q extends 'marketplace_dapp' ? MarketplaceAppOverview :
+Q extends 'validators' ? ValidatorsResponse :
+Q extends 'validators_counters' ? ValidatorsCountersResponse :
+Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
+Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
+Q extends 'shibarium_withdrawals_count' ? number :
+Q extends 'shibarium_deposits_count' ? number :
+Q extends 'arbitrum_l2_messages' ? ArbitrumL2MessagesResponse :
+Q extends 'arbitrum_l2_messages_count' ? number :
+Q extends 'arbitrum_l2_txn_batches' ? ArbitrumL2TxnBatchesResponse :
+Q extends 'arbitrum_l2_txn_batches_count' ? number :
+Q extends 'arbitrum_l2_txn_batch' ? ArbitrumL2TxnBatch :
+Q extends 'arbitrum_l2_txn_batch_txs' ? ArbitrumL2BatchTxs :
+Q extends 'arbitrum_l2_txn_batch_blocks' ? ArbitrumL2BatchBlocks :
+Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse :
+Q extends 'zkevm_l2_deposits_count' ? number :
+Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse :
+Q extends 'zkevm_l2_withdrawals_count' ? number :
+Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse :
+Q extends 'zkevm_l2_txn_batches_count' ? number :
+Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
+Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
+Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse :
+Q extends 'zksync_l2_txn_batches_count' ? number :
+Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch :
+Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs :
+Q extends 'contract_security_audits' ? SmartContractSecurityAudits :
+Q extends 'addresses_lookup' ? bens.LookupAddressResponse :
+Q extends 'domain_info' ? bens.DetailedDomain :
+Q extends 'domain_events' ? bens.ListDomainEventsResponse :
+Q extends 'domains_lookup' ? bens.LookupDomainNameResponse :
+Q extends 'domain_protocols' ? bens.GetProtocolsResponse :
+Q extends 'user_ops' ? UserOpsResponse :
+Q extends 'user_op' ? UserOp :
+Q extends 'user_ops_account' ? UserOpsAccount :
+Q extends 'user_op_interpretation'? TxInterpretationResponse :
+Q extends 'noves_transaction' ? NovesResponseData :
+Q extends 'noves_address_history' ? NovesAccountHistoryResponse :
+Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse :
+Q extends 'mud_worlds' ? MudWorldsResponse :
+Q extends 'address_mud_tables' ? AddressMudTables :
+Q extends 'address_mud_tables_count' ? number :
+Q extends 'address_mud_records' ? AddressMudRecords :
+Q extends 'address_mud_record' ? AddressMudRecord :
+never;
+/* eslint-enable @typescript-eslint/indent */
+
+export type ResourcePayload = ResourcePayloadA | ResourcePayloadB;
+export type PaginatedResponseItems = Q extends PaginatedResources ? ResourcePayloadA['items'] | ResourcePayloadB['items'] : never;
+export type PaginatedResponseNextPageParams = Q extends PaginatedResources ?
+ ResourcePayloadA['next_page_params'] | ResourcePayloadB['next_page_params'] :
+ never;
+
+/* eslint-disable @typescript-eslint/indent */
+export type PaginationFilters =
+Q extends 'blocks' ? BlockFilters :
+Q extends 'block_txs' ? TTxsWithBlobsFilters :
+Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
+Q extends 'txs_with_blobs' ? TTxsWithBlobsFilters :
+Q extends 'tx_token_transfers' ? TokenTransferFilters :
+Q extends 'token_transfers' ? TokenTransferFilters :
+Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
+Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
+Q extends 'address_tokens' ? AddressTokensFilter :
+Q extends 'address_nfts' ? AddressNFTTokensFilter :
+Q extends 'address_collections' ? AddressNFTTokensFilter :
+Q extends 'search' ? SearchResultFilters :
+Q extends 'token_inventory' ? TokenInventoryFilters :
+Q extends 'tokens' ? TokensFilters :
+Q extends 'tokens_bridged' ? TokensBridgedFilters :
+Q extends 'verified_contracts' ? VerifiedContractsFilters :
+Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
+Q extends 'domains_lookup' ? EnsDomainLookupFilters :
+Q extends 'user_ops' ? UserOpsFilters :
+Q extends 'validators' ? ValidatorsFilters :
+Q extends 'address_mud_tables' ? AddressMudTablesFilter :
+Q extends 'address_mud_records' ? AddressMudRecordsFilter :
+never;
+/* eslint-enable @typescript-eslint/indent */
+
+/* eslint-disable @typescript-eslint/indent */
+export type PaginationSorting =
+Q extends 'tokens' ? TokensSorting :
+Q extends 'tokens_bridged' ? TokensSorting :
+Q extends 'verified_contracts' ? VerifiedContractsSorting :
+Q extends 'address_txs' ? TransactionsSorting :
+Q extends 'addresses_lookup' ? EnsLookupSorting :
+Q extends 'domains_lookup' ? EnsLookupSorting :
+Q extends 'validators' ? ValidatorsSorting :
+Q extends 'address_mud_records' ? AddressMudRecordsSorting :
+never;
+/* eslint-enable @typescript-eslint/indent */
diff --git a/lib/api/useApiFetch.tsx b/lib/api/useApiFetch.tsx
new file mode 100644
index 0000000000..85da773ab1
--- /dev/null
+++ b/lib/api/useApiFetch.tsx
@@ -0,0 +1,65 @@
+import { useQueryClient } from '@tanstack/react-query';
+import _omit from 'lodash/omit';
+import _pickBy from 'lodash/pickBy';
+import React from 'react';
+
+import type { CsrfData } from 'types/client/account';
+
+import config from 'configs/app';
+import isBodyAllowed from 'lib/api/isBodyAllowed';
+import isNeedProxy from 'lib/api/isNeedProxy';
+import { getResourceKey } from 'lib/api/useApiQuery';
+import * as cookies from 'lib/cookies';
+import type { Params as FetchParams } from 'lib/hooks/useFetch';
+import useFetch from 'lib/hooks/useFetch';
+
+import buildUrl from './buildUrl';
+import { RESOURCES } from './resources';
+import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
+
+export interface Params {
+ pathParams?: ResourcePathParams;
+ queryParams?: Record | number | boolean | undefined>;
+ fetchParams?: Pick;
+}
+
+export default function useApiFetch() {
+ const fetch = useFetch();
+ const queryClient = useQueryClient();
+ const { token: csrfToken } = queryClient.getQueryData(getResourceKey('csrf')) || {};
+
+ return React.useCallback((
+ resourceName: R,
+ { pathParams, queryParams, fetchParams }: Params = {},
+ ) => {
+ const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
+
+ const resource: ApiResource = RESOURCES[resourceName];
+ const url = buildUrl(resourceName, pathParams, queryParams);
+ const withBody = isBodyAllowed(fetchParams?.method);
+ const headers = _pickBy({
+ 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
+ Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
+ 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
+ ...resource.headers,
+ ...fetchParams?.headers,
+ }, Boolean) as HeadersInit;
+
+ return fetch(
+ url,
+ {
+ // as of today, we use cookies only
+ // for user authentication in My account
+ // for API rate-limits (cannot use in the condition though, but we agreed with devops team that should not be an issue)
+ // change condition here if something is changed
+ credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
+ headers,
+ ..._omit(fetchParams, 'headers'),
+ },
+ {
+ resource: resource.path,
+ omitSentryErrorLog: true, // disable logging of API errors to Sentry
+ },
+ );
+ }, [ fetch, csrfToken ]);
+}
diff --git a/lib/api/useApiQuery.tsx b/lib/api/useApiQuery.tsx
new file mode 100644
index 0000000000..91b2ab6ae5
--- /dev/null
+++ b/lib/api/useApiQuery.tsx
@@ -0,0 +1,37 @@
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
+
+import type { ResourceError, ResourceName, ResourcePayload } from './resources';
+import type { Params as ApiFetchParams } from './useApiFetch';
+import useApiFetch from './useApiFetch';
+
+export interface Params> extends ApiFetchParams {
+ queryOptions?: Omit, ResourceError, D>, 'queryKey' | 'queryFn'>;
+}
+
+export function getResourceKey(resource: R, { pathParams, queryParams }: Params = {}) {
+ if (pathParams || queryParams) {
+ return [ resource, { ...pathParams, ...queryParams } ];
+ }
+
+ return [ resource ];
+}
+
+export default function useApiQuery>(
+ resource: R,
+ { queryOptions, pathParams, queryParams, fetchParams }: Params = {},
+) {
+ const apiFetch = useApiFetch();
+
+ return useQuery, ResourceError, D>({
+ // eslint-disable-next-line @tanstack/query/exhaustive-deps
+ queryKey: getResourceKey(resource, { pathParams, queryParams }),
+ queryFn: async() => {
+ // all errors and error typing is handled by react-query
+ // so error response will never go to the data
+ // that's why we are safe here to do type conversion "as Promise>"
+ return apiFetch(resource, { pathParams, queryParams, fetchParams }) as Promise>;
+ },
+ ...queryOptions,
+ });
+}
diff --git a/lib/api/useQueryClientConfig.tsx b/lib/api/useQueryClientConfig.tsx
new file mode 100644
index 0000000000..b7a8fd2550
--- /dev/null
+++ b/lib/api/useQueryClientConfig.tsx
@@ -0,0 +1,33 @@
+import { QueryClient } from '@tanstack/react-query';
+import React from 'react';
+
+import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
+import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
+
+export const retry = (failureCount: number, error: unknown) => {
+ const errorPayload = getErrorObjPayload<{ status: number }>(error);
+ const status = errorPayload?.status || getErrorObjStatusCode(error);
+ if (status && status >= 400 && status < 500) {
+ // don't do retry for client error responses
+ return false;
+ }
+ return failureCount < 2;
+};
+
+export default function useQueryClientConfig() {
+ const [ queryClient ] = React.useState(() => new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ retry,
+ throwOnError: (error) => {
+ const status = getErrorObjStatusCode(error);
+ // don't catch error for "Too many requests" response
+ return status === 429;
+ },
+ },
+ },
+ }));
+
+ return queryClient;
+}
diff --git a/lib/bigint/compareBns.ts b/lib/bigint/compareBns.ts
new file mode 100644
index 0000000000..34a86239c4
--- /dev/null
+++ b/lib/bigint/compareBns.ts
@@ -0,0 +1,13 @@
+import BigNumber from 'bignumber.js';
+
+export default function compareBns(value1: string | number, value2: string | number) {
+ const value1Bn = new BigNumber(value1);
+ const value2Bn = new BigNumber(value2);
+ if (value1Bn.isGreaterThan(value2Bn)) {
+ return 1;
+ }
+ if (value1Bn.isLessThan(value2Bn)) {
+ return -1;
+ }
+ return 0;
+}
diff --git a/lib/bigint/sumBnReducer.ts b/lib/bigint/sumBnReducer.ts
new file mode 100644
index 0000000000..6057853a96
--- /dev/null
+++ b/lib/bigint/sumBnReducer.ts
@@ -0,0 +1,5 @@
+import type BigNumber from 'bignumber.js';
+
+export default function sumBnReducer(result: BigNumber, item: BigNumber) {
+ return result.plus(item);
+}
diff --git a/lib/blob/guessDataType.ts b/lib/blob/guessDataType.ts
new file mode 100644
index 0000000000..fb409019e3
--- /dev/null
+++ b/lib/blob/guessDataType.ts
@@ -0,0 +1,12 @@
+import filetype from 'magic-bytes.js';
+
+import hexToBytes from 'lib/hexToBytes';
+
+import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes';
+
+export default function guessDataType(data: string) {
+ const bytes = new Uint8Array(hexToBytes(data));
+ const filteredBytes = removeNonSignificantZeroBytes(bytes);
+
+ return filetype(filteredBytes)[0];
+}
diff --git a/lib/blob/index.ts b/lib/blob/index.ts
new file mode 100644
index 0000000000..ab178e8231
--- /dev/null
+++ b/lib/blob/index.ts
@@ -0,0 +1 @@
+export { default as guessDataType } from './guessDataType';
diff --git a/lib/blob/removeNonSignificantZeroBytes.ts b/lib/blob/removeNonSignificantZeroBytes.ts
new file mode 100644
index 0000000000..9b25287478
--- /dev/null
+++ b/lib/blob/removeNonSignificantZeroBytes.ts
@@ -0,0 +1,20 @@
+export default function removeNonSignificantZeroBytes(bytes: Uint8Array) {
+ return shouldRemoveBytes(bytes) ? bytes.filter((item, index) => index % 32) : bytes;
+}
+
+// check if every 0, 32, 64, etc byte is 0 in the provided array
+function shouldRemoveBytes(bytes: Uint8Array) {
+ let result = true;
+
+ for (let index = 0; index < bytes.length; index += 32) {
+ const element = bytes[index];
+ if (element === 0) {
+ continue;
+ } else {
+ result = false;
+ break;
+ }
+ }
+
+ return result;
+}
diff --git a/lib/block/getBlockReward.ts b/lib/block/getBlockReward.ts
new file mode 100644
index 0000000000..3c00ac8d19
--- /dev/null
+++ b/lib/block/getBlockReward.ts
@@ -0,0 +1,18 @@
+import BigNumber from 'bignumber.js';
+
+import type { Block } from 'types/api/block';
+
+export default function getBlockReward(block: Block) {
+ const txFees = BigNumber(block.tx_fees || 0);
+ const burntFees = BigNumber(block.burnt_fees || 0);
+ const minerReward = block.rewards?.find(({ type }) => type === 'Miner Reward' || type === 'Validator Reward')?.reward;
+ const totalReward = BigNumber(minerReward || 0);
+ const staticReward = totalReward.minus(txFees).plus(burntFees);
+
+ return {
+ totalReward,
+ staticReward,
+ txFees,
+ burntFees,
+ };
+}
diff --git a/lib/block/getBlockTotalReward.ts b/lib/block/getBlockTotalReward.ts
new file mode 100644
index 0000000000..3426b08501
--- /dev/null
+++ b/lib/block/getBlockTotalReward.ts
@@ -0,0 +1,13 @@
+import BigNumber from 'bignumber.js';
+
+import type { Block } from 'types/api/block';
+
+import { WEI, ZERO } from 'lib/consts';
+
+export default function getBlockTotalReward(block: Block) {
+ const totalReward = block.rewards
+ ?.map(({ reward }) => BigNumber(reward))
+ .reduce((result, item) => result.plus(item), ZERO) || ZERO;
+
+ return totalReward.div(WEI);
+}
diff --git a/lib/bytesToBase64.ts b/lib/bytesToBase64.ts
new file mode 100644
index 0000000000..60b23ad437
--- /dev/null
+++ b/lib/bytesToBase64.ts
@@ -0,0 +1,10 @@
+export default function bytesToBase64(bytes: Uint8Array) {
+ let binary = '';
+ for (const byte of bytes) {
+ binary += String.fromCharCode(byte);
+ }
+
+ const base64String = btoa(binary);
+
+ return base64String;
+}
diff --git a/lib/capitalizeFirstLetter.ts b/lib/capitalizeFirstLetter.ts
new file mode 100644
index 0000000000..ae054d9423
--- /dev/null
+++ b/lib/capitalizeFirstLetter.ts
@@ -0,0 +1,7 @@
+export default function capitalizeFirstLetter(text: string) {
+ if (!text || !text.length) {
+ return '';
+ }
+
+ return text.charAt(0).toUpperCase() + text.slice(1);
+}
diff --git a/lib/consts.ts b/lib/consts.ts
new file mode 100644
index 0000000000..10f889ad0b
--- /dev/null
+++ b/lib/consts.ts
@@ -0,0 +1,19 @@
+import BigNumber from 'bignumber.js';
+
+export const WEI = new BigNumber(10 ** 18);
+export const GWEI = new BigNumber(10 ** 9);
+export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
+export const ZERO = new BigNumber(0);
+
+export const SECOND = 1_000;
+export const MINUTE = 60 * SECOND;
+export const HOUR = 60 * MINUTE;
+export const DAY = 24 * HOUR;
+export const WEEK = 7 * DAY;
+export const MONTH = 30 * DAY;
+export const YEAR = 365 * DAY;
+
+export const Kb = 1_000;
+export const Mb = 1_000 * Kb;
+
+export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
diff --git a/lib/contexts/addressHighlight.tsx b/lib/contexts/addressHighlight.tsx
new file mode 100644
index 0000000000..085e676ecc
--- /dev/null
+++ b/lib/contexts/addressHighlight.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+
+interface AddressHighlightProviderProps {
+ children: React.ReactNode;
+}
+
+interface TAddressHighlightContext {
+ onMouseEnter: (event: React.MouseEvent) => void;
+ onMouseLeave: (event: React.MouseEvent) => void;
+}
+
+export const AddressHighlightContext = React.createContext(null);
+
+export function AddressHighlightProvider({ children }: AddressHighlightProviderProps) {
+ const timeoutId = React.useRef(null);
+ const hashRef = React.useRef(null);
+
+ const onMouseEnter = React.useCallback((event: React.MouseEvent) => {
+ const hash = event.currentTarget.getAttribute('data-hash');
+ if (hash) {
+ hashRef.current = hash;
+ timeoutId.current = window.setTimeout(() => {
+ // for better performance we update DOM-nodes directly bypassing React reconciliation
+ const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`);
+ for (const node of nodes) {
+ node.classList.add('address-entity_highlighted');
+ }
+ }, 100);
+ }
+ }, []);
+
+ const onMouseLeave = React.useCallback(() => {
+ if (hashRef.current) {
+ const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`);
+ for (const node of nodes) {
+ node.classList.remove('address-entity_highlighted');
+ }
+ hashRef.current = null;
+ }
+ typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current);
+ }, []);
+
+ const value = React.useMemo(() => {
+ return {
+ onMouseEnter,
+ onMouseLeave,
+ };
+ }, [ onMouseEnter, onMouseLeave ]);
+
+ React.useEffect(() => {
+ return () => {
+ typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current);
+ };
+ }, []);
+
+ return (
+
+ { children }
+
+ );
+}
+
+export function useAddressHighlightContext(disabled?: boolean) {
+ const context = React.useContext(AddressHighlightContext);
+ if (context === undefined || disabled) {
+ return null;
+ }
+ return context;
+}
diff --git a/lib/contexts/app.tsx b/lib/contexts/app.tsx
new file mode 100644
index 0000000000..dbeceace4d
--- /dev/null
+++ b/lib/contexts/app.tsx
@@ -0,0 +1,29 @@
+import React, { createContext, useContext } from 'react';
+
+import type { Route } from 'nextjs-routes';
+import type { Props as PageProps } from 'nextjs/getServerSideProps';
+
+type Props = {
+ children: React.ReactNode;
+ pageProps: PageProps;
+}
+
+const AppContext = createContext({
+ cookies: '',
+ referrer: '',
+ query: {},
+ adBannerProvider: null,
+ apiData: null,
+});
+
+export function AppContextProvider({ children, pageProps }: Props) {
+ return (
+
+ { children }
+
+ );
+}
+
+export function useAppContext() {
+ return useContext>(AppContext);
+}
diff --git a/lib/contexts/chakra.tsx b/lib/contexts/chakra.tsx
new file mode 100644
index 0000000000..d009d3b5f0
--- /dev/null
+++ b/lib/contexts/chakra.tsx
@@ -0,0 +1,26 @@
+import {
+ ChakraProvider as ChakraProviderDefault,
+ cookieStorageManagerSSR,
+ localStorageManager,
+} from '@chakra-ui/react';
+import type { ChakraProviderProps } from '@chakra-ui/react';
+import React from 'react';
+
+import theme from 'theme';
+
+interface Props extends ChakraProviderProps {
+ cookies?: string;
+}
+
+export function ChakraProvider({ cookies, children }: Props) {
+ const colorModeManager =
+ typeof cookies === 'string' ?
+ cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
+ localStorageManager;
+
+ return (
+
+ { children }
+
+ );
+}
diff --git a/lib/contexts/marketplace.tsx b/lib/contexts/marketplace.tsx
new file mode 100644
index 0000000000..2aba76e39b
--- /dev/null
+++ b/lib/contexts/marketplace.tsx
@@ -0,0 +1,48 @@
+import { useRouter } from 'next/router';
+import React, { createContext, useContext, useEffect, useState, useMemo } from 'react';
+
+type Props = {
+ children: React.ReactNode;
+}
+
+type TMarketplaceContext = {
+ isAutoConnectDisabled: boolean;
+ setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void;
+}
+
+const MarketplaceContext = createContext({
+ isAutoConnectDisabled: false,
+ setIsAutoConnectDisabled: () => {},
+});
+
+export function MarketplaceContextProvider({ children }: Props) {
+ const router = useRouter();
+ const [ isAutoConnectDisabled, setIsAutoConnectDisabled ] = useState(false);
+
+ useEffect(() => {
+ const handleRouteChange = () => {
+ setIsAutoConnectDisabled(false);
+ };
+
+ router.events.on('routeChangeStart', handleRouteChange);
+
+ return () => {
+ router.events.off('routeChangeStart', handleRouteChange);
+ };
+ }, [ router.events ]);
+
+ const value = useMemo(() => ({
+ isAutoConnectDisabled,
+ setIsAutoConnectDisabled,
+ }), [ isAutoConnectDisabled, setIsAutoConnectDisabled ]);
+
+ return (
+
+ { children }
+
+ );
+}
+
+export function useMarketplaceContext() {
+ return useContext(MarketplaceContext);
+}
diff --git a/lib/contexts/scrollDirection.tsx b/lib/contexts/scrollDirection.tsx
new file mode 100644
index 0000000000..154b5438b4
--- /dev/null
+++ b/lib/contexts/scrollDirection.tsx
@@ -0,0 +1,58 @@
+import clamp from 'lodash/clamp';
+import throttle from 'lodash/throttle';
+import React from 'react';
+
+const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null);
+import isBrowser from 'lib/isBrowser';
+
+const SCROLL_DIFF_THRESHOLD = 20;
+
+type Directions = 'up' | 'down';
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export function ScrollDirectionProvider({ children }: Props) {
+ const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0);
+ const [ scrollDirection, setDirection ] = React.useState(null);
+
+ const handleScroll = React.useCallback(() => {
+ const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight);
+ const scrollDiff = currentScrollPosition - prevScrollPosition.current;
+
+ if (window.pageYOffset === 0) {
+ setDirection(null);
+ } else if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) {
+ setDirection(scrollDiff < 0 ? 'up' : 'down');
+ }
+
+ prevScrollPosition.current = currentScrollPosition;
+ }, [ ]);
+
+ React.useEffect(() => {
+ const throttledHandleScroll = throttle(handleScroll, 300);
+
+ window.addEventListener('scroll', throttledHandleScroll);
+
+ return () => {
+ window.removeEventListener('scroll', throttledHandleScroll);
+ };
+ // replicate componentDidMount
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+ { children }
+
+ );
+}
+
+export function useScrollDirection() {
+ const context = React.useContext(ScrollDirectionContext);
+ if (context === undefined) {
+ throw new Error('useScrollDirection must be used within a ScrollDirectionProvider');
+ }
+ return context;
+}
diff --git a/lib/contracts/licenses.ts b/lib/contracts/licenses.ts
new file mode 100644
index 0000000000..123149e294
--- /dev/null
+++ b/lib/contracts/licenses.ts
@@ -0,0 +1,88 @@
+import type { ContractLicense } from 'types/client/contract';
+
+export const CONTRACT_LICENSES: Array = [
+ {
+ type: 'none',
+ label: 'None',
+ title: 'No License',
+ url: 'https://choosealicense.com/no-permission/',
+ },
+ {
+ type: 'unlicense',
+ label: 'Unlicense',
+ title: 'The Unlicense',
+ url: 'https://choosealicense.com/licenses/unlicense/',
+ },
+ {
+ type: 'mit',
+ label: 'MIT',
+ title: 'MIT License',
+ url: 'https://choosealicense.com/licenses/mit/',
+ },
+ {
+ type: 'gnu_gpl_v2',
+ label: 'GNU GPLv2',
+ title: 'GNU General Public License v2.0',
+ url: 'https://choosealicense.com/licenses/gpl-2.0/',
+ },
+ {
+ type: 'gnu_gpl_v3',
+ label: 'GNU GPLv3',
+ title: 'GNU General Public License v3.0',
+ url: 'https://choosealicense.com/licenses/gpl-3.0/',
+ },
+ {
+ type: 'gnu_lgpl_v2_1',
+ label: 'GNU LGPLv2.1',
+ title: 'GNU Lesser General Public License v2.1',
+ url: 'https://choosealicense.com/licenses/lgpl-2.1/',
+ },
+ {
+ type: 'gnu_lgpl_v3',
+ label: 'GNU LGPLv3',
+ title: 'GNU Lesser General Public License v3.0',
+ url: 'https://choosealicense.com/licenses/lgpl-3.0/',
+ },
+ {
+ type: 'bsd_2_clause',
+ label: 'BSD-2-Clause',
+ title: 'BSD 2-clause "Simplified" license',
+ url: 'https://choosealicense.com/licenses/bsd-2-clause/',
+ },
+ {
+ type: 'bsd_3_clause',
+ label: 'BSD-3-Clause',
+ title: 'BSD 3-clause "New" Or "Revised" license',
+ url: 'https://choosealicense.com/licenses/bsd-3-clause/',
+ },
+ {
+ type: 'mpl_2_0',
+ label: 'MPL-2.0',
+ title: 'Mozilla Public License 2.0',
+ url: 'https://choosealicense.com/licenses/mpl-2.0/',
+ },
+ {
+ type: 'osl_3_0',
+ label: 'OSL-3.0',
+ title: 'Open Software License 3.0',
+ url: 'https://choosealicense.com/licenses/osl-3.0/',
+ },
+ {
+ type: 'apache_2_0',
+ label: 'Apache',
+ title: 'Apache 2.0',
+ url: 'https://choosealicense.com/licenses/apache-2.0/',
+ },
+ {
+ type: 'gnu_agpl_v3',
+ label: 'GNU AGPLv3',
+ title: 'GNU Affero General Public License',
+ url: 'https://choosealicense.com/licenses/agpl-3.0/',
+ },
+ {
+ type: 'bsl_1_1',
+ label: 'BSL 1.1',
+ title: 'Business Source License',
+ url: 'https://mariadb.com/bsl11/',
+ },
+];
diff --git a/lib/cookies.ts b/lib/cookies.ts
new file mode 100644
index 0000000000..cef6a38a42
--- /dev/null
+++ b/lib/cookies.ts
@@ -0,0 +1,39 @@
+import Cookies from 'js-cookie';
+
+import isBrowser from './isBrowser';
+
+export enum NAMES {
+ NAV_BAR_COLLAPSED='nav_bar_collapsed',
+ API_TOKEN='_explorer_key',
+ INVALID_SESSION='invalid_session',
+ CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
+ TXS_SORT='txs_sort',
+ COLOR_MODE='chakra-ui-color-mode',
+ COLOR_MODE_HEX='chakra-ui-color-mode-hex',
+ ADDRESS_IDENTICON_TYPE='address_identicon_type',
+ INDEXING_ALERT='indexing_alert',
+ ADBLOCK_DETECTED='adblock_detected',
+ MIXPANEL_DEBUG='_mixpanel_debug',
+ ADDRESS_NFT_DISPLAY_TYPE='address_nft_display_type',
+ UUID='uuid',
+}
+
+export function get(name?: NAMES | undefined | null, serverCookie?: string) {
+ if (!isBrowser()) {
+ return serverCookie ? getFromCookieString(serverCookie, name) : undefined;
+ }
+
+ if (name) {
+ return Cookies.get(name);
+ }
+}
+
+export function set(name: string, value: string, attributes: Cookies.CookieAttributes = {}) {
+ attributes.path = '/';
+
+ return Cookies.set(name, value, attributes);
+}
+
+export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) {
+ return cookieString.split(`${ name }=`)[1]?.split(';')[0];
+}
diff --git a/lib/date/dayjs.ts b/lib/date/dayjs.ts
new file mode 100644
index 0000000000..e0191ce1e0
--- /dev/null
+++ b/lib/date/dayjs.ts
@@ -0,0 +1,65 @@
+// eslint-disable-next-line no-restricted-imports
+import dayjs from 'dayjs';
+import duration from 'dayjs/plugin/duration';
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import minMax from 'dayjs/plugin/minMax';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import updateLocale from 'dayjs/plugin/updateLocale';
+import weekOfYear from 'dayjs/plugin/weekOfYear';
+
+import { nbsp } from 'lib/html-entities';
+
+const relativeTimeConfig = {
+ thresholds: [
+ { l: 's', r: 1 },
+ { l: 'ss', r: 59, d: 'second' },
+ { l: 'm', r: 1 },
+ { l: 'mm', r: 59, d: 'minute' },
+ { l: 'h', r: 1 },
+ { l: 'hh', r: 23, d: 'hour' },
+ { l: 'd', r: 1 },
+ { l: 'dd', r: 6, d: 'day' },
+ { l: 'w', r: 1 },
+ { l: 'ww', r: 4, d: 'week' },
+ { l: 'M', r: 1 },
+ { l: 'MM', r: 11, d: 'month' },
+ { l: 'y', r: 17 },
+ { l: 'yy', d: 'year' },
+ ],
+};
+
+dayjs.extend(relativeTime, relativeTimeConfig);
+dayjs.extend(updateLocale);
+dayjs.extend(localizedFormat);
+dayjs.extend(duration);
+dayjs.extend(weekOfYear);
+dayjs.extend(minMax);
+
+dayjs.updateLocale('en', {
+ formats: {
+ llll: `MMM DD YYYY HH:mm:ss A (Z${ nbsp }UTC)`,
+ lll: 'MMM D, YYYY h:mm A',
+ },
+ relativeTime: {
+ s: '1s',
+ ss: '%ds',
+ future: 'in %s',
+ past: '%s ago',
+ m: '1m',
+ mm: '%dm',
+ h: '1h',
+ hh: '%dh',
+ d: '1d',
+ dd: '%dd',
+ w: '1w',
+ ww: '%dw',
+ M: '1mo',
+ MM: '%dmo',
+ y: '1y',
+ yy: '%dy',
+ },
+});
+
+dayjs.locale('en');
+
+export default dayjs;
diff --git a/lib/delay.ts b/lib/delay.ts
new file mode 100644
index 0000000000..efd2e216a0
--- /dev/null
+++ b/lib/delay.ts
@@ -0,0 +1,3 @@
+export default function delay(time: number) {
+ return new Promise((resolve) => window.setTimeout(resolve, time));
+}
diff --git a/lib/downloadBlob.ts b/lib/downloadBlob.ts
new file mode 100644
index 0000000000..1a20dc6236
--- /dev/null
+++ b/lib/downloadBlob.ts
@@ -0,0 +1,10 @@
+export default function downloadBlob(blob: Blob, filename: string) {
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.setAttribute('href', url);
+ link.setAttribute('download', filename);
+ link.click();
+
+ link.remove();
+ URL.revokeObjectURL(url);
+}
diff --git a/lib/errors/getErrorCause.ts b/lib/errors/getErrorCause.ts
new file mode 100644
index 0000000000..5b553a7018
--- /dev/null
+++ b/lib/errors/getErrorCause.ts
@@ -0,0 +1,8 @@
+export default function getErrorCause(error: Error | undefined): Record | undefined {
+ return (
+ error && 'cause' in error &&
+ typeof error.cause === 'object' && error.cause !== null &&
+ error.cause as Record
+ ) ||
+ undefined;
+}
diff --git a/lib/errors/getErrorCauseStatusCode.ts b/lib/errors/getErrorCauseStatusCode.ts
new file mode 100644
index 0000000000..f9884f03cd
--- /dev/null
+++ b/lib/errors/getErrorCauseStatusCode.ts
@@ -0,0 +1,6 @@
+import getErrorCause from './getErrorCause';
+
+export default function getErrorCauseStatusCode(error: Error | undefined): number | undefined {
+ const cause = getErrorCause(error);
+ return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
+}
diff --git a/lib/errors/getErrorObj.ts b/lib/errors/getErrorObj.ts
new file mode 100644
index 0000000000..7a06fdc6cd
--- /dev/null
+++ b/lib/errors/getErrorObj.ts
@@ -0,0 +1,15 @@
+export default function getErrorObj(error: unknown) {
+ if (typeof error !== 'object') {
+ return;
+ }
+
+ if (Array.isArray(error)) {
+ return;
+ }
+
+ if (error === null) {
+ return;
+ }
+
+ return error;
+}
diff --git a/lib/errors/getErrorObjPayload.ts b/lib/errors/getErrorObjPayload.ts
new file mode 100644
index 0000000000..524979fe41
--- /dev/null
+++ b/lib/errors/getErrorObjPayload.ts
@@ -0,0 +1,23 @@
+import getErrorObj from './getErrorObj';
+
+export default function getErrorObjPayload(error: unknown): Payload | undefined {
+ const errorObj = getErrorObj(error);
+
+ if (!errorObj || !('payload' in errorObj)) {
+ return;
+ }
+
+ if (typeof errorObj.payload !== 'object') {
+ return;
+ }
+
+ if (errorObj === null) {
+ return;
+ }
+
+ if (Array.isArray(errorObj)) {
+ return;
+ }
+
+ return errorObj.payload as Payload;
+}
diff --git a/lib/errors/getErrorObjStatusCode.ts b/lib/errors/getErrorObjStatusCode.ts
new file mode 100644
index 0000000000..a672b9d7a4
--- /dev/null
+++ b/lib/errors/getErrorObjStatusCode.ts
@@ -0,0 +1,11 @@
+import getErrorObj from './getErrorObj';
+
+export default function getErrorObjStatusCode(error: unknown) {
+ const errorObj = getErrorObj(error);
+
+ if (!errorObj || !('status' in errorObj) || typeof errorObj.status !== 'number') {
+ return;
+ }
+
+ return errorObj.status;
+}
diff --git a/lib/errors/getResourceErrorPayload.tsx b/lib/errors/getResourceErrorPayload.tsx
new file mode 100644
index 0000000000..bbe3981a38
--- /dev/null
+++ b/lib/errors/getResourceErrorPayload.tsx
@@ -0,0 +1,9 @@
+import type { ResourceError } from 'lib/api/resources';
+
+import getErrorCause from './getErrorCause';
+
+export default function getResourceErrorPayload | string>(error: Error | undefined):
+ResourceError['payload'] | undefined {
+ const cause = getErrorCause(error);
+ return cause && 'payload' in cause ? cause.payload as ResourceError['payload'] : undefined;
+}
diff --git a/lib/errors/throwOnAbsentParamError.ts b/lib/errors/throwOnAbsentParamError.ts
new file mode 100644
index 0000000000..5978b600a5
--- /dev/null
+++ b/lib/errors/throwOnAbsentParamError.ts
@@ -0,0 +1,5 @@
+export default function throwOnAbsentParamError(param: unknown) {
+ if (!param) {
+ throw new Error('Required param not provided', { cause: { status: 404 } });
+ }
+}
diff --git a/lib/errors/throwOnResourceLoadError.ts b/lib/errors/throwOnResourceLoadError.ts
new file mode 100644
index 0000000000..32e5169fc7
--- /dev/null
+++ b/lib/errors/throwOnResourceLoadError.ts
@@ -0,0 +1,19 @@
+import type { ResourceError, ResourceName } from 'lib/api/resources';
+
+type Params = ({
+ isError: true;
+ error: ResourceError;
+} | {
+ isError: false;
+ error: null;
+}) & {
+ resource?: ResourceName;
+}
+
+export const RESOURCE_LOAD_ERROR_MESSAGE = 'Resource load error';
+
+export default function throwOnResourceLoadError({ isError, error, resource }: Params) {
+ if (isError) {
+ throw Error(RESOURCE_LOAD_ERROR_MESSAGE, { cause: { ...error, resource } as unknown as Error });
+ }
+}
diff --git a/lib/escapeRegExp.ts b/lib/escapeRegExp.ts
new file mode 100644
index 0000000000..a92ec80037
--- /dev/null
+++ b/lib/escapeRegExp.ts
@@ -0,0 +1,3 @@
+export default function escapeRegExp(string: string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
diff --git a/lib/getArbitrumVerificationStepStatus.ts b/lib/getArbitrumVerificationStepStatus.ts
new file mode 100644
index 0000000000..e86114d115
--- /dev/null
+++ b/lib/getArbitrumVerificationStepStatus.ts
@@ -0,0 +1,25 @@
+import type { ArbitrumBatchStatus, ArbitrumL2TxData } from 'types/api/arbitrumL2';
+
+type Args = {
+ status: ArbitrumBatchStatus;
+ commitment_transaction: ArbitrumL2TxData;
+ confirmation_transaction: ArbitrumL2TxData;
+}
+
+export default function getArbitrumVerificationStepStatus({
+ status,
+ commitment_transaction: commitTx,
+ confirmation_transaction: confirmTx,
+}: Args) {
+ if (status === 'Sent to base') {
+ if (commitTx.status === 'unfinalized') {
+ return 'pending';
+ }
+ }
+ if (status === 'Confirmed on base') {
+ if (confirmTx.status === 'unfinalized') {
+ return 'pending';
+ }
+ }
+ return 'finalized';
+}
diff --git a/lib/getCurrencyValue.ts b/lib/getCurrencyValue.ts
new file mode 100644
index 0000000000..1009b1e80f
--- /dev/null
+++ b/lib/getCurrencyValue.ts
@@ -0,0 +1,32 @@
+import BigNumber from 'bignumber.js';
+
+import { ZERO } from 'lib/consts';
+
+interface Params {
+ value: string;
+ exchangeRate?: string | null;
+ accuracy?: number;
+ accuracyUsd?: number;
+ decimals?: string | null;
+}
+
+export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimals, exchangeRate }: Params) {
+ const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18')));
+ const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
+
+ let usdResult: string | undefined;
+ let usdBn = ZERO;
+
+ if (exchangeRate) {
+ const exchangeRateBn = new BigNumber(exchangeRate);
+ usdBn = valueCurr.times(exchangeRateBn);
+ if (accuracyUsd && !usdBn.isEqualTo(0)) {
+ const usdBnDp = usdBn.dp(accuracyUsd);
+ usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
+ } else {
+ usdResult = usdBn.toFormat();
+ }
+ }
+
+ return { valueStr: valueResult, usd: usdResult, usdBn };
+}
diff --git a/lib/getErrorMessage.ts b/lib/getErrorMessage.ts
new file mode 100644
index 0000000000..b9fa1c9e77
--- /dev/null
+++ b/lib/getErrorMessage.ts
@@ -0,0 +1,3 @@
+export default function getErrorMessage(error: Record> | undefined, field: string) {
+ return error?.[field]?.join(', ');
+}
diff --git a/lib/getFilterValueFromQuery.ts b/lib/getFilterValueFromQuery.ts
new file mode 100644
index 0000000000..77d3c99893
--- /dev/null
+++ b/lib/getFilterValueFromQuery.ts
@@ -0,0 +1,5 @@
+export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined): FilterType | undefined {
+ if (typeof val === 'string' && filterValues.includes(val as FilterType)) {
+ return val as FilterType;
+ }
+}
diff --git a/lib/getFilterValuesFromQuery.ts b/lib/getFilterValuesFromQuery.ts
new file mode 100644
index 0000000000..ce5eddeb63
--- /dev/null
+++ b/lib/getFilterValuesFromQuery.ts
@@ -0,0 +1,15 @@
+export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined) {
+ if (val === undefined) {
+ return;
+ }
+
+ const valArray = [];
+ if (typeof val === 'string') {
+ valArray.push(...val.split(','));
+ }
+ if (Array.isArray(val)) {
+ val.forEach(el => valArray.push(...el.split(',')));
+ }
+
+ return valArray.filter(el => filterValues.includes(el as unknown as FilterType)) as unknown as Array;
+}
diff --git a/lib/getValueWithUnit.tsx b/lib/getValueWithUnit.tsx
new file mode 100644
index 0000000000..eeaf8a0883
--- /dev/null
+++ b/lib/getValueWithUnit.tsx
@@ -0,0 +1,23 @@
+import BigNumber from 'bignumber.js';
+
+import type { Unit } from 'types/unit';
+
+import { WEI, GWEI } from 'lib/consts';
+
+export default function getValueWithUnit(value: string | number, unit: Unit = 'wei') {
+ let unitBn: BigNumber.Value;
+ switch (unit) {
+ case 'wei':
+ unitBn = WEI;
+ break;
+ case 'gwei':
+ unitBn = GWEI;
+ break;
+ default:
+ unitBn = new BigNumber(1);
+ }
+
+ const valueBn = new BigNumber(value);
+ const valueCurr = valueBn.dividedBy(unitBn);
+ return valueCurr;
+}
diff --git a/lib/growthbook/consts.ts b/lib/growthbook/consts.ts
new file mode 100644
index 0000000000..b687eedbb3
--- /dev/null
+++ b/lib/growthbook/consts.ts
@@ -0,0 +1,2 @@
+export const STORAGE_KEY = 'growthbook:experiments';
+export const STORAGE_LIMIT = 20;
diff --git a/lib/growthbook/init.ts b/lib/growthbook/init.ts
new file mode 100644
index 0000000000..d98b2b94b7
--- /dev/null
+++ b/lib/growthbook/init.ts
@@ -0,0 +1,69 @@
+import { GrowthBook } from '@growthbook/growthbook-react';
+
+import config from 'configs/app';
+import * as mixpanel from 'lib/mixpanel';
+
+import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
+
+export interface GrowthBookFeatures {
+ test_value: string;
+}
+
+export const growthBook = (() => {
+ const feature = config.features.growthBook;
+
+ if (!feature.isEnabled) {
+ return;
+ }
+
+ return new GrowthBook({
+ apiHost: 'https://cdn.growthbook.io',
+ clientKey: feature.clientKey,
+ enableDevMode: config.app.isDev,
+ attributes: {
+ id: mixpanel.getUuid(),
+ chain_id: config.chain.id,
+ },
+ trackingCallback: (experiment, result) => {
+ if (isExperimentStarted(experiment.key)) {
+ return;
+ }
+
+ saveExperimentInStorage(experiment.key);
+ mixpanel.logEvent(mixpanel.EventTypes.EXPERIMENT_STARTED, {
+ 'Experiment name': experiment.key,
+ 'Variant name': result.value,
+ Source: 'growthbook',
+ });
+ },
+ });
+})();
+
+function getStorageValue(): Array | undefined {
+ const item = window.localStorage.getItem(STORAGE_KEY);
+ if (!item) {
+ return;
+ }
+
+ try {
+ const parsedValue = JSON.parse(item);
+ if (Array.isArray(parsedValue)) {
+ return parsedValue;
+ }
+ } catch {
+ return;
+ }
+}
+
+function isExperimentStarted(key: string): boolean {
+ const items = getStorageValue() ?? [];
+ return items.some((item) => item === key);
+}
+
+function saveExperimentInStorage(key: string) {
+ const items = getStorageValue() ?? [];
+ const newItems = [ key, ...items ].slice(0, STORAGE_LIMIT);
+ try {
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newItems));
+ } catch (error) {}
+}
diff --git a/lib/growthbook/useFeatureValue.ts b/lib/growthbook/useFeatureValue.ts
new file mode 100644
index 0000000000..7dde7e8522
--- /dev/null
+++ b/lib/growthbook/useFeatureValue.ts
@@ -0,0 +1,14 @@
+import type { WidenPrimitives } from '@growthbook/growthbook';
+import { useFeatureValue, useGrowthBook } from '@growthbook/growthbook-react';
+
+import type { GrowthBookFeatures } from './init';
+
+export default function useGbFeatureValue(
+ name: Name,
+ fallback: GrowthBookFeatures[Name],
+): { value: WidenPrimitives; isLoading: boolean } {
+ const value = useFeatureValue(name, fallback);
+ const growthBook = useGrowthBook();
+
+ return { value, isLoading: !(growthBook?.ready ?? true) };
+}
diff --git a/lib/growthbook/useLoadFeatures.ts b/lib/growthbook/useLoadFeatures.ts
new file mode 100644
index 0000000000..993e84505c
--- /dev/null
+++ b/lib/growthbook/useLoadFeatures.ts
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { SECOND } from 'lib/consts';
+
+import { growthBook } from './init';
+
+export default function useLoadFeatures() {
+ React.useEffect(() => {
+ growthBook?.setAttributes({
+ ...growthBook.getAttributes(),
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ language: window.navigator.language,
+ });
+
+ growthBook?.loadFeatures({ timeout: SECOND });
+ }, []);
+}
diff --git a/lib/hexToAddress.ts b/lib/hexToAddress.ts
new file mode 100644
index 0000000000..4ff126edde
--- /dev/null
+++ b/lib/hexToAddress.ts
@@ -0,0 +1,4 @@
+export default function hexToAddress(hex: string) {
+ const shortenHex = hex.slice(0, 66);
+ return shortenHex.slice(0, 2) + shortenHex.slice(26);
+}
diff --git a/lib/hexToBase64.ts b/lib/hexToBase64.ts
new file mode 100644
index 0000000000..5b1366a6da
--- /dev/null
+++ b/lib/hexToBase64.ts
@@ -0,0 +1,8 @@
+import bytesToBase64 from './bytesToBase64';
+import hexToBytes from './hexToBytes';
+
+export default function hexToBase64(hex: string) {
+ const bytes = new Uint8Array(hexToBytes(hex));
+
+ return bytesToBase64(bytes);
+}
diff --git a/lib/hexToBytes.ts b/lib/hexToBytes.ts
new file mode 100644
index 0000000000..e34435fbf4
--- /dev/null
+++ b/lib/hexToBytes.ts
@@ -0,0 +1,9 @@
+// hex can be with prefix - `0x{string}` - or without it - `{string}`
+export default function hexToBytes(hex: string) {
+ const bytes = [];
+ const startIndex = hex.startsWith('0x') ? 2 : 0;
+ for (let c = startIndex; c < hex.length; c += 2) {
+ bytes.push(parseInt(hex.substring(c, c + 2), 16));
+ }
+ return bytes;
+}
diff --git a/lib/hexToDecimal.ts b/lib/hexToDecimal.ts
new file mode 100644
index 0000000000..43bf1be78b
--- /dev/null
+++ b/lib/hexToDecimal.ts
@@ -0,0 +1,4 @@
+export default function hetToDecimal(hex: string) {
+ const strippedHex = hex.startsWith('0x') ? hex.slice(2) : hex;
+ return parseInt(strippedHex, 16);
+}
diff --git a/lib/hexToUtf8.ts b/lib/hexToUtf8.ts
new file mode 100644
index 0000000000..8766ee25cc
--- /dev/null
+++ b/lib/hexToUtf8.ts
@@ -0,0 +1,8 @@
+import hexToBytes from 'lib/hexToBytes';
+
+export default function hexToUtf8(hex: string) {
+ const utf8decoder = new TextDecoder();
+ const bytes = new Uint8Array(hexToBytes(hex));
+
+ return utf8decoder.decode(bytes);
+}
diff --git a/lib/highlightText.ts b/lib/highlightText.ts
new file mode 100644
index 0000000000..85bfe91066
--- /dev/null
+++ b/lib/highlightText.ts
@@ -0,0 +1,8 @@
+import xss from 'xss';
+
+import escapeRegExp from 'lib/escapeRegExp';
+
+export default function highlightText(text: string, query: string) {
+ const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
+ return xss(text.replace(regex, '$1'));
+}
diff --git a/lib/hooks/useAdblockDetect.tsx b/lib/hooks/useAdblockDetect.tsx
new file mode 100644
index 0000000000..a3b42de1dd
--- /dev/null
+++ b/lib/hooks/useAdblockDetect.tsx
@@ -0,0 +1,47 @@
+import { useEffect } from 'react';
+
+import type { AdBannerProviders } from 'types/client/adProviders';
+
+import config from 'configs/app';
+import { useAppContext } from 'lib/contexts/app';
+import * as cookies from 'lib/cookies';
+import isBrowser from 'lib/isBrowser';
+
+const DEFAULT_URL = 'https://request-global.czilladx.com';
+
+// in general, detect should work with any ad-provider url (that is alive)
+// but we see some false-positive results in certain browsers
+const TEST_URLS: Record = {
+ slise: 'https://v1.slise.xyz/serve',
+ coinzilla: 'https://request-global.czilladx.com',
+ adbutler: 'https://servedbyadbutler.com/app.js',
+ hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js',
+ // I don't have an url for getit to test
+ // getit: DEFAULT_URL,
+ none: DEFAULT_URL,
+};
+
+const feature = config.features.adsBanner;
+
+export default function useAdblockDetect() {
+ const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
+ const provider = feature.isEnabled && feature.provider;
+
+ useEffect(() => {
+ if (isBrowser() && !hasAdblockCookie && provider) {
+ const url = TEST_URLS[provider] || DEFAULT_URL;
+ fetch(url, {
+ method: 'HEAD',
+ mode: 'no-cors',
+ cache: 'no-store',
+ })
+ .then(() => {
+ cookies.set(cookies.NAMES.ADBLOCK_DETECTED, 'false', { expires: 1 });
+ })
+ .catch(() => {
+ cookies.set(cookies.NAMES.ADBLOCK_DETECTED, 'true', { expires: 1 });
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+}
diff --git a/lib/hooks/useClientRect.tsx b/lib/hooks/useClientRect.tsx
new file mode 100644
index 0000000000..05aa278f5b
--- /dev/null
+++ b/lib/hooks/useClientRect.tsx
@@ -0,0 +1,37 @@
+import _debounce from 'lodash/debounce';
+import type { LegacyRef } from 'react';
+import React from 'react';
+
+export default function useClientRect(): [ DOMRect | null, LegacyRef | undefined ] {
+ const [ rect, setRect ] = React.useState(null);
+ const nodeRef = React.useRef();
+
+ const ref = React.useCallback((node: E) => {
+ if (node !== null) {
+ setRect(node.getBoundingClientRect());
+ }
+ nodeRef.current = node;
+ }, []);
+
+ React.useEffect(() => {
+ const content = window.document.querySelector('main');
+ if (!content) {
+ return;
+ }
+
+ const resizeHandler = _debounce(() => {
+ setRect(nodeRef.current?.getBoundingClientRect() ?? null);
+ }, 100);
+
+ const resizeObserver = new ResizeObserver(resizeHandler);
+ resizeObserver.observe(content);
+ resizeObserver.observe(window.document.body);
+
+ return function cleanup() {
+ resizeObserver.unobserve(content);
+ resizeObserver.unobserve(window.document.body);
+ };
+ }, [ ]);
+
+ return [ rect, ref ];
+}
diff --git a/lib/hooks/useContractTabs.tsx b/lib/hooks/useContractTabs.tsx
new file mode 100644
index 0000000000..24667f69a1
--- /dev/null
+++ b/lib/hooks/useContractTabs.tsx
@@ -0,0 +1,143 @@
+import { useRouter } from 'next/router';
+import React from 'react';
+
+import type { Address } from 'types/api/address';
+
+import useApiQuery from 'lib/api/useApiQuery';
+import * as cookies from 'lib/cookies';
+import getQueryParamString from 'lib/router/getQueryParamString';
+import useSocketChannel from 'lib/socket/useSocketChannel';
+import * as stubs from 'stubs/contract';
+import ContractCode from 'ui/address/contract/ContractCode';
+import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom';
+import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy';
+import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular';
+import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils';
+
+const CONTRACT_TAB_IDS = [
+ 'contract_code',
+ 'read_contract',
+ 'read_contract_rpc',
+ 'read_proxy',
+ 'read_custom_methods',
+ 'write_contract',
+ 'write_contract_rpc',
+ 'write_proxy',
+ 'write_custom_methods',
+] as const;
+
+interface ContractTab {
+ id: typeof CONTRACT_TAB_IDS[number];
+ title: string;
+ component: JSX.Element;
+}
+
+interface ReturnType {
+ tabs: Array;
+ isLoading: boolean;
+}
+
+export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean): ReturnType {
+ const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
+
+ const router = useRouter();
+ const tab = getQueryParamString(router.query.tab);
+
+ const isEnabled = Boolean(data?.hash) && data?.is_contract && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab);
+
+ const enableQuery = React.useCallback(() => {
+ setIsQueryEnabled(true);
+ }, []);
+
+ const contractQuery = useApiQuery('contract', {
+ pathParams: { hash: data?.hash },
+ queryOptions: {
+ enabled: isEnabled && isQueryEnabled,
+ refetchOnMount: false,
+ placeholderData: data?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
+ },
+ });
+
+ const customAbiQuery = useApiQuery('custom_abi', {
+ queryOptions: {
+ enabled: isEnabled && isQueryEnabled && Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
+ refetchOnMount: false,
+ },
+ });
+
+ const channel = useSocketChannel({
+ topic: `addresses:${ data?.hash?.toLowerCase() }`,
+ isDisabled: !isEnabled,
+ onJoin: enableQuery,
+ onSocketError: enableQuery,
+ });
+
+ const methods = React.useMemo(() => divideAbiIntoMethodTypes(contractQuery.data?.abi ?? []), [ contractQuery.data?.abi ]);
+ const methodsCustomAbi = React.useMemo(() => {
+ return divideAbiIntoMethodTypes(
+ customAbiQuery.data
+ ?.find((item) => data && item.contract_address_hash.toLowerCase() === data.hash.toLowerCase())
+ ?.abi ??
+ [],
+ );
+ }, [ customAbiQuery.data, data ]);
+
+ const verifiedImplementations = React.useMemo(() => {
+ return data?.implementations?.filter(({ name, address }) => name && address && address !== data?.hash) || [];
+ }, [ data?.hash, data?.implementations ]);
+
+ return React.useMemo(() => {
+ return {
+ tabs: [
+ {
+ id: 'contract_code' as const,
+ title: 'Code',
+ component: ,
+ },
+ methods.read.length > 0 && {
+ id: 'read_contract' as const,
+ title: 'Read contract',
+ component: ,
+ },
+ methodsCustomAbi.read.length > 0 && {
+ id: 'read_custom_methods' as const,
+ title: 'Read custom',
+ component: ,
+ },
+ verifiedImplementations.length > 0 && {
+ id: 'read_proxy' as const,
+ title: 'Read proxy',
+ component: (
+
+ ),
+ },
+ methods.write.length > 0 && {
+ id: 'write_contract' as const,
+ title: 'Write contract',
+ component: ,
+ },
+ methodsCustomAbi.write.length > 0 && {
+ id: 'write_custom_methods' as const,
+ title: 'Write custom',
+ component: ,
+ },
+ verifiedImplementations.length > 0 && {
+ id: 'write_proxy' as const,
+ title: 'Write proxy',
+ component: (
+
+ ),
+ },
+ ].filter(Boolean),
+ isLoading: contractQuery.isPlaceholderData,
+ };
+ }, [ contractQuery, channel, data?.hash, verifiedImplementations, methods.read, methods.write, methodsCustomAbi.read, methodsCustomAbi.write ]);
+}
diff --git a/lib/hooks/useDebounce.tsx b/lib/hooks/useDebounce.tsx
new file mode 100644
index 0000000000..5dfc71c141
--- /dev/null
+++ b/lib/hooks/useDebounce.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+export default function useDebounce(value: string, delay: number) {
+ const [ debouncedValue, setDebouncedValue ] = React.useState(value);
+ React.useEffect(
+ () => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+ return () => {
+ clearTimeout(handler);
+ };
+ },
+ [ value, delay ],
+ );
+ return debouncedValue;
+}
diff --git a/lib/hooks/useFetch.tsx b/lib/hooks/useFetch.tsx
new file mode 100644
index 0000000000..06eea3fcae
--- /dev/null
+++ b/lib/hooks/useFetch.tsx
@@ -0,0 +1,79 @@
+import * as Sentry from '@sentry/react';
+import React from 'react';
+
+import isBodyAllowed from 'lib/api/isBodyAllowed';
+import type { ResourceError, ResourcePath } from 'lib/api/resources';
+
+export interface Params {
+ method?: RequestInit['method'];
+ headers?: RequestInit['headers'];
+ signal?: RequestInit['signal'];
+ body?: Record | FormData;
+ credentials?: RequestCredentials;
+}
+
+interface Meta {
+ resource?: ResourcePath;
+ omitSentryErrorLog?: boolean;
+}
+
+export default function useFetch() {
+ return React.useCallback((path: string, params?: Params, meta?: Meta): Promise> => {
+ const _body = params?.body;
+ const isFormData = _body instanceof FormData;
+ const withBody = isBodyAllowed(params?.method);
+
+ const body: FormData | string | undefined = (() => {
+ if (!withBody) {
+ return;
+ }
+
+ if (isFormData) {
+ return _body;
+ }
+
+ return JSON.stringify(_body);
+ })();
+
+ const reqParams = {
+ ...params,
+ body,
+ headers: {
+ ...(withBody && !isFormData ? { 'Content-type': 'application/json' } : undefined),
+ ...params?.headers,
+ },
+ };
+
+ return fetch(path, reqParams).then(response => {
+ if (!response.ok) {
+ const error = {
+ status: response.status,
+ statusText: response.statusText,
+ };
+
+ if (!meta?.omitSentryErrorLog) {
+ Sentry.captureException(new Error('Client fetch failed'), { tags: {
+ source: 'fetch',
+ 'source.resource': meta?.resource,
+ 'status.code': error.status,
+ 'status.text': error.statusText,
+ } });
+ }
+
+ return response.json().then(
+ (jsonError) => Promise.reject({
+ payload: jsonError as Error,
+ status: response.status,
+ statusText: response.statusText,
+ }),
+ () => {
+ return Promise.reject(error);
+ },
+ );
+
+ } else {
+ return response.json() as Promise;
+ }
+ });
+ }, [ ]);
+}
diff --git a/lib/hooks/useFetchProfileInfo.tsx b/lib/hooks/useFetchProfileInfo.tsx
new file mode 100644
index 0000000000..4df2f8d57b
--- /dev/null
+++ b/lib/hooks/useFetchProfileInfo.tsx
@@ -0,0 +1,11 @@
+import useApiQuery from 'lib/api/useApiQuery';
+import * as cookies from 'lib/cookies';
+
+export default function useFetchProfileInfo() {
+ return useApiQuery('user_info', {
+ queryOptions: {
+ refetchOnMount: false,
+ enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
+ },
+ });
+}
diff --git a/lib/hooks/useFirstMountState.tsx b/lib/hooks/useFirstMountState.tsx
new file mode 100644
index 0000000000..b8850fd519
--- /dev/null
+++ b/lib/hooks/useFirstMountState.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+// Returns true if component is just mounted (on first render) and false otherwise.
+export function useFirstMountState(): boolean {
+ const isFirst = React.useRef(true);
+
+ if (isFirst.current) {
+ isFirst.current = false;
+
+ return true;
+ }
+
+ return isFirst.current;
+}
diff --git a/lib/hooks/useGetCsrfToken.tsx b/lib/hooks/useGetCsrfToken.tsx
new file mode 100644
index 0000000000..297f1567c2
--- /dev/null
+++ b/lib/hooks/useGetCsrfToken.tsx
@@ -0,0 +1,38 @@
+import * as Sentry from '@sentry/react';
+import { useQuery } from '@tanstack/react-query';
+
+import buildUrl from 'lib/api/buildUrl';
+import isNeedProxy from 'lib/api/isNeedProxy';
+import { getResourceKey } from 'lib/api/useApiQuery';
+import * as cookies from 'lib/cookies';
+import useFetch from 'lib/hooks/useFetch';
+
+export default function useGetCsrfToken() {
+ const nodeApiFetch = useFetch();
+
+ useQuery({
+ queryKey: getResourceKey('csrf'),
+ queryFn: async() => {
+ if (!isNeedProxy()) {
+ const url = buildUrl('csrf');
+ const apiResponse = await fetch(url, { credentials: 'include' });
+ const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
+
+ if (!csrfFromHeader) {
+ Sentry.captureException(new Error('Client fetch failed'), { tags: {
+ source: 'fetch',
+ 'source.resource': 'csrf',
+ 'status.code': 500,
+ 'status.text': 'Unable to obtain csrf token from header',
+ } });
+ return;
+ }
+
+ return { token: csrfFromHeader };
+ }
+
+ return nodeApiFetch('/node-api/csrf');
+ },
+ enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
+ });
+}
diff --git a/lib/hooks/useGradualIncrement.tsx b/lib/hooks/useGradualIncrement.tsx
new file mode 100644
index 0000000000..e1bab30e27
--- /dev/null
+++ b/lib/hooks/useGradualIncrement.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+
+const DURATION = 300;
+
+export default function useGradualIncrement(initialValue: number): [number, (inc: number) => void] {
+ const [ num, setNum ] = React.useState(initialValue);
+ const queue = React.useRef