diff --git a/explorer/.gitignore b/explorer/.gitignore new file mode 100644 index 000000000..b9e25d5b1 --- /dev/null +++ b/explorer/.gitignore @@ -0,0 +1,65 @@ +# 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/sprite.*.svg +/public/icons/registry.json +/public/icons/README.md +/public/static/og_image.png +/public/sitemap.xml +/public/robots.txt +/analyze + +# production +/build + +# misc +.DS_Store +*.pem +.tools +grafana +.cursor + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env*.local +/configs/envs/.env.secrets +/configs/envs/.samples + +# typescript +*.tsbuildinfo + +.eslintcache + +**.decrypted~** +/test-results/ +/playwright-report/ +/playwright/.cache/ +/playwright/.browser/ +/playwright/envs.js +/playwright/affected-tests.txt + +**.dec** + +# build outputs +/tools/preset-sync/index.js +/toolkit/package/dist \ No newline at end of file diff --git a/explorer/README.md b/explorer/README.md new file mode 100644 index 000000000..9bef0feb3 --- /dev/null +++ b/explorer/README.md @@ -0,0 +1,3 @@ +# Monorepo + +This project consists of 2 parts, frontend which is the BlockScout frontend that feeds from a database and a cron-job that updates this database with the wss data. \ No newline at end of file diff --git a/explorer/frontend/.devcontainer/devcontainer.json b/explorer/frontend/.devcontainer/devcontainer.json new file mode 100644 index 000000000..cc48cec34 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.dockerignore b/explorer/frontend/.dockerignore new file mode 100644 index 000000000..13a543a44 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.env.example b/explorer/frontend/.env.example new file mode 100644 index 000000000..43503e0d5 --- /dev/null +++ b/explorer/frontend/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL= +NEXT_PUBLIC_APP_PROTOCOL=https +NEXT_PUBLIC_APP_HOST=127.0.0.1:3000 +NEXT_PUBLIC_NETWORK_NAME=Yellow \ No newline at end of file diff --git a/explorer/frontend/.gitattributes b/explorer/frontend/.gitattributes new file mode 100644 index 000000000..d4431e296 --- /dev/null +++ b/explorer/frontend/.gitattributes @@ -0,0 +1 @@ +__snapshots__/** filter=lfs diff=lfs merge=lfs -text diff --git a/explorer/frontend/.github/ISSUE_TEMPLATE/bug_report.yml b/explorer/frontend/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..732e01d37 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/ISSUE_TEMPLATE/config.yml b/explorer/frontend/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..439bca677 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/workflows/checks.yml b/explorer/frontend/.github/workflows/checks.yml new file mode 100644 index 000000000..ad5636c2f --- /dev/null +++ b/explorer/frontend/.github/workflows/checks.yml @@ -0,0 +1,287 @@ +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: 22.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 + + - name: Run ESLint + run: yarn lint:eslint + + - name: Compile TypeScript + run: yarn lint:tsc + + toolkit_build_check: + name: Toolkit build check + needs: [ code_quality ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22.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 project dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile + + - name: Install package dependencies + run: | + cd ./toolkit/package + yarn --frozen-lockfile + + - name: Type check the package + run: | + cd ./toolkit/package + yarn typecheck + + - name: Build the package + run: | + cd ./toolkit/package + yarn build + + - name: Verify build output + run: | + cd ./toolkit/package + if [ ! -d "dist" ]; then + echo "Build failed: dist directory not found" + exit 1 + fi + + if [ ! -f "dist/index.js" ]; then + echo "Build failed: dist/index.js not found" + exit 1 + fi + + if [ ! -f "dist/index.d.ts" ]; then + echo "Build failed: dist/index.d.ts not found" + exit 1 + fi + + 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: 22.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 + + - name: Install script dependencies + run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile + + - 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: 22.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 + + - 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: 22.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 + + - 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.49.0-noble + + 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: 22.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 + + - 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/explorer/frontend/.github/workflows/cleanup.yml b/explorer/frontend/.github/workflows/cleanup.yml new file mode 100644 index 000000000..26b4d6782 --- /dev/null +++ b/explorer/frontend/.github/workflows/cleanup.yml @@ -0,0 +1,33 @@ +name: Cleanup environments + +on: + pull_request: + types: + - closed + - merged + workflow_dispatch: + +jobs: + cleanup_release: + uses: blockscout/actions/.github/workflows/cleanup_helmfile.yaml@main + 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/actions/.github/workflows/cleanup_helmfile.yaml@main + 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/explorer/frontend/.github/workflows/copy-issues-labels.yml b/explorer/frontend/.github/workflows/copy-issues-labels.yml new file mode 100644 index 000000000..e05b6e88e --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/workflows/deploy-main.yml b/explorer/frontend/.github/workflows/deploy-main.yml new file mode 100644 index 000000000..8a7cd4bc8 --- /dev/null +++ b/explorer/frontend/.github/workflows/deploy-main.yml @@ -0,0 +1,31 @@ +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 + with: + tags: | + type=ref,event=branch + type=raw,value=unstable diff --git a/explorer/frontend/.github/workflows/deploy-review-l2.yml b/explorer/frontend/.github/workflows/deploy-review-l2.yml new file mode 100644 index 000000000..3482219b7 --- /dev/null +++ b/explorer/frontend/.github/workflows/deploy-review-l2.yml @@ -0,0 +1,72 @@ +name: Deploy review environment (L2) + +on: + workflow_dispatch: + inputs: + envs_preset: + description: ENVs preset + required: false + default: "" + type: choice + options: + - none + - arbitrum + - arbitrum_nova + - arbitrum_sepolia + - base + - celo_alfajores + - garnet + - gnosis + - eth + - eth_sepolia + - eth_goerli + - filecoin + - immutable + - neon_devnet + - optimism + - optimism_sepolia + - polygon + - rootstock + - scroll_sepolia + - shibarium + - stability + - zkevm + - zilliqa_prototestnet + - zksync + - zora + +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: | + type=raw,value=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/actions/.github/workflows/deploy_helmfile.yaml@main + 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/explorer/frontend/.github/workflows/deploy-review.yml b/explorer/frontend/.github/workflows/deploy-review.yml new file mode 100644 index 000000000..bc49f5c67 --- /dev/null +++ b/explorer/frontend/.github/workflows/deploy-review.yml @@ -0,0 +1,75 @@ +name: Deploy review environment + +on: + workflow_dispatch: + inputs: + envs_preset: + description: ENVs preset + required: false + default: main + type: choice + options: + - none + - arbitrum + - arbitrum_nova + - arbitrum_sepolia + - base + - celo_alfajores + - garnet + - gnosis + - eth + - eth_sepolia + - eth_goerli + - filecoin + - immutable + - main + - mekong + - neon_devnet + - optimism + - optimism_sepolia + - polygon + - rari_testnet + - rootstock + - shibarium + - scroll_sepolia + - stability + - zkevm + - zilliqa_prototestnet + - zksync + - zora + +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: | + type=raw,value=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/actions/.github/workflows/deploy_helmfile.yaml@main + 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/explorer/frontend/.github/workflows/e2e-tests.yml b/explorer/frontend/.github/workflows/e2e-tests.yml new file mode 100644 index 000000000..b4ed44844 --- /dev/null +++ b/explorer/frontend/.github/workflows/e2e-tests.yml @@ -0,0 +1,76 @@ +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 + with: + # NOTE: by default the image will be built with type=ref,event=tag; so we don't need to specify it here + tags: | + type=raw,value=e2e + + 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 + 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 tests + uses: convictional/trigger-workflow-and-wait@v1.6.1 + with: + owner: blockscout + repo: blockscout-ci-cd + github_token: ${{ env.WORKFLOW_TRIGGER_TOKEN }} + workflow_file_name: e2e_new.yaml + ref: master + wait_interval: 30 diff --git a/explorer/frontend/.github/workflows/label-issues-in-release.yml b/explorer/frontend/.github/workflows/label-issues-in-release.yml new file mode 100644 index 000000000..b95fd4561 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/workflows/pre-release.yml b/explorer/frontend/.github/workflows/pre-release.yml new file mode 100644 index 000000000..05155cbd3 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/workflows/project-management.yml b/explorer/frontend/.github/workflows/project-management.yml new file mode 100644 index 000000000..1da733bae --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/workflows/publish-image.yml b/explorer/frontend/.github/workflows/publish-image.yml new file mode 100644 index 000000000..8c39c9bca --- /dev/null +++ b/explorer/frontend/.github/workflows/publish-image.yml @@ -0,0 +1,94 @@ +name: Publish Docker image + +on: + workflow_dispatch: + inputs: + tags: + description: Image tags (e.g. "type=raw,value=foo") + 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 + linux/arm64/v8 + workflow_call: + inputs: + tags: + description: Image tags (e.g. "type=raw,value=foo") + 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 + linux/arm64/v8 + +jobs: + run: + name: Run + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + # 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 + flavor: | + latest=false + tags: | + type=ref,event=tag + ${{ inputs.tags }} + + - 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: Setup repo + uses: blockscout/actions/.github/actions/setup-multiarch-buildx@no-metadata + id: setup + with: + docker-image: ghcr.io/blockscout/frontend + docker-username: ${{ github.actor }} + docker-password: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + cache-from: type=gha + 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/explorer/frontend/.github/workflows/release.yml b/explorer/frontend/.github/workflows/release.yml new file mode 100644 index 000000000..f64f8dcb8 --- /dev/null +++ b/explorer/frontend/.github/workflows/release.yml @@ -0,0 +1,107 @@ +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 + # NOTE: by default the image will be built with type=ref,event=tag; so we need to specify it here + tags: | + type=raw,value=latest + + sync_envs_docs: + name: Sync ENV variables docs + uses: './.github/workflows/sync-envs-docs.yml' + needs: publish_image + secrets: inherit + + publish_toolkit: + name: Publish toolkit package to NPM + uses: './.github/workflows/toolkit-npm-publisher.yml' + secrets: inherit + with: + version: ${{ github.ref_name }} + + # 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/explorer/frontend/.github/workflows/stale-issues.yml b/explorer/frontend/.github/workflows/stale-issues.yml new file mode 100644 index 000000000..bcc1a36ca --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.github/workflows/sync-envs-docs.yml b/explorer/frontend/.github/workflows/sync-envs-docs.yml new file mode 100644 index 000000000..462b22b31 --- /dev/null +++ b/explorer/frontend/.github/workflows/sync-envs-docs.yml @@ -0,0 +1,41 @@ +name: Sync ENV variables docs + +on: + workflow_dispatch: + workflow_call: + +jobs: + run: + name: Run + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Copy main ENV file to Blockscout Docs repository + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.GITHIB_BOT_TOKEN }} + with: + source_file: 'docs/ENVS.md' + destination_repo: 'blockscout/docs' + destination_folder: 'setup/env-variables/frontend-common-envs' + rename: 'envs.md' + destination_branch: 'master' + user_email: 'bot@blockscout.com' + user_name: 'blockscout-bot' + use_rsync: true + + - name: Copy deprecated ENV file to Blockscout Docs repository + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.GITHIB_BOT_TOKEN }} + with: + source_file: 'docs/DEPRECATED_ENVS.md' + destination_repo: 'blockscout/docs' + destination_folder: 'setup/env-variables/frontend-common-envs' + rename: 'deprecated-envs.md' + destination_branch: 'master' + user_email: 'bot@blockscout.com' + user_name: 'blockscout-bot' + use_rsync: true diff --git a/explorer/frontend/.github/workflows/toolkit-npm-publisher.yml b/explorer/frontend/.github/workflows/toolkit-npm-publisher.yml new file mode 100644 index 000000000..20f77df55 --- /dev/null +++ b/explorer/frontend/.github/workflows/toolkit-npm-publisher.yml @@ -0,0 +1,52 @@ +name: Publish toolkit package to NPM + +on: + workflow_dispatch: + inputs: + version: + description: Package version + type: string + required: true + workflow_call: + inputs: + version: + description: Package version + type: string + required: true + +jobs: + publish: + runs-on: ubuntu-latest + name: Publish package to NPM registry + permissions: + id-token: write + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + # Also it will setup .npmrc file to publish to npm + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: '22.11.0' + registry-url: 'https://registry.npmjs.org' + + - name: Update package version + run: | + cd ./toolkit/package + npm version ${{ inputs.version }} + + - name: Build the package + run: | + yarn + cd ./toolkit/package + yarn + yarn build + + - name: Publish to NPM registry + run: | + cd ./toolkit/package + npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/explorer/frontend/.github/workflows/update-project-cards.yml b/explorer/frontend/.github/workflows/update-project-cards.yml new file mode 100644 index 000000000..ecf3ab4f5 --- /dev/null +++ b/explorer/frontend/.github/workflows/update-project-cards.yml @@ -0,0 +1,244 @@ +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(`Found ${ projects.length } with the similar name:`); + projects.forEach((issue) => { + core.info(` #${ projects.number } - ${ projects.title }`); + }) + core.setFailed('Found 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 }...`); + try { + await changeItemFieldValue(item); + core.info('Done.\n'); + } catch (error) { + core.info(`Error: ${ error.message }\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/explorer/frontend/.github/workflows/upload-source-maps.yml b/explorer/frontend/.github/workflows/upload-source-maps.yml new file mode 100644 index 000000000..6c0ec2fa0 --- /dev/null +++ b/explorer/frontend/.github/workflows/upload-source-maps.yml @@ -0,0 +1,49 @@ +# TODO @tom2drum setup source maps for Rollbar +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: 22.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 + + - 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/explorer/frontend/.gitignore b/explorer/frontend/.gitignore new file mode 100644 index 000000000..b9e25d5b1 --- /dev/null +++ b/explorer/frontend/.gitignore @@ -0,0 +1,65 @@ +# 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/sprite.*.svg +/public/icons/registry.json +/public/icons/README.md +/public/static/og_image.png +/public/sitemap.xml +/public/robots.txt +/analyze + +# production +/build + +# misc +.DS_Store +*.pem +.tools +grafana +.cursor + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env*.local +/configs/envs/.env.secrets +/configs/envs/.samples + +# typescript +*.tsbuildinfo + +.eslintcache + +**.decrypted~** +/test-results/ +/playwright-report/ +/playwright/.cache/ +/playwright/.browser/ +/playwright/envs.js +/playwright/affected-tests.txt + +**.dec** + +# build outputs +/tools/preset-sync/index.js +/toolkit/package/dist \ No newline at end of file diff --git a/explorer/frontend/.husky/.gitignore b/explorer/frontend/.husky/.gitignore new file mode 100644 index 000000000..31354ec13 --- /dev/null +++ b/explorer/frontend/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/explorer/frontend/.husky/post-checkout b/explorer/frontend/.husky/post-checkout new file mode 100755 index 000000000..c37815e2b --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.husky/post-commit b/explorer/frontend/.husky/post-commit new file mode 100755 index 000000000..e5230c305 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.husky/post-merge b/explorer/frontend/.husky/post-merge new file mode 100755 index 000000000..c99b752a5 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.husky/pre-commit b/explorer/frontend/.husky/pre-commit new file mode 100755 index 000000000..c81f321ca --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.husky/pre-push b/explorer/frontend/.husky/pre-push new file mode 100755 index 000000000..216e91527 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.nvmrc b/explorer/frontend/.nvmrc new file mode 100644 index 000000000..fdb2eaaff --- /dev/null +++ b/explorer/frontend/.nvmrc @@ -0,0 +1 @@ +22.11.0 \ No newline at end of file diff --git a/explorer/frontend/.vscode/extensions.json b/explorer/frontend/.vscode/extensions.json new file mode 100644 index 000000000..e2c659e24 --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.vscode/launch.json b/explorer/frontend/.vscode/launch.json new file mode 100644 index 000000000..b24083bbe --- /dev/null +++ b/explorer/frontend/.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/explorer/frontend/.vscode/settings.json b/explorer/frontend/.vscode/settings.json new file mode 100644 index 000000000..ee3f389ee --- /dev/null +++ b/explorer/frontend/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "javascript.preferences.autoImportFileExcludePatterns": [ + "./toolkit/package/**", + "./toolkit/components/**/index.ts", + ], + "typescript.preferences.autoImportFileExcludePatterns": [ + "./toolkit/package/**", + "./toolkit/components/**/index.ts", + ] +} \ No newline at end of file diff --git a/explorer/frontend/.vscode/tasks.json b/explorer/frontend/.vscode/tasks.json new file mode 100644 index 000000000..bc94afc29 --- /dev/null +++ b/explorer/frontend/.vscode/tasks.json @@ -0,0 +1,394 @@ +{ + // 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": [ + "all", + "main", + "localhost", + "arbitrum", + "arbitrum_sepolia", + "base", + "blackfort_testnet", + "celo_alfajores", + "garnet", + "gnosis", + "immutable", + "eth", + "eth_goerli", + "eth_sepolia", + "filecoin", + "mekong", + "neon_devnet", + "optimism", + "optimism_interop_0", + "optimism_sepolia", + "polygon", + "rari_testnet", + "rootstock_testnet", + "scroll_sepolia", + "shibarium", + "stability_testnet", + "zkevm", + "zilliqa_prototestnet", + "zksync", + "zora", + ], + "default": "main" + }, + ], +} \ No newline at end of file diff --git a/explorer/frontend/CODE_OF_CONDUCT.md b/explorer/frontend/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..5726fbc35 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/Dockerfile b/explorer/frontend/Dockerfile new file mode 100644 index 000000000..36ca785c8 --- /dev/null +++ b/explorer/frontend/Dockerfile @@ -0,0 +1,177 @@ +# ***************************** +# *** STAGE 1: Dependencies *** +# ***************************** +FROM node:22.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 tsconfig.json ./ +COPY types ./types +COPY lib ./lib +COPY configs/app ./configs/app +COPY toolkit/theme ./toolkit/theme +COPY toolkit/utils ./toolkit/utils +COPY toolkit/components/forms/validators/url.ts ./toolkit/components/forms/validators/url.ts +RUN apk add git +RUN yarn --frozen-lockfile --network-timeout 100000 + + +### FEATURE REPORTER +# Install dependencies +WORKDIR /feature-reporter +COPY ./deploy/tools/feature-reporter/package.json ./deploy/tools/feature-reporter/yarn.lock ./ +RUN yarn --frozen-lockfile --network-timeout 100000 + + +### 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 --network-timeout 100000 + +### FAVICON GENERATOR +# Install dependencies +WORKDIR /favicon-generator +COPY ./deploy/tools/favicon-generator/package.json ./deploy/tools/favicon-generator/yarn.lock ./ +RUN yarn --frozen-lockfile --network-timeout 100000 + +### SITEMAP GENERATOR +# Install dependencies +WORKDIR /sitemap-generator +COPY ./deploy/tools/sitemap-generator/package.json ./deploy/tools/sitemap-generator/yarn.lock ./ +RUN yarn --frozen-lockfile --network-timeout 100000 + + +# ***************************** +# ****** STAGE 2: Build ******* +# ***************************** +FROM node:22.11.0-alpine AS builder +RUN apk add --no-cache --upgrade libc6-compat bash jq + +# 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 . . + +# Build SVG sprite and generate .env.registry with ENVs list and save build args into .env file +RUN set -a && \ + source ./deploy/scripts/build_sprite.sh && \ + ./deploy/scripts/collect_envs.sh ./docs/ENVS.md && \ + set +a + +# 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 +ENV NODE_OPTIONS="--max-old-space-size=4096" +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 + + +### FAVICON GENERATOR +# Copy dependencies and source code +COPY --from=deps /favicon-generator/node_modules ./deploy/tools/favicon-generator/node_modules + + +### SITEMAP GENERATOR +# Copy dependencies and source code +COPY --from=deps /sitemap-generator/node_modules ./deploy/tools/sitemap-generator/node_modules + + +# ***************************** +# ******* STAGE 3: Run ******** +# ***************************** +# Production image, copy all the files and run next +FROM node:22.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 . +## OG image generator +COPY ./deploy/scripts/og_image_generator.js . +## Favicon generator +COPY --chmod=755 ./deploy/scripts/favicon_generator.sh . +COPY --from=builder /app/deploy/tools/favicon-generator ./deploy/tools/favicon-generator +RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"] +RUN ["chmod", "-R", "777", "./public"] +## Sitemap generator +COPY --chmod=755 ./deploy/scripts/sitemap_generator.sh . +COPY --from=builder /app/deploy/tools/sitemap-generator ./deploy/tools/sitemap-generator + +# 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/explorer/frontend/LICENSE b/explorer/frontend/LICENSE new file mode 100644 index 000000000..20d40b6bc --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/README.md b/explorer/frontend/README.md new file mode 100644 index 000000000..1256043ef --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/RELEASE_NOTES.md b/explorer/frontend/RELEASE_NOTES.md new file mode 100644 index 000000000..2be3dba12 --- /dev/null +++ b/explorer/frontend/RELEASE_NOTES.md @@ -0,0 +1,46 @@ +## 🚀 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. + +## 🎨 Design updates +- New style 1. +- New style 2. + +## ✨ 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) + +## 💑 Compatibility +From this version onward, the app is compatible only with the following API versions: + +| Service | Version | +| --- | --- | +| Blockscout API | ... | +| Stats microservice API | ... | +| ... | ... | + +## 🦄 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/explorer/frontend/components/LatestChannels.tsx b/explorer/frontend/components/LatestChannels.tsx new file mode 100644 index 000000000..447cc25d7 --- /dev/null +++ b/explorer/frontend/components/LatestChannels.tsx @@ -0,0 +1,196 @@ +import { Box, Flex, Text } from '@chakra-ui/react'; +import { useEffect, useState } from 'react'; + +interface Channel { + channelId: string; + adjudicator: string; + amount: string; + chainId: number; + challenge: number; + createdAt: string; + nonce: string; + participant: string; + status: string; + token: string; + updatedAt: string; + version: number; + wallet: string; +} + +function isChannel(item: unknown): item is Channel { + if (!item || typeof item !== 'object') return false; + const channel = item as Record; + return ( + typeof channel.channelId === 'string' && + typeof channel.adjudicator === 'string' && + typeof channel.amount === 'string' && + typeof channel.chainId === 'number' && + typeof channel.challenge === 'number' && + typeof channel.createdAt === 'string' && + typeof channel.nonce === 'string' && + typeof channel.participant === 'string' && + typeof channel.status === 'string' && + typeof channel.token === 'string' && + typeof channel.updatedAt === 'string' && + typeof channel.version === 'number' && + typeof channel.wallet === 'string' + ); +} + +function isChannelArray(data: unknown): data is Array { + return Array.isArray(data) && data.every(isChannel); +} + +const getStatusColor = (status: string): string => { + switch (status.toLowerCase()) { + case 'open': + return 'green.500'; + case 'closed': + return 'red.500'; + case 'disputed': + return 'yellow.500'; + default: + return 'gray.500'; + } +}; + +export default function LatestChannels() { + const [ channels, setChannels ] = useState>([]); + const [ loading, setLoading ] = useState(true); + const [ error, setError ] = useState(null); + + useEffect(() => { + const fetchLatestChannels = async() => { + try { + const response = await fetch('/api/latest-channels'); + if (!response.ok) { + throw new Error('Failed to fetch latest channels'); + } + + const data = await response.json(); + if (!isChannelArray(data)) { + throw new Error('Invalid API response format'); + } + + setChannels(data); + setError(null); + } catch (err) { + setError('Failed to load latest channels'); + // eslint-disable-next-line no-console + console.error('Error fetching latest channels:', err); + } finally { + setLoading(false); + } + }; + + fetchLatestChannels(); + }, []); + + if (loading) { + return ( + + {[ ...Array(5) ].map((_, i) => ( + + + + ))} + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + return ( + + {channels.map((channel, index) => ( + + + + + Channel ID: {channel.channelId} + + + Chain ID: {channel.chainId} + + + + + + Created: {new Date(channel.createdAt).toLocaleString()} + + + Updated: {new Date(channel.updatedAt).toLocaleString()} + + + + + + Status: + {channel.status} + + + + Amount: {Number(channel.amount).toLocaleString()} + + + + + Participant: {channel.participant} + + + + Wallet: {channel.wallet} + + + + ))} + + ); +} diff --git a/explorer/frontend/components/LatestLedgerEntries.tsx b/explorer/frontend/components/LatestLedgerEntries.tsx new file mode 100644 index 000000000..ec5b88c4d --- /dev/null +++ b/explorer/frontend/components/LatestLedgerEntries.tsx @@ -0,0 +1,149 @@ +import { Box, Flex, Text } from '@chakra-ui/react'; +import { useEffect, useState } from 'react'; + +import { Link } from 'toolkit/chakra/link'; +import { + TableRoot as Table, + TableHeader as Thead, + TableBody as Tbody, + TableRow as Tr, + TableColumnHeader as Th, + TableCell as Td, +} from 'toolkit/chakra/table'; + +interface LedgerEntry { + id: number; + accountId: string; + accountType: number; + asset: string; + createdAt: string; + credit: string; + debit: string; + participant: string; +} + +function isLedgerEntry(item: unknown): item is LedgerEntry { + if (!item || typeof item !== 'object') return false; + const entry = item as Record; + return ( + typeof entry.id === 'number' && + typeof entry.accountId === 'string' && + typeof entry.accountType === 'number' && + typeof entry.asset === 'string' && + typeof entry.createdAt === 'string' && + typeof entry.credit === 'string' && + typeof entry.debit === 'string' && + typeof entry.participant === 'string' + ); +} + +function isLedgerEntryArray(data: unknown): data is Array { + return Array.isArray(data) && data.every(isLedgerEntry); +} + +const formatAmount = (amount: string): string => { + return parseFloat(amount).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 8, + }); +}; + +export default function LatestLedgerEntries() { + const [ entries, setEntries ] = useState>([]); + const [ loading, setLoading ] = useState(true); + const [ error, setError ] = useState(null); + + useEffect(() => { + const fetchLatestEntries = async() => { + try { + const response = await fetch('/api/latest-ledger-entries'); + if (!response.ok) { + throw new Error('Failed to fetch latest ledger entries'); + } + + const data = await response.json(); + if (!isLedgerEntryArray(data)) { + throw new Error('Invalid API response format'); + } + + setEntries(data); + setError(null); + } catch (err) { + setError('Failed to load latest ledger entries'); + // eslint-disable-next-line no-console + console.error('Error fetching latest ledger entries:', err); + } finally { + setLoading(false); + } + }; + + fetchLatestEntries(); + }, []); + + if (loading) { + return ( + + {[ ...Array(5) ].map((_, i) => ( + + + + ))} + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + return ( + + + + + + + + + + + + {entries.map((entry) => ( + + + + + + + ))} + +
AccountAssetCreditDebit
{entry.accountId}{entry.asset} 0 ? 'green.500' : undefined}> + {formatAmount(entry.credit)} + 0 ? 'red.500' : undefined}> + {formatAmount(entry.debit)} +
+ + + View all ledger entries → + + +
+ ); +} \ No newline at end of file diff --git a/explorer/frontend/configs/app/apis.ts b/explorer/frontend/configs/app/apis.ts new file mode 100644 index 000000000..ab17e8550 --- /dev/null +++ b/explorer/frontend/configs/app/apis.ts @@ -0,0 +1,142 @@ +import type { ApiName } from 'lib/api/types'; + +import { stripTrailingSlash } from 'toolkit/utils/url'; + +import { getEnvValue } from './utils'; + +interface ApiPropsBase { + endpoint: string; + basePath?: string; +} + +interface ApiPropsFull extends ApiPropsBase { + host: string; + protocol: string; + port?: string; + socketEndpoint: string; +} + +const generalApi = (() => { + 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(''); + + return Object.freeze({ + endpoint: apiEndpoint, + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_API_BASE_PATH') || ''), + socketEndpoint: socketEndpoint, + host: apiHost ?? '', + protocol: apiSchema ?? 'https', + port: apiPort, + }); +})(); + +const adminApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + }); +})(); + +const bensApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + }); +})(); + +const contractInfoApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + }); +})(); + +const metadataApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + }); +})(); + +const rewardsApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_REWARDS_SERVICE_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + }); +})(); + +const statsApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_STATS_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_STATS_API_BASE_PATH') || ''), + }); +})(); + +const visualizeApi = (() => { + const apiHost = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST'); + if (!apiHost) { + return; + } + + return Object.freeze({ + endpoint: apiHost, + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_VISUALIZE_API_BASE_PATH') || ''), + }); +})(); + +type Apis = { + general: ApiPropsFull; +} & Partial, ApiPropsBase>>; + +const apis: Apis = Object.freeze({ + general: generalApi, + admin: adminApi, + bens: bensApi, + contractInfo: contractInfoApi, + metadata: metadataApi, + rewards: rewardsApi, + stats: statsApi, + visualize: visualizeApi, +}); + +export default apis; diff --git a/explorer/frontend/configs/app/app.ts b/explorer/frontend/configs/app/app.ts new file mode 100644 index 000000000..f5cca629e --- /dev/null +++ b/explorer/frontend/configs/app/app.ts @@ -0,0 +1,27 @@ +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 isPw = getEnvValue('NEXT_PUBLIC_APP_INSTANCE') === 'pw'; +const spriteHash = getEnvValue('NEXT_PUBLIC_ICON_SPRITE_HASH'); + +const app = Object.freeze({ + isDev, + isPw, + protocol: appSchema, + host: appHost, + port: appPort, + baseUrl, + useProxy: getEnvValue('NEXT_PUBLIC_USE_NEXT_JS_PROXY') === 'true', + spriteHash, +}); + +export default app; diff --git a/explorer/frontend/configs/app/chain.ts b/explorer/frontend/configs/app/chain.ts new file mode 100644 index 000000000..5dfa4c3f1 --- /dev/null +++ b/explorer/frontend/configs/app/chain.ts @@ -0,0 +1,55 @@ +import type { RollupType } from 'types/client/rollup'; +import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks'; + +import { urlValidator } from 'toolkit/components/forms/validators/url'; + +import { getEnvValue, parseEnvJson } from './utils'; + +const DEFAULT_CURRENCY_DECIMALS = 18; + +const rollupType = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE') as RollupType; + +const verificationType: NetworkVerificationType = (() => { + if (rollupType === 'arbitrum') { + return 'posting'; + } + if (rollupType === 'zkEvm') { + return 'sequencing'; + } + return getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') as NetworkVerificationTypeEnvs || 'mining'; +})(); + +const rpcUrls = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'); + const isUrl = urlValidator(envValue); + + if (envValue && isUrl === true) { + return [ envValue ]; + } + + const parsedValue = parseEnvJson>(envValue); + + return Array.isArray(parsedValue) ? parsedValue : []; +})(); + +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', + rpcUrls, + isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', + verificationType, +}); + +export default chain; diff --git a/explorer/frontend/configs/app/features/account.ts b/explorer/frontend/configs/app/features/account.ts new file mode 100644 index 000000000..82ae3b458 --- /dev/null +++ b/explorer/frontend/configs/app/features/account.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import services from '../services'; +import { getEnvValue } from '../utils'; + +const title = 'My account'; + +const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => { + if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV2.siteKey) { + return Object.freeze({ + title, + isEnabled: true, + recaptchaSiteKey: services.reCaptchaV2.siteKey, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/addressMetadata.ts b/explorer/frontend/configs/app/features/addressMetadata.ts new file mode 100644 index 000000000..385432037 --- /dev/null +++ b/explorer/frontend/configs/app/features/addressMetadata.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import apis from '../apis'; + +const title = 'Address metadata'; + +const config: Feature<{}> = (() => { + if (apis.metadata) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/addressProfileAPI.ts b/explorer/frontend/configs/app/features/addressProfileAPI.ts new file mode 100644 index 000000000..e46301ee6 --- /dev/null +++ b/explorer/frontend/configs/app/features/addressProfileAPI.ts @@ -0,0 +1,45 @@ +import type { Feature } from './types'; +import type { AddressProfileAPIConfig } from 'types/client/addressProfileAPIConfig'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const value = parseEnvJson(getEnvValue('NEXT_PUBLIC_ADDRESS_USERNAME_TAG')); + +function checkApiUrlTemplate(apiUrlTemplate: string): boolean { + try { + const testUrl = apiUrlTemplate.replace('{address}', '0x0000000000000000000000000000000000000000'); + new URL(testUrl).toString(); + return true; + } catch (error) { + return false; + } +} + +const title = 'User profile API'; + +const config: Feature<{ + apiUrlTemplate: string; + tagLinkTemplate?: string; + tagIcon?: string; + tagBgColor?: string; + tagTextColor?: string; +}> = (() => { + if (value && checkApiUrlTemplate(value.api_url_template)) { + return Object.freeze({ + title, + isEnabled: true, + apiUrlTemplate: value.api_url_template, + tagLinkTemplate: value.tag_link_template, + tagIcon: value.tag_icon, + tagBgColor: value.tag_bg_color, + tagTextColor: value.tag_text_color, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/addressVerification.ts b/explorer/frontend/configs/app/features/addressVerification.ts new file mode 100644 index 000000000..bd83be12b --- /dev/null +++ b/explorer/frontend/configs/app/features/addressVerification.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import apis from '../apis'; +import account from './account'; +import verifiedTokens from './verifiedTokens'; + +const title = 'Address verification in "My account"'; + +const config: Feature<{}> = (() => { + if (account.isEnabled && verifiedTokens.isEnabled && apis.admin) { + return Object.freeze({ + title: 'Address verification in "My account"', + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/adsBanner.ts b/explorer/frontend/configs/app/features/adsBanner.ts new file mode 100644 index 000000000..c455ed454 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/adsText.ts b/explorer/frontend/configs/app/features/adsText.ts new file mode 100644 index 000000000..82946f984 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/advancedFilter.ts b/explorer/frontend/configs/app/features/advancedFilter.ts new file mode 100644 index 000000000..7495d369f --- /dev/null +++ b/explorer/frontend/configs/app/features/advancedFilter.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const isDisabled = getEnvValue('NEXT_PUBLIC_ADVANCED_FILTER_ENABLED') === 'false'; + +const title = 'Advanced filter'; + +const config: Feature<{}> = (() => { + if (!isDisabled) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/beaconChain.ts b/explorer/frontend/configs/app/features/beaconChain.ts new file mode 100644 index 000000000..a29540b75 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/blockchainInteraction.ts b/explorer/frontend/configs/app/features/blockchainInteraction.ts new file mode 100644 index 000000000..670012608 --- /dev/null +++ b/explorer/frontend/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.rpcUrls.length > 0 && + walletConnectProjectId + ) { + return Object.freeze({ + title, + isEnabled: true, + walletConnect: { + projectId: walletConnectProjectId, + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/bridgedTokens.ts b/explorer/frontend/configs/app/features/bridgedTokens.ts new file mode 100644 index 000000000..ae275c539 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/celo.ts b/explorer/frontend/configs/app/features/celo.ts new file mode 100644 index 000000000..9d169e422 --- /dev/null +++ b/explorer/frontend/configs/app/features/celo.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'Celo chain'; + +const config: Feature<{ L2UpgradeBlock: number | undefined; BLOCKS_PER_EPOCH: number }> = (() => { + + if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + L2UpgradeBlock: getEnvValue('NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK') ? Number(getEnvValue('NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK')) : undefined, + BLOCKS_PER_EPOCH: 17_280, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/csvExport.ts b/explorer/frontend/configs/app/features/csvExport.ts new file mode 100644 index 000000000..fe1e24ec3 --- /dev/null +++ b/explorer/frontend/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.reCaptchaV2.siteKey) { + return Object.freeze({ + title, + isEnabled: true, + reCaptcha: { + siteKey: services.reCaptchaV2.siteKey, + }, + }); + } + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/dataAvailability.ts b/explorer/frontend/configs/app/features/dataAvailability.ts new file mode 100644 index 000000000..add9e5fec --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/deFiDropdown.ts b/explorer/frontend/configs/app/features/deFiDropdown.ts new file mode 100644 index 000000000..62b8fdcd1 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/easterEggBadge.ts b/explorer/frontend/configs/app/features/easterEggBadge.ts new file mode 100644 index 000000000..d8ce02926 --- /dev/null +++ b/explorer/frontend/configs/app/features/easterEggBadge.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const badgeClaimLink = getEnvValue('NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK'); + +const title = 'Easter egg badge'; + +const config: Feature<{ badgeClaimLink: string }> = (() => { + if (badgeClaimLink) { + return Object.freeze({ + title, + isEnabled: true, + badgeClaimLink, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/externalTxs.ts b/explorer/frontend/configs/app/features/externalTxs.ts new file mode 100644 index 000000000..3b78f70ea --- /dev/null +++ b/explorer/frontend/configs/app/features/externalTxs.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; +import type { TxExternalTxsConfig } from 'types/client/externalTxsConfig'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const externalTransactionsConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG')); + +const title = 'External transactions'; + +const config: Feature<{ chainName: string; chainLogoUrl: string; explorerUrlTemplate: string }> = (() => { + if (externalTransactionsConfig) { + return Object.freeze({ + title, + isEnabled: true, + chainName: externalTransactionsConfig.chain_name, + chainLogoUrl: externalTransactionsConfig.chain_logo_url, + explorerUrlTemplate: externalTransactionsConfig.explorer_url_template, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/faultProofSystem.ts b/explorer/frontend/configs/app/features/faultProofSystem.ts new file mode 100644 index 000000000..38a8f021d --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/gasTracker.ts b/explorer/frontend/configs/app/features/gasTracker.ts new file mode 100644 index 000000000..c20242e60 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/getGasButton.ts b/explorer/frontend/configs/app/features/getGasButton.ts new file mode 100644 index 000000000..db392f3c9 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/googleAnalytics.ts b/explorer/frontend/configs/app/features/googleAnalytics.ts new file mode 100644 index 000000000..4fe9a9bf9 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/graphqlApiDocs.ts b/explorer/frontend/configs/app/features/graphqlApiDocs.ts new file mode 100644 index 000000000..d26c3bfde --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/growthBook.ts b/explorer/frontend/configs/app/features/growthBook.ts new file mode 100644 index 000000000..af672c5ac --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/index.ts b/explorer/frontend/configs/app/features/index.ts new file mode 100644 index 000000000..6cb6425f5 --- /dev/null +++ b/explorer/frontend/configs/app/features/index.ts @@ -0,0 +1,45 @@ +export { default as advancedFilter } from './advancedFilter'; +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 celo } from './celo'; +export { default as csvExport } from './csvExport'; +export { default as dataAvailability } from './dataAvailability'; +export { default as deFiDropdown } from './deFiDropdown'; +export { default as easterEggBadge } from './easterEggBadge'; +export { default as externalTxs } from './externalTxs'; +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 pools } from './pools'; +export { default as publicTagsSubmission } from './publicTagsSubmission'; +export { default as restApiDocs } from './restApiDocs'; +export { default as rewards } from './rewards'; +export { default as rollbar } from './rollbar'; +export { default as rollup } from './rollup'; +export { default as safe } from './safe'; +export { default as saveOnGas } from './saveOnGas'; +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 addressProfileAPI } from './addressProfileAPI'; +export { default as validators } from './validators'; +export { default as verifiedTokens } from './verifiedTokens'; +export { default as web3Wallet } from './web3Wallet'; +export { default as xStarScore } from './xStarScore'; diff --git a/explorer/frontend/configs/app/features/marketplace.ts b/explorer/frontend/configs/app/features/marketplace.ts new file mode 100644 index 000000000..da6dbe1ca --- /dev/null +++ b/explorer/frontend/configs/app/features/marketplace.ts @@ -0,0 +1,77 @@ +import type { Feature } from './types'; + +import apis from '../apis'; +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 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 graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL'); + +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; + graphLinksUrl: string | undefined; +}> = (() => { + if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) { + const props = { + submitFormUrl, + categoriesUrl, + suggestIdeasFormUrl, + securityReportsUrl, + featuredApp, + banner: bannerContentUrl && bannerLinkUrl ? { + contentUrl: bannerContentUrl, + linkUrl: bannerLinkUrl, + } : undefined, + rating: ratingAirtableApiKey && ratingAirtableBaseId ? { + airtableApiKey: ratingAirtableApiKey, + airtableBaseId: ratingAirtableBaseId, + } : undefined, + graphLinksUrl, + }; + + if (configUrl) { + return Object.freeze({ + title, + isEnabled: true, + configUrl, + ...props, + }); + } else if (apis.admin) { + return Object.freeze({ + title, + isEnabled: true, + api: apis.admin, + ...props, + }); + } + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/metasuites.ts b/explorer/frontend/configs/app/features/metasuites.ts new file mode 100644 index 000000000..333e7d5a8 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/mixpanel.ts b/explorer/frontend/configs/app/features/mixpanel.ts new file mode 100644 index 000000000..ef9fabd91 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/mudFramework.ts b/explorer/frontend/configs/app/features/mudFramework.ts new file mode 100644 index 000000000..86df2af34 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/multichainButton.ts b/explorer/frontend/configs/app/features/multichainButton.ts new file mode 100644 index 000000000..923a16bd2 --- /dev/null +++ b/explorer/frontend/configs/app/features/multichainButton.ts @@ -0,0 +1,31 @@ +import type { Feature } from './types'; +import type { MultichainProviderConfig, MultichainProviderConfigParsed } 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<{ providers: Array }> = (() => { + if (value) { + return Object.freeze({ + title, + isEnabled: true, + providers: value.map((provider) => ({ + name: provider.name, + logoUrl: provider.logo, + urlTemplate: provider.url_template, + dappId: marketplace.isEnabled ? provider.dapp_id : undefined, + })), + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/nameService.ts b/explorer/frontend/configs/app/features/nameService.ts new file mode 100644 index 000000000..96efcee26 --- /dev/null +++ b/explorer/frontend/configs/app/features/nameService.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import apis from '../apis'; + +const title = 'Name service integration'; + +const config: Feature<{}> = (() => { + if (apis.bens) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/pools.ts b/explorer/frontend/configs/app/features/pools.ts new file mode 100644 index 000000000..5973fb3ac --- /dev/null +++ b/explorer/frontend/configs/app/features/pools.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import apis from '../apis'; +import { getEnvValue } from '../utils'; + +const dexPoolsEnabled = getEnvValue('NEXT_PUBLIC_DEX_POOLS_ENABLED') === 'true'; + +const title = 'DEX Pools'; + +const config: Feature<{ }> = (() => { + if (apis.contractInfo && dexPoolsEnabled) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/publicTagsSubmission.ts b/explorer/frontend/configs/app/features/publicTagsSubmission.ts new file mode 100644 index 000000000..d5f544862 --- /dev/null +++ b/explorer/frontend/configs/app/features/publicTagsSubmission.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import apis from '../apis'; +import services from '../services'; +import addressMetadata from './addressMetadata'; + +const title = 'Public tag submission'; + +const config: Feature<{}> = (() => { + if (services.reCaptchaV2.siteKey && addressMetadata.isEnabled && apis.admin) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/restApiDocs.ts b/explorer/frontend/configs/app/features/restApiDocs.ts new file mode 100644 index 000000000..ae25f05c0 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/rewards.ts b/explorer/frontend/configs/app/features/rewards.ts new file mode 100644 index 000000000..3b12d46fb --- /dev/null +++ b/explorer/frontend/configs/app/features/rewards.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import apis from '../apis'; +import account from './account'; +import blockchainInteraction from './blockchainInteraction'; + +const title = 'Rewards service integration'; + +const config: Feature<{}> = (() => { + if (apis.rewards && account.isEnabled && blockchainInteraction.isEnabled) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/rollbar.ts b/explorer/frontend/configs/app/features/rollbar.ts new file mode 100644 index 000000000..276f782e4 --- /dev/null +++ b/explorer/frontend/configs/app/features/rollbar.ts @@ -0,0 +1,43 @@ +import type { Feature } from './types'; + +import app from '../app'; +import { getEnvValue } from '../utils'; + +const clientToken = getEnvValue('NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN'); +const instance = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_APP_INSTANCE'); + if (envValue) { + return envValue; + } + + return app.host?.replace('.blockscout.com', '').replace('.k8s-dev', '').replaceAll('-', '_'); +})(); +const environment = getEnvValue('NEXT_PUBLIC_APP_ENV') || 'production'; +const codeVersion = getEnvValue('NEXT_PUBLIC_GIT_TAG') || getEnvValue('NEXT_PUBLIC_GIT_COMMIT_SHA'); + +const title = 'Rollbar error monitoring'; + +const config: Feature<{ + clientToken: string; + environment: string; + instance: string | undefined; + codeVersion: string | undefined; +}> = (() => { + if (clientToken) { + return Object.freeze({ + title, + isEnabled: true, + clientToken, + environment, + instance, + codeVersion, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/rollup.ts b/explorer/frontend/configs/app/features/rollup.ts new file mode 100644 index 000000000..ddc52a211 --- /dev/null +++ b/explorer/frontend/configs/app/features/rollup.ts @@ -0,0 +1,75 @@ +import type { Feature } from './types'; +import type { ParentChain, RollupType } from 'types/client/rollup'; +import { ROLLUP_TYPES } from 'types/client/rollup'; + +import { stripTrailingSlash } from 'toolkit/utils/url'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const type = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE'); + return ROLLUP_TYPES.find((type) => type === envValue); +})(); + +const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL'); + +const parentChain: ParentChain | undefined = (() => { + const envValue = parseEnvJson(getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN')); + const baseUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL') || ''); + const chainName = getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME'); + + if (!baseUrl && !envValue?.baseUrl) { + return; + } + + return { + ...envValue, + name: chainName ?? envValue?.name, + baseUrl: baseUrl ?? envValue?.baseUrl, + }; +})(); + +const title = 'Rollup (L2) chain'; + +const config: Feature<{ + type: RollupType; + homepage: { showLatestBlocks: boolean }; + outputRootsEnabled: boolean; + interopEnabled: boolean; + L2WithdrawalUrl: string | undefined; + parentChain: ParentChain; + DA: { + celestia: { + namespace: string | undefined; + celeniumUrl: string | undefined; + }; + }; +}> = (() => { + if (type && parentChain) { + return Object.freeze({ + title, + isEnabled: true, + type, + L2WithdrawalUrl: type === 'optimistic' ? L2WithdrawalUrl : undefined, + outputRootsEnabled: type === 'optimistic' && getEnvValue('NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED') === 'true', + interopEnabled: type === 'optimistic' && getEnvValue('NEXT_PUBLIC_INTEROP_ENABLED') === 'true', + homepage: { + showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true', + }, + parentChain, + DA: { + celestia: { + namespace: type === 'arbitrum' ? getEnvValue('NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE') : undefined, + celeniumUrl: getEnvValue('NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL'), + }, + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/safe.ts b/explorer/frontend/configs/app/features/safe.ts new file mode 100644 index 000000000..b2762a78d --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/saveOnGas.ts b/explorer/frontend/configs/app/features/saveOnGas.ts new file mode 100644 index 000000000..64d24ed56 --- /dev/null +++ b/explorer/frontend/configs/app/features/saveOnGas.ts @@ -0,0 +1,25 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; +import marketplace from './marketplace'; + +const title = 'Save on gas with GasHawk'; + +const config: Feature<{ + apiUrlTemplate: string; +}> = (() => { + if (getEnvValue('NEXT_PUBLIC_SAVE_ON_GAS_ENABLED') === 'true' && marketplace.isEnabled) { + return Object.freeze({ + title, + isEnabled: true, + apiUrlTemplate: 'https://core.gashawk.io/apiv2/stats/address/
/savingsPotential/0x1', + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/sol2uml.ts b/explorer/frontend/configs/app/features/sol2uml.ts new file mode 100644 index 000000000..5b7fe222a --- /dev/null +++ b/explorer/frontend/configs/app/features/sol2uml.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import apis from '../apis'; + +const title = 'Solidity to UML diagrams'; + +const config: Feature<{}> = (() => { + if (apis.visualize) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/stats.ts b/explorer/frontend/configs/app/features/stats.ts new file mode 100644 index 000000000..58809dd83 --- /dev/null +++ b/explorer/frontend/configs/app/features/stats.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import apis from '../apis'; + +const title = 'Blockchain statistics'; + +const config: Feature<{}> = (() => { + if (apis.stats) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/suave.ts b/explorer/frontend/configs/app/features/suave.ts new file mode 100644 index 000000000..f96b80ede --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/txInterpretation.ts b/explorer/frontend/configs/app/features/txInterpretation.ts new file mode 100644 index 000000000..c22067ee2 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/types.ts b/explorer/frontend/configs/app/features/types.ts new file mode 100644 index 000000000..e0c4a26b5 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/userOps.ts b/explorer/frontend/configs/app/features/userOps.ts new file mode 100644 index 000000000..0e127f62f --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/validators.ts b/explorer/frontend/configs/app/features/validators.ts new file mode 100644 index 000000000..668501e28 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/verifiedTokens.ts b/explorer/frontend/configs/app/features/verifiedTokens.ts new file mode 100644 index 000000000..e1e4bf26f --- /dev/null +++ b/explorer/frontend/configs/app/features/verifiedTokens.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import apis from '../apis'; + +const title = 'Verified tokens info'; + +const config: Feature<{}> = (() => { + if (apis.contractInfo) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/features/web3Wallet.ts b/explorer/frontend/configs/app/features/web3Wallet.ts new file mode 100644 index 000000000..893f92394 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/features/xStarScore.ts b/explorer/frontend/configs/app/features/xStarScore.ts new file mode 100644 index 000000000..4f8efa262 --- /dev/null +++ b/explorer/frontend/configs/app/features/xStarScore.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'XStar score'; +const url = getEnvValue('NEXT_PUBLIC_XSTAR_SCORE_URL'); + +const config: Feature<{ url: string }> = (() => { + if (url) { + return Object.freeze({ + title, + url, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/explorer/frontend/configs/app/index.ts b/explorer/frontend/configs/app/index.ts new file mode 100644 index 000000000..7d0741117 --- /dev/null +++ b/explorer/frontend/configs/app/index.ts @@ -0,0 +1,19 @@ +import apis from './apis'; +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, + apis, + UI, + features, + services, + meta, +}); + +export default config; diff --git a/explorer/frontend/configs/app/meta.ts b/explorer/frontend/configs/app/meta.ts new file mode 100644 index 000000000..87f640528 --- /dev/null +++ b/explorer/frontend/configs/app/meta.ts @@ -0,0 +1,18 @@ +import app from './app'; +import { getEnvValue, getExternalAssetFilePath } from './utils'; + +const defaultImageUrl = '/static/og_image.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/explorer/frontend/configs/app/services.ts b/explorer/frontend/configs/app/services.ts new file mode 100644 index 000000000..ee7d3a0b6 --- /dev/null +++ b/explorer/frontend/configs/app/services.ts @@ -0,0 +1,7 @@ +import { getEnvValue } from './utils'; + +export default Object.freeze({ + reCaptchaV2: { + siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'), + }, +}); diff --git a/explorer/frontend/configs/app/ui.ts b/explorer/frontend/configs/app/ui.ts new file mode 100644 index 000000000..ba0d3ad36 --- /dev/null +++ b/explorer/frontend/configs/app/ui.ts @@ -0,0 +1,115 @@ +import type { ContractCodeIde } from 'types/client/contract'; +import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation'; +import { HOME_STATS_WIDGET_IDS, type ChainIndicatorId, type HeroBannerConfig, type HomeStatsWidgetId } from 'types/homepage'; +import type { NetworkExplorer } from 'types/networks'; +import type { ColorThemeId } from 'types/settings'; +import type { FontFamily } from 'types/ui'; + +import { COLOR_THEMES, type ColorTheme } from 'lib/settings/colorTheme'; + +import * as features from './features'; +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 homePageStats: Array = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_STATS')); + + if (!Array.isArray(parsedValue)) { + const rollupFeature = features.rollup; + + if (rollupFeature.isEnabled && [ 'zkEvm', 'zkSync', 'arbitrum' ].includes(rollupFeature.type)) { + return [ 'latest_batch', 'average_block_time', 'total_txs', 'wallet_addresses', 'gas_tracker' ]; + } + + return [ 'total_blocks', 'average_block_time', 'total_txs', 'wallet_addresses', 'gas_tracker' ]; + } + + return parsedValue.filter((item) => HOME_STATS_WIDGET_IDS.includes(item)); +})(); + +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) as ColorTheme | undefined; +})(); + +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')) || [], + stats: homePageStats, + heroBanner: parseEnvJson(getEnvValue('NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG')), + // !!! DEPRECATED !!! + plate: { + background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND'), + textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR'), + }, + }, + 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, + }, + fonts: { + heading: parseEnvJson(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_HEADING')), + body: parseEnvJson(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_BODY')), + }, + maxContentWidth: getEnvValue('NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED') === 'false' ? false : true, +}); + +export default UI; diff --git a/explorer/frontend/configs/app/ui/views/address.ts b/explorer/frontend/configs/app/ui/views/address.ts new file mode 100644 index 000000000..8972c02fc --- /dev/null +++ b/explorer/frontend/configs/app/ui/views/address.ts @@ -0,0 +1,90 @@ +import type { VerifiedContractsFilter } from 'types/api/contracts'; +import type { SmartContractVerificationMethodExtra } from 'types/client/contract'; +import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, SMART_CONTRACT_LANGUAGE_FILTERS } from 'types/client/contract'; +import type { AddressFormat, AddressViewId, IdenticonType } from 'types/views/address'; +import { ADDRESS_FORMATS, 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 formats: Array = (() => { + const value = (parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT')) || []) + .filter((format) => ADDRESS_FORMATS.includes(format)); + + if (value.length === 0) { + return [ 'base16' ]; + } + + return value; +})(); + +const bech32Prefix = (() => { + const value = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX'); + + if (!value || !formats.includes('bech32')) { + return undefined; + } + + // these are the limits of the bech32 prefix - https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 + return value.length >= 1 && value.length <= 83 ? value : undefined; +})(); + +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>(envValue) || []; + + return parsedMethods.filter((method) => SMART_CONTRACT_EXTRA_VERIFICATION_METHODS.includes(method)); +})(); + +const languageFilters: Array = (() => { + const envValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS')); + if (!envValue) { + // "Scilla" is chain specific language, so we don't want to show it in default scenario + const DEFAULT_LANGUAGE_FILTERS = SMART_CONTRACT_LANGUAGE_FILTERS.filter((filter) => filter !== 'scilla'); + return DEFAULT_LANGUAGE_FILTERS; + } + + return envValue.filter((filter) => SMART_CONTRACT_LANGUAGE_FILTERS.includes(filter)); +})(); + +const config = Object.freeze({ + identiconType, + hashFormat: { + availableFormats: formats, + bech32Prefix, + }, + hiddenViews, + solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true', + extraVerificationMethods, + languageFilters, +}); + +export default config; diff --git a/explorer/frontend/configs/app/ui/views/block.ts b/explorer/frontend/configs/app/ui/views/block.ts new file mode 100644 index 000000000..5e42136a4 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/ui/views/index.ts b/explorer/frontend/configs/app/ui/views/index.ts new file mode 100644 index 000000000..ef4b874dd --- /dev/null +++ b/explorer/frontend/configs/app/ui/views/index.ts @@ -0,0 +1,5 @@ +export { default as address } from './address'; +export { default as block } from './block'; +export { default as nft } from './nft'; +export { default as token } from './token'; +export { default as tx } from './tx'; diff --git a/explorer/frontend/configs/app/ui/views/nft.ts b/explorer/frontend/configs/app/ui/views/nft.ts new file mode 100644 index 000000000..ab9636cce --- /dev/null +++ b/explorer/frontend/configs/app/ui/views/nft.ts @@ -0,0 +1,12 @@ +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')) || [], + verifiedFetch: { + isEnabled: getEnvValue('NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED') === 'false' ? false : true, + }, +}); + +export default config; diff --git a/explorer/frontend/configs/app/ui/views/token.ts b/explorer/frontend/configs/app/ui/views/token.ts new file mode 100644 index 000000000..05052ce91 --- /dev/null +++ b/explorer/frontend/configs/app/ui/views/token.ts @@ -0,0 +1,7 @@ +import { getEnvValue } from 'configs/app/utils'; + +const config = Object.freeze({ + hideScamTokensEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED') === 'true', +}); + +export default config; diff --git a/explorer/frontend/configs/app/ui/views/tx.ts b/explorer/frontend/configs/app/ui/views/tx.ts new file mode 100644 index 000000000..f72536350 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/app/utils.ts b/explorer/frontend/configs/app/utils.ts new file mode 100644 index 000000000..3389a43da --- /dev/null +++ b/explorer/frontend/configs/app/utils.ts @@ -0,0 +1,60 @@ +import { isBrowser } from 'toolkit/utils/isBrowser'; +import * as regexp from 'toolkit/utils/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 fileExtension = getAssetFileExtension(value); + if (!fileExtension) { + throw new Error('Cannot get file path'); + } + return `/assets/configs/${ fileName }.${ fileExtension }`; + } catch (error) { + return; + } +}; + +function getAssetFileExtension(value: string) { + try { + const url = new URL(value); + return url.pathname.match(regexp.FILE_EXTENSION)?.[1]; + } catch (error) { + return parseEnvJson(value) ? 'json' : undefined; + } +} diff --git a/explorer/frontend/configs/envs/.env.arbitrum b/explorer/frontend/configs/envs/.env.arbitrum new file mode 100644 index 000000000..269cb33f8 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.arbitrum @@ -0,0 +1,65 @@ +# Set of ENVs for Arbitrum One network explorer +# https://arbitrum.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/arbitrum-one.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x37c798810d49ba132b40efe7f4fdf6806a8fc58226bb5e185ddc91f896577abf +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(27, 74, 221, 1)']} +NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker'] +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-arbitrum.us.auth0.com/v2/logout +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://arbitrum.blockscout.com/apps/revokescout?chainId=42161 +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': '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_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/arbitrum/token/price'}},{'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-one.publicnode.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_PARENT_CHAIN={'baseUrl':'https://eth.blockscout.com','currency':{'name':'Ether','symbol':'ETH','decimals':18},'isTestnet':false,'id':1,'name':'Ethereum Mainnet','rpcUrls':['https://eth.drpc.org']} +NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME=Ethereum +NEXT_PUBLIC_ROLLUP_TYPE=arbitrum +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://arbitrum.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.arbitrum_nova b/explorer/frontend/configs/envs/.env.arbitrum_nova new file mode 100644 index 000000000..2888565d7 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.arbitrum_nova @@ -0,0 +1,42 @@ +# Set of ENVs for Arbitrum One network explorer +# https://arbitrum.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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-nova.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-nova-icon.svg +NEXT_PUBLIC_NETWORK_ID=42170 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-nova.svg +NEXT_PUBLIC_NETWORK_NAME=Arbitrum Nova +NEXT_PUBLIC_NETWORK_RPC_URL=https://arbitrum.llamarpc.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum Nova +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-nova.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/explorer/frontend/configs/envs/.env.arbitrum_sepolia b/explorer/frontend/configs/envs/.env.arbitrum_sepolia new file mode 100644 index 000000000..f324dacdf --- /dev/null +++ b/explorer/frontend/configs/envs/.env.arbitrum_sepolia @@ -0,0 +1,52 @@ +# Set of ENVs for Arbitrum Sepolia network explorer +# https://arbitrum-sepolia.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=arbitrum_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=arbitrum-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_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/arbitrum-sepolia.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xb730960249381c72588024f5e213abd8e032d968aeb9629103e70677b0850bfa +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(27, 74, 221, 1)']} +NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker'] +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=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_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-sepolia.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-sepolia-dark.svg +NEXT_PUBLIC_NETWORK_ID=421614 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-sepolia.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-sepolia-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Arbitrum Sepolia +NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc +NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum Sepolia +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-sepolia.png +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com +NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'baseUrl':'https://eth-sepolia.blockscout.com','currency':{'name':'Ether','symbol':'ETH','decimals':18},'isTestnet':true,'id':11155111,'name':'Sepolia','rpcUrls':['https://eth-sepolia.public.blastapi.io']} +NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME=Sepolia +NEXT_PUBLIC_ROLLUP_TYPE=arbitrum +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://arbitrum-sepolia.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.base b/explorer/frontend/configs/envs/.env.base new file mode 100644 index 000000000..fa0829c56 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.base @@ -0,0 +1,74 @@ +# Set of ENVs for Base Mainnet network explorer +# https://base.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FAULT_PROOF_ENABLED=true +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/base-mainnet.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xfd5c5dae7b69fe29e61d19b9943e688aa0f1be1e983c4fba8fe985f90ff69d5f +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'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_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://base.blockscout.com/apps/swapscout +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_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}] +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/base/token/price'}},{'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':'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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://base.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_VIEWS_NFT_MARKETPLACES=[{'name':'Rarible','collection_url':'https://rarible.com/collection/base/{hash}/items','instance_url':'https://rarible.com/token/base/{hash}:{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/rarible.png'},{'name':'OpenSea','collection_url':'https://opensea.io/assets/base/{hash}','instance_url':'https://opensea.io/assets/base/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'}, {'name':'MagicEden','collection_url':'https://magiceden.io/collections/base/{hash}','instance_url':'https://magiceden.io/item-details/base/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/magiceden.png'}] +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.blackfort_testnet b/explorer/frontend/configs/envs/.env.blackfort_testnet new file mode 100644 index 000000000..2f6c3cdee --- /dev/null +++ b/explorer/frontend/configs/envs/.env.blackfort_testnet @@ -0,0 +1,46 @@ +# Set of ENVs for BXN Testnet network explorer +# https://blackfort-testnet.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=blackfort_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=testnet.blackfortscan.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/blackfort-testnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/blackfort.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x12b9dc860aaa12918dece27124775d334fe245f9fdb7feddf622b180a605e0a4 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(92deg, rgb(3, 150, 254) 0.24%, rgb(36, 209, 245) 98.31%)']} +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=TBXN +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=TBXN +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/blackfort.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/blackfort-dark.svg +NEXT_PUBLIC_NETWORK_ID=4888 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/blackfort.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/blackfort-dark.svg +NEXT_PUBLIC_NETWORK_NAME=BXN Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.blackfort.network/testnet/rpc +NEXT_PUBLIC_NETWORK_SHORT_NAME=BXN Testnet +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/blackfort.png +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://testnet.blackfortscan.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.celo_alfajores b/explorer/frontend/configs/envs/.env.celo_alfajores new file mode 100644 index 000000000..42aea78fc --- /dev/null +++ b/explorer/frontend/configs/envs/.env.celo_alfajores @@ -0,0 +1,51 @@ +# 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 dev: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_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +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_CELO_ENABLED=true +NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=26369280 +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/celo.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_TRACKER_ENABLED=false +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x9767ce30754afad2a3279b9df2d13257f467c3dad4e0e601271e66d16dfd1641 +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(252, 255, 82, 1)'],'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_MULTIPLE_GAS_CURRENCIES=true +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_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://celo-alfajores.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees'] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.eth b/explorer/frontend/configs/envs/.env.eth new file mode 100644 index 000000000..8a32019f8 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.eth @@ -0,0 +1,70 @@ +# Set of ENVs for Ethereum network explorer +# https://eth.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xd01175f1efa23f36c5579b3c13e2bbd0885017643a7efef5cbcb6b474384dfa8 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=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_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://eth.blockscout.com/apps/revokescout?chainId=1 +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_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}] +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/ethereum/token/price'}},{'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://rpc.eth.gateway.fm +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_REWARDS_SERVICE_API_HOST=https://merits.blockscout.com +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global +NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://eth.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +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':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'Rarible','collection_url':'https://rarible.com/collection/{hash}/items','instance_url':'https://rarible.com/token/{hash}:{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/rarible.png'},{'name':'Blur','collection_url':'https://blur.io/eth/collection/{hash}','instance_url':'https://blur.io/eth/asset/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/blur.png'},{'name':'MagicEden','collection_url':'https://magiceden.io/collections/ethereum/{hash}','instance_url':'https://magiceden.io/item-details/ethereum/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/magiceden.png'}] +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.eth_goerli b/explorer/frontend/configs/envs/.env.eth_goerli new file mode 100644 index 000000000..09cab1823 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.eth_goerli @@ -0,0 +1,56 @@ +# Set of ENVs for Göerli network explorer +# https://eth-goerli.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=eth_goerli" + +# 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': '610111', 'w': '728', 'h': '90' } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ 'id': '610112', 'w': '300', 'h': '100' } +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=eth-goerli.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':'Swapscout','icon':'swap','dappId':'swapscout'},{'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-goerli.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/eth-goerli.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x5fd9325045efc08eef82ff15f59c765cb6df34a30d77b4f4db70ba59a226106a +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +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_MAINTENANCE_ALERT_MESSAGE=

Ethereum Goerli testnet has been deprecated. Migrate to Sepolia and Holesky testnets.

+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=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','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'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/goerli'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg +NEXT_PUBLIC_NETWORK_ID=5 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg +NEXT_PUBLIC_NETWORK_NAME=Göerli +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli +NEXT_PUBLIC_NETWORK_SHORT_NAME=Göerli +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-goerli.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://goerli.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://testnets.opensea.io/assets/goerli/{hash}','instance_url':'https://testnets.opensea.io/assets/goerli/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'Rarible','collection_url':'https://testnet.rarible.com/collection/{hash}/items','instance_url':'https://testnet.rarible.com/token/{hash}:{id}','logo_url':'https://theme.zdassets.com/theme_assets/10342982/b874f2d764307e820514e17252b783f0f344ede6.svg'}] +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.eth_sepolia b/explorer/frontend/configs/envs/.env.eth_sepolia new file mode 100644 index 000000000..1fa16df76 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.eth_sepolia @@ -0,0 +1,73 @@ +# Set of ENVs for Sepolia network explorer +# https://eth-sepolia.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +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/sepolia.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +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_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'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_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html, +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://revoke.blockscout.com?utm_source=blockscout&utm_medium=eth-sepolia, +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_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}] +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/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':'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_REWARDS_SERVICE_API_HOST=https://merits.blockscout.com +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://eth-sepolia.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.filecoin b/explorer/frontend/configs/envs/.env.filecoin new file mode 100644 index 000000000..b2a17bb2d --- /dev/null +++ b/explorer/frontend/configs/envs/.env.filecoin @@ -0,0 +1,51 @@ +# Set of ENVs for Filecoin Virtual Machine network explorer +# https://filecoin.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=filecoin" + +# 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=filecoin.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':'Revokescout','icon':'integration/partial','dappId':'revokescout'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/filecoin-mainnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/fvm.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x5abb6212c1802402b828ed20c2bd4d4a6153b8bee68a5259cba3c8d7a7c6b775 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(237deg, rgb(26, 58, 150) 14.83%, rgb(111, 223, 164) 132.56%)'],'text_color':['rgba(255, 255, 255, 1)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-filecoin.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=FIL +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=FIL +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':'/filecoin/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/filecoin-icon-dark.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/filecoin-icon-light.svg +NEXT_PUBLIC_NETWORK_ID=314 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/filecoin-logo-dark.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/filecoin-logo-light.svg +NEXT_PUBLIC_NETWORK_NAME=Filecoin Virtual Machine +NEXT_PUBLIC_NETWORK_RPC_URL=https://api.node.glif.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=Filecoin Virtual Machine +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/filecoin.png +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://filecoin.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.garnet b/explorer/frontend/configs/envs/.env.garnet new file mode 100644 index 000000000..eaf25df51 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.garnet @@ -0,0 +1,56 @@ +# Set of ENVs for Garnet Testnet network explorer +# https://explorer.garnetchain.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=garnet" + +# 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_BANNER_PROVIDER=none +NEXT_PUBLIC_AD_TEXT_PROVIDER=none +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=explorer.garnetchain.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/redstone.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/redstone.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x5b0ba69f2cf5fbc6da96b6cf475c5521f7a385efd9d68673f69c1fc54f737a52 +NEXT_PUBLIC_HAS_MUD_FRAMEWORK=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgb(169, 31, 47)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://redstone-lattice.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=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet-dark.svg +NEXT_PUBLIC_NETWORK_ID=17069 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Garnet Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.garnetchain.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Garnet Testnet +NEXT_PUBLIC_OG_DESCRIPTION=Redstone is the home for onchain games, worlds, and other MUD applications +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/garnet.png +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-holesky.blockscout.com/ +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://garnet.qry.live/withdraw +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://explorer.garnetchain.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.gnosis b/explorer/frontend/configs/envs/.env.gnosis new file mode 100644 index 000000000..257e950f7 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.gnosis @@ -0,0 +1,76 @@ +# Set of ENVs for Gnosis chain network explorer +# https://gnosis.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x082762f95047d39d612daafec832f88163f3815fde4ddd8944f2a5198a396e0f +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=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_HERO_BANNER_CONFIG={'background':['rgb(46, 74, 60)'],'text_color':['rgb(255, 255, 255)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://login.blockscout.com/v2/logout +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://gnosis.blockscout.com/apps/revokescout?chainId=100 +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_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/marketplace-subgraph-links/subgraph-links.json +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}] +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +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_REWARDS_SERVICE_API_HOST=https://merits.blockscout.com +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-gnosis-chain.safe.global +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://gnosis.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.immutable b/explorer/frontend/configs/envs/.env.immutable new file mode 100644 index 000000000..810203d7c --- /dev/null +++ b/explorer/frontend/configs/envs/.env.immutable @@ -0,0 +1,54 @@ +# Set of ENVs for Immutable network explorer +# https://explorer.immutable.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=immutable" + +# 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_BANNER_PROVIDER=none +NEXT_PUBLIC_AD_TEXT_PROVIDER=none +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=explorer.immutable.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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/immutable-mainnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/immutable.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x6166cece570f4731ccc94c2d17d854ce88496cd3b48e03b537959992ab6685c8 +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['no-repeat center/100% 100% url(https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-skins/immutable.jpg)'],'text_color':['rgba(19, 19, 19, 1)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-immutable.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=IMX +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=IMX +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':'/immutable-zkevm/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/immutable-short.svg +NEXT_PUBLIC_NETWORK_ID=13371 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/immutable.svg +NEXT_PUBLIC_NETWORK_NAME=Immutable +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.immutable.com/ +NEXT_PUBLIC_NETWORK_SHORT_NAME=Immutable +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/immutable.png +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://explorer.immutable.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=["miner"] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.jest b/explorer/frontend/configs/envs/.env.jest new file mode 100644 index 000000000..abe2107a8 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.jest @@ -0,0 +1,52 @@ +# 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_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/explorer/frontend/configs/envs/.env.localhost b/explorer/frontend/configs/envs/.env.localhost new file mode 100644 index 000000000..3956c0d11 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/configs/envs/.env.main b/explorer/frontend/configs/envs/.env.main new file mode 100644 index 000000000..17a181515 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.main @@ -0,0 +1,75 @@ +# 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 dev: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_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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x93e00d4d48cf0dc229f5102e18277fa1bb6130d5b319697a87698a35cf67f706 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'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_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}?utm_source=blockscout&utm_medium=address', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}] +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NAVIGATION_LAYOUT=vertical +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_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global +NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://eth-sepolia.k8s-dev.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=nouns +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer-test.k8s-dev.blockscout.com +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.mekong b/explorer/frontend/configs/envs/.env.mekong new file mode 100644 index 000000000..39c048182 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.mekong @@ -0,0 +1,34 @@ +# Set of ENVs for Mekong network explorer +# https://mekong.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=mekong" + +# 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=mekong.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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x7c7d9e09a5e0e6441a81efe57dbcf08848cd18a1f4238e28152faead390066a4 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_IS_TESTNET=true +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_ID=7078815900 +NEXT_PUBLIC_NETWORK_NAME=Mekong +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.mekong.ethpandaops.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=Mekong +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.neon_devnet b/explorer/frontend/configs/envs/.env.neon_devnet new file mode 100644 index 000000000..89aca9295 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.neon_devnet @@ -0,0 +1,46 @@ +# Set of ENVs for Neon Devnet network explorer +# https://neon-devnet.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=neon_devnet" + +# 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=neon-devnet.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/neon-devnet.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x0716b7a70a1c3b83f731084d7c1449148392512318c2ce0fd812d029204707b5 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(0, rgb(223, 66, 171), rgb(176, 40, 209))'],'text_color':['rgba(255, 255, 255, 1)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-neon.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=NEON +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=NEON +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/neon-short.svg +NEXT_PUBLIC_NETWORK_ID=245022926 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/neon.svg +NEXT_PUBLIC_NETWORK_NAME=Neon Devnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://devnet.neonevm.org +NEXT_PUBLIC_NETWORK_SHORT_NAME=Neon +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/neon-devnet.png +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://neon-devnet.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG={'chain_name':'Solana Devnet','chain_logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/network-icons/solana.svg','explorer_url_template':'https://solscan.io/tx/{hash}?cluster=devnet'} +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.optimism b/explorer/frontend/configs/envs/.env.optimism new file mode 100644 index 000000000..3cf0bf057 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.optimism @@ -0,0 +1,74 @@ +# Set of ENVs for OP Mainnet network explorer +# https://optimism.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_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':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97f34a4cf685e365460dd38dbe16e092d8e4cc4b6ac779e3abcf4c18df6b1329 +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap', 'secondary_coin_price'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)'],'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_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://optimism.blockscout.com/apps/swapscout +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_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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', '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=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/optimism/token/price'}},{'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-mainnet-light.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism-mainnet-dark.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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://optimism.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask'] +NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.optimism_interop_0 b/explorer/frontend/configs/envs/.env.optimism_interop_0 new file mode 100644 index 000000000..0bc99d1c5 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.optimism_interop_0 @@ -0,0 +1,50 @@ +# Set of ENVs for OP Interop Alpha 0 network explorer +# https://optimism-interop-alpha-0.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=optimism_interop_0" + +# 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_BANNER_PROVIDER=none +NEXT_PUBLIC_AD_TEXT_PROVIDER=none +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=optimism-interop-alpha-0.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_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)'],'text_color':['rgb(255, 255, 255)']} +NEXT_PUBLIC_INTEROP_ENABLED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +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=420120000 +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 Interop Alpha 0 +NEXT_PUBLIC_NETWORK_RPC_URL=https://interop-alpha-0.optimism.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Interop Alpha 0 +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://bridge.interop-alpha-0.optimism.io +NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED=false +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://optimism-interop-alpha-0.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_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/explorer/frontend/configs/envs/.env.optimism_sepolia b/explorer/frontend/configs/envs/.env.optimism_sepolia new file mode 100644 index 000000000..313881ba9 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.optimism_sepolia @@ -0,0 +1,56 @@ +# 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 dev: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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x59d26836041ab35169bdce431d68d070b7b8acb589fa52e126e6c828b6ece5e9 +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)'],'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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://optimism-sepolia.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_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/explorer/frontend/configs/envs/.env.polygon b/explorer/frontend/configs/envs/.env.polygon new file mode 100644 index 000000000..8f5e0b69f --- /dev/null +++ b/explorer/frontend/configs/envs/.env.polygon @@ -0,0 +1,63 @@ +# Set of ENVs for Polygon Mainnet network explorer +# https://polygon.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +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_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9 +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)'],'text_color':['rgba(255, 255, 255, 1)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-polygon.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://polygon.blockscout.com/apps/swapscout +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_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POL +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POL +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/polygon/token/price'}},{'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-rpc.com +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_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://polygon.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_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/explorer/frontend/configs/envs/.env.pw b/explorer/frontend/configs/envs/.env.pw new file mode 100644 index 000000000..6229341ea --- /dev/null +++ b/explorer/frontend/configs/envs/.env.pw @@ -0,0 +1,60 @@ +# 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'] +## 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_NAME_SERVICE_API_HOST=http://localhost:3008 +NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=http://localhost:3009 +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx +NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] +NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=tom +NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.rari_testnet b/explorer/frontend/configs/envs/.env.rari_testnet new file mode 100644 index 000000000..8d64dd5e5 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.rari_testnet @@ -0,0 +1,41 @@ +# Set of ENVs for Rari Testnet network explorer +# https://rari-testnet.cloud.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=rari_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_AD_BANNER_PROVIDER=slise +NEXT_PUBLIC_AD_TEXT_PROVIDER=coinzilla +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=rari-testnet.cloud.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_COLOR_THEME_DEFAULT=light +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=radial-gradient(farthest-corner at 0% 0%, rgba(183, 148, 244, 0.80) 0%, rgba(0, 163, 196, 0.80) 100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=[] +NEXT_PUBLIC_NAVIGATION_LAYOUT=vertical +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_ID=1 +NEXT_PUBLIC_NETWORK_NAME=Rari Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://rari-testnet.calderachain.xyz/partner-blockscout +NEXT_PUBLIC_NETWORK_SHORT_NAME=Rari Testnet +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=false +NEXT_PUBLIC_OTHER_LINKS=[] +NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE=0x00000000000000000000000000000000000000ca1de12a9905be97beaf +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://arbitrum-sepolia.blockscout.com/ +NEXT_PUBLIC_ROLLUP_TYPE=arbitrum +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=false +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=jazzicon +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.rootstock_testnet b/explorer/frontend/configs/envs/.env.rootstock_testnet new file mode 100644 index 000000000..bbc89a77f --- /dev/null +++ b/explorer/frontend/configs/envs/.env.rootstock_testnet @@ -0,0 +1,49 @@ +# 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 dev: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_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +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_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/rsk-testnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/rootstock.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x98b25020fa6551a439dfee58fb16ca11d9e93d4cdf15f3f07b697cf08cf11643 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgb(255, 145, 0)'],'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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://rootstock-testnet.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_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.scroll_sepolia b/explorer/frontend/configs/envs/.env.scroll_sepolia new file mode 100644 index 000000000..aa17a2465 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.scroll_sepolia @@ -0,0 +1,46 @@ +# Set of ENVs for Scroll Sepolia Testnet network explorer +# https://scroll-sepolia.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=scroll_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=scroll-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_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/scroll-testnet.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xa0d22caf6217a488b1e97b646c5ed88e8a3020a607bcd1f3fe8d4c430bb19ad5 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(255, 238, 218, 1)'],'text_color':['rgba(25, 6, 2, 1)']} +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/scroll.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/scroll-dark.svg +NEXT_PUBLIC_NETWORK_ID=534351 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/scroll.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/scroll-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Scroll Sepolia Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia-rpc.scroll.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=Scroll Sepolia Testnet +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/scroll-testnet.png +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com +NEXT_PUBLIC_ROLLUP_TYPE=scroll +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://scroll-sepolia.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','scilla'] +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.shibarium b/explorer/frontend/configs/envs/.env.shibarium new file mode 100644 index 000000000..e96f2fb19 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.shibarium @@ -0,0 +1,68 @@ +# Set of ENVs for Shibarium network explorer +# https://www.shibariumscan.io +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=shibarium" + +# 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_BANNER_PROVIDER=hype +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=www.shibariumscan.io +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_APP_INSTANCE=shibarium_mainnet +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':'Revokescout','icon':'integration/partial','dappId':'revokescout'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/shibarium-mainnet.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei', 'usd'] +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xce531d29c0c469fb00b443b8091b8c059b4f13d7e025dd0ef843401d02b9a1a9 +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(180deg, rgba(224, 111, 44, 1) 0%, rgba(228, 144, 52, 1) 100%)'],'text_color':['rgba(255, 255, 255, 1)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://shibarium.us.auth0.com/v2/logout +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.shibariumscan.io/apps/revokescout?chainId=109 +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_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=BONE +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=BONE +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':'/shibarium/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/shibarium-short.png +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/shibarium-short.png +NEXT_PUBLIC_NETWORK_ID=109 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/shibarium-light.png +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/shibarium-dark.png +NEXT_PUBLIC_NETWORK_NAME=Shibarium +NEXT_PUBLIC_NETWORK_RPC_URL=https://www.shibrpc.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Shibarium +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/shibarium-mainnet.png +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_ROLLUP_TYPE=shibarium +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://www.shibariumscan.io +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['miner'] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.stability_testnet b/explorer/frontend/configs/envs/.env.stability_testnet new file mode 100644 index 000000000..864283bdf --- /dev/null +++ b/explorer/frontend/configs/envs/.env.stability_testnet @@ -0,0 +1,56 @@ +# 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 dev: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=explorer.testnet.stabilityprotocol.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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x38125475465a4113a216448af2c9570d0e2c25ef313f8cfbef74f1daad7a97b5 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(46, 51, 81, 1)'],'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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://explorer.testnet.stabilityprotocol.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_TOKEN_SCAM_TOGGLE_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/explorer/frontend/configs/envs/.env.zilliqa_prototestnet b/explorer/frontend/configs/envs/.env.zilliqa_prototestnet new file mode 100644 index 000000000..75830ec3a --- /dev/null +++ b/explorer/frontend/configs/envs/.env.zilliqa_prototestnet @@ -0,0 +1,44 @@ +# Set of ENVs for Zilliqa EVM proto-testnet network explorer +# https://zilliqa-prototestnet.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=zilliqa_prototestnet" + +# 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=zilliqa-prototestnet.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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x3d1ded3a7924cd3256a4b1a447c9bfb194f54b9a8ceb441edb8bb01563b516db +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgba(0, 208, 198, 1) 0.06%, rgba(43, 146, 151, 1) 99.97%)','linear-gradient(90deg, rgba(0, 208, 198, 1) 0.06%, rgba(43, 146, 151, 1) 50.02%, rgba(0, 0, 0, 1) 99.97%)'],'text_color':['rgba(255, 255, 255, 1)','rgba(255, 255, 255, 1)'],'button':{'_default':{'background':['rgba(38, 6, 124, 1)']},'_hover':{'background':['rgba(17, 4, 87, 1)']}}} +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ZIL +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ZIL +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zilliqa.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zilliqa-dark.svg +NEXT_PUBLIC_NETWORK_ID=33103 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zilliqa.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zilliqa-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Zilliqa EVM proto-testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://api.zq2-prototestnet.zilliqa.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Zilliqa EVM proto-testnet +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zilliqa.png +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=zilliqa +NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=zil +NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=["base16", "bech32"] +NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','scilla'] +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.zkevm b/explorer/frontend/configs/envs/.env.zkevm new file mode 100644 index 000000000..3ee02e415 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.zkevm @@ -0,0 +1,60 @@ +# Set of ENVs for Polygon zkEVM network explorer +# https://zkevm.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +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_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zkevm.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9 +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)'],'text_color':['rgba(255, 255, 255, 1)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-polygon.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_METASUITES_ENABLED=true +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +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':'/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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://zkevm.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_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/explorer/frontend/configs/envs/.env.zksync b/explorer/frontend/configs/envs/.env.zksync new file mode 100644 index 000000000..d95638f45 --- /dev/null +++ b/explorer/frontend/configs/envs/.env.zksync @@ -0,0 +1,61 @@ +# Set of ENVs for ZkSync Era network explorer +# https://zksync.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev: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_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +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_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x79c7802ccdf3be5a49c47cc751aad351b0027e8275f6f54878eda50ee559a648 +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(53, 103, 246, 1)'],'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_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_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_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +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_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://zksync.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/configs/envs/.env.zora b/explorer/frontend/configs/envs/.env.zora new file mode 100644 index 000000000..cc6711d1e --- /dev/null +++ b/explorer/frontend/configs/envs/.env.zora @@ -0,0 +1,65 @@ +# Set of ENVs for Zora Mainnet network explorer +# https://explorer.zora.energy +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=zora" + +# 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_BANNER_PROVIDER=none +NEXT_PUBLIC_AD_TEXT_PROVIDER=none +NEXT_PUBLIC_ADDRESS_USERNAME_TAG={'api_url_template': 'https://api.zora.co/discover/user/{address}', 'tag_link_template': 'https://zora.co/{username}', 'tag_icon': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora-zorb.png', 'tag_bg_color': 'rgba(0,0,0)', 'tag_text_color': 'rgba(255,255,255)'} +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=explorer.zora.energy +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':'uniswap'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'}] +NEXT_PUBLIC_DEX_POOLS_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zora.json +NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x6d54c0226a57f5bc854f8aa589bb15113388f984f318c9e1b2722115e4e35873 +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(89deg, rgb(63, 36, 22) 0.56%, rgb(44, 56, 105) 98.31%)']} +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://zora-blockscout.us.auth0.com/v2/logout +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Joined recent campaigns? Mint your Merit Badge here

+NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://explorer.zora.energy/apps/revokescout?chainId=7777777 +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +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_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools'] +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':'/zora-network/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora-dark.svg +NEXT_PUBLIC_NETWORK_ID=7777777 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zora.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zora-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Zora Mainnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.zora.energy +NEXT_PUBLIC_NETWORK_SHORT_NAME=Zora 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/zora-mainnet.png +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.zora.energy +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service +NEXT_PUBLIC_STATS_API_HOST=https://explorer.zora.energy +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/explorer/frontend/decs.d.ts b/explorer/frontend/decs.d.ts new file mode 100644 index 000000000..0f623b644 --- /dev/null +++ b/explorer/frontend/decs.d.ts @@ -0,0 +1,2 @@ +declare module 'react-identicons'; +declare module 'use-font-face-observer'; diff --git a/explorer/frontend/deploy/helmfile.yaml b/explorer/frontend/deploy/helmfile.yaml new file mode 100644 index 000000000..199073c4a --- /dev/null +++ b/explorer/frontend/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.*.* + 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.*.* + namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + labels: + app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + values: + - values/review/values.yaml.gotmpl + - global: + env: "review" diff --git a/explorer/frontend/deploy/scripts/build_sprite.sh b/explorer/frontend/deploy/scripts/build_sprite.sh new file mode 100755 index 000000000..5eace1725 --- /dev/null +++ b/explorer/frontend/deploy/scripts/build_sprite.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +icons_dir="./icons" +target_dir="./public/icons" + +yarn icons build -i $icons_dir -o $target_dir --optimize + +create_registry_file() { + # Create a temporary file to store the registry + local registry_file="$target_dir/registry.json" + + # Start the JSON array + echo "[]" > "$registry_file" + + # Detect OS and set appropriate stat command + get_file_size() { + local file="$1" + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + stat -f%z "$file" + else + # Linux and others + stat -c%s "$file" + fi + } + + # Function to process each file + process_file() { + local file="$1" + local relative_path="${file#$icons_dir/}" + local file_size=$(get_file_size "$file") + + # Create a temporary file with the new entry + jq --arg name "$relative_path" --arg size "$file_size" \ + '. + [{"name": $name, "file_size": ($size|tonumber)}]' \ + "$registry_file" > "${registry_file}.tmp" + + # Move the temporary file back + mv "${registry_file}.tmp" "$registry_file" + } + + # Find all SVG files and process them + find "$icons_dir" -type f -name "*.svg" | while read -r file; do + process_file "$file" + done +} + +# Skip hash creation and renaming for playwright environment +if [ "$NEXT_PUBLIC_APP_ENV" != "pw" ]; then + # Generate hash from the sprite file + HASH=$(md5sum $target_dir/sprite.svg | cut -d' ' -f1 | head -c 8) + + # Remove old sprite files + rm -f $target_dir/sprite.*.svg + + # Rename the new sprite file + mv $target_dir/sprite.svg "$target_dir/sprite.${HASH}.svg" + + export NEXT_PUBLIC_ICON_SPRITE_HASH=${HASH} + + # Skip registry creation in development environment + # just to make the dev build faster + # remove this condition if you want to create the registry file in development environment + if [ "$NEXT_PUBLIC_APP_ENV" != "development" ]; then + create_registry_file + fi + + echo "SVG sprite created: sprite.${HASH}.svg" +else + echo "SVG sprite created: sprite.svg (hash skipped for playwright environment)" +fi \ No newline at end of file diff --git a/explorer/frontend/deploy/scripts/collect_envs.sh b/explorer/frontend/deploy/scripts/collect_envs.sh new file mode 100755 index 000000000..d53dc989e --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/scripts/download_assets.sh b/explorer/frontend/deploy/scripts/download_assets.sh new file mode 100755 index 000000000..a1c31d766 --- /dev/null +++ b/explorer/frontend/deploy/scripts/download_assets.sh @@ -0,0 +1,128 @@ +#!/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_MARKETPLACE_GRAPH_LINKS_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 + if [[ "$url" == http* ]]; then + # Remove query parameters from the URL and get the filename + local filename=$(basename "${url%%\?*}") + # Extract the extension from the filename + local extension="${filename##*.}" + else + local extension="json" + fi + 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 + # Check if the value is a URL + if [[ "$url" == http* ]]; then + # Download the asset using curl with timeouts + if ! curl -f -s --connect-timeout 5 --max-time 15 -o "$destination" "$url"; then + echo " [-] $env_var: Failed to download from $url (timeout or connection error)" + return 1 + fi + else + # Convert single-quoted JSON-like content to valid JSON + json_content=$(echo "${!env_var}" | sed "s/'/\"/g") + + # Save the JSON content to a file + echo "$json_content" > "$destination" + fi + fi + + if [[ "$url" == file://* ]] || [[ "$url" == http* ]]; then + local source_name=$url + else + local source_name="raw input" + fi + + # Check if the download was successful + if [ $? -eq 0 ]; then + echo " [+] $env_var: Successfully saved file from $source_name to $destination." + return 0 + else + echo " [-] $env_var: Failed to save file from $source_name." + 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/explorer/frontend/deploy/scripts/entrypoint.sh b/explorer/frontend/deploy/scripts/entrypoint.sh new file mode 100755 index 000000000..6abcb1fa6 --- /dev/null +++ b/explorer/frontend/deploy/scripts/entrypoint.sh @@ -0,0 +1,74 @@ +#!/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 +if [ "$SKIP_ENVS_VALIDATION" != "true" ]; then + ./validate_envs.sh + if [ $? -ne 0 ]; then + exit 1 + fi +else + echo "😱 Skipping ENVs validation." + echo +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 + +# Generate OG image +node --no-warnings ./og_image_generator.js + +# Create envs.js file with run-time environment variables for the client app +./make_envs_script.sh + +# Generate sitemap.xml and robots.txt files +./sitemap_generator.sh + +# Print list of enabled features +node ./feature-reporter.js + +echo "Starting Next.js application" +exec "$@" diff --git a/explorer/frontend/deploy/scripts/favicon_generator.sh b/explorer/frontend/deploy/scripts/favicon_generator.sh new file mode 100755 index 000000000..7863bda40 --- /dev/null +++ b/explorer/frontend/deploy/scripts/favicon_generator.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +master_url="${FAVICON_MASTER_URL:-$NEXT_PUBLIC_NETWORK_ICON}" +export MASTER_URL="$master_url" + +cd ./deploy/tools/favicon-generator +yarn install --frozen-lockfile +node "$(pwd)/index.js" +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/explorer/frontend/deploy/scripts/make_envs_script.sh b/explorer/frontend/deploy/scripts/make_envs_script.sh new file mode 100755 index 000000000..b3548cb20 --- /dev/null +++ b/explorer/frontend/deploy/scripts/make_envs_script.sh @@ -0,0 +1,39 @@ +#!/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 + # Skip variables that start with NEXT_PUBLIC_VERCEL. Vercel injects these + # and they can cause runtime errors, particularly when commit messages wrap lines. + if [[ $var == NEXT_PUBLIC_VERCEL* ]]; then + continue + fi + + # 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/explorer/frontend/deploy/scripts/og_image_generator.js b/explorer/frontend/deploy/scripts/og_image_generator.js new file mode 100755 index 000000000..6f607376f --- /dev/null +++ b/explorer/frontend/deploy/scripts/og_image_generator.js @@ -0,0 +1,68 @@ +/* eslint-disable no-console */ +import fs from 'fs'; +import path from 'path'; + +console.log('🎨 Generating OG image...'); + +const targetFile = path.resolve(process.cwd(), 'public/static/og_image.png'); + +function copyPlaceholderImage() { + const sourceFile = path.resolve(process.cwd(), 'public/static/og_placeholder.png'); + fs.copyFileSync(sourceFile, targetFile); +} + +if (process.env.NEXT_PUBLIC_OG_IMAGE_URL) { + console.log('⏩ NEXT_PUBLIC_OG_IMAGE_URL is set. Skipping OG image generation...'); +} else if (!process.env.NEXT_PUBLIC_NETWORK_NAME) { + console.log('⏩ NEXT_PUBLIC_NETWORK_NAME is not set. Copying placeholder image...'); + copyPlaceholderImage(); +} else if (!process.env.NEXT_PUBLIC_NETWORK_LOGO && !process.env.NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG) { + console.log('⏩ Neither NEXT_PUBLIC_NETWORK_LOGO nor NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG is set. Copying placeholder image...'); + copyPlaceholderImage(); +} else { + try { + const bannerConfig = JSON.parse(process.env.NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG?.replaceAll('\'', '"') || '{}'); + const data = { + title: `${ process.env.NEXT_PUBLIC_NETWORK_NAME } explorer`, + logo_url: process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK ?? process.env.NEXT_PUBLIC_NETWORK_LOGO, + background: bannerConfig.background?.[0], + title_color: bannerConfig.text_color?.[0], + invert_logo: !process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK, + app_url: process.env.NEXT_PUBLIC_APP_HOST, + }; + + console.log('⏳ Making request to OG image generator service...'); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30_000); + + const response = await fetch('https://bigs.services.blockscout.com/generate/og', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + console.log('⬇️ Downloading the image...'); + const buffer = await response.arrayBuffer(); + const imageBuffer = Buffer.from(buffer); + fs.writeFileSync(targetFile, imageBuffer); + } else { + const payload = response.headers.get('Content-type')?.includes('application/json') ? await response.json() : await response.text(); + console.error('🛑 Failed to generate OG image. Response:', payload); + console.log('Copying placeholder image...'); + copyPlaceholderImage(); + } + } catch (error) { + console.error('🛑 Failed to generate OG image. Error:', error?.message); + console.log('Copying placeholder image...'); + copyPlaceholderImage(); + } +} + +console.log('✅ Done.'); diff --git a/explorer/frontend/deploy/scripts/sitemap_generator.sh b/explorer/frontend/deploy/scripts/sitemap_generator.sh new file mode 100644 index 000000000..fbeb89b51 --- /dev/null +++ b/explorer/frontend/deploy/scripts/sitemap_generator.sh @@ -0,0 +1,2 @@ +cd ./deploy/tools/sitemap-generator +yarn next-sitemap \ No newline at end of file diff --git a/explorer/frontend/deploy/scripts/validate_envs.sh b/explorer/frontend/deploy/scripts/validate_envs.sh new file mode 100755 index 000000000..55211d1c5 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/affected-tests/.gitignore b/explorer/frontend/deploy/tools/affected-tests/.gitignore new file mode 100644 index 000000000..30bc16279 --- /dev/null +++ b/explorer/frontend/deploy/tools/affected-tests/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/affected-tests/index.js b/explorer/frontend/deploy/tools/affected-tests/index.js new file mode 100644 index 000000000..f2fc450eb --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/affected-tests/package.json b/explorer/frontend/deploy/tools/affected-tests/package.json new file mode 100644 index 000000000..bfba5734f --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/affected-tests/yarn.lock b/explorer/frontend/deploy/tools/affected-tests/yarn.lock new file mode 100644 index 000000000..ebfe2bd8b --- /dev/null +++ b/explorer/frontend/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.3: + 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.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + 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.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +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.7" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.7.tgz#0b22032e51a967900e0ae9f32762c23a87036bd0" + integrity "sha1-CyIDLlGpZ5AOCunzJ2LCOocDa9A= sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==" + +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/explorer/frontend/deploy/tools/envs-validator/.gitignore b/explorer/frontend/deploy/tools/envs-validator/.gitignore new file mode 100644 index 000000000..caba20fba --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/index.ts b/explorer/frontend/deploy/tools/envs-validator/index.ts new file mode 100644 index 000000000..98769d1fe --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/index.ts @@ -0,0 +1,201 @@ +/* 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); + + printDeprecationWarning(appEnvs); + await checkPlaceholdersCongruity(appEnvs); + checkDeprecatedEnvs(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_MARKETPLACE_GRAPH_LINKS_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)); + }); + }); +} + +function printDeprecationWarning(envsMap: Record) { + if (envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { + // eslint-disable-next-line max-len + console.warn('❗ The NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable is now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variable.'); + } + + if ( + (envsMap.NEXT_PUBLIC_SENTRY_DSN || envsMap.SENTRY_CSP_REPORT_URI || envsMap.NEXT_PUBLIC_SENTRY_ENABLE_TRACING) && + envsMap.NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN + ) { + // eslint-disable-next-line max-len + console.warn('❗ The Sentry monitoring is now deprecated and will be removed in the next release. Please migrate to the Rollbar error monitoring.'); + } + + if ( + envsMap.NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME || + envsMap.NEXT_PUBLIC_ROLLUP_L1_BASE_URL + ) { + // eslint-disable-next-line max-len + console.warn('❗ The NEXT_PUBLIC_ROLLUP_L1_BASE_URL and NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_ROLLUP_PARENT_CHAIN variable.'); + } + + if ( + envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR || + envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND + ) { + // eslint-disable-next-line max-len + console.warn('❗ The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.'); + } + + if ( + envsMap.NEXT_PUBLIC_AUTH0_CLIENT_ID || + envsMap.NEXT_PUBLIC_AUTH_URL || + envsMap.NEXT_PUBLIC_LOGOUT_URL + ) { + // eslint-disable-next-line max-len + console.warn('❗ The NEXT_PUBLIC_AUTH0_CLIENT_ID, NEXT_PUBLIC_AUTH_URL and NEXT_PUBLIC_LOGOUT_URL variables are now deprecated and will be removed in the next release.'); + } +} + +function checkDeprecatedEnvs(envsMap: Record) { + !silent && console.log(`🌀 Checking deprecated environment variables...`); + + if (!envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { + // eslint-disable-next-line max-len + console.log('🚨 The NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable is no longer supported. Please pass NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY or remove it completely.'); + throw new Error(); + } + + if ( + (envsMap.NEXT_PUBLIC_SENTRY_DSN || envsMap.SENTRY_CSP_REPORT_URI || envsMap.NEXT_PUBLIC_SENTRY_ENABLE_TRACING) && + !envsMap.NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN + ) { + // eslint-disable-next-line max-len + console.log('🚨 The Sentry error monitoring is no longer supported. Please migrate to the Rollbar error monitoring.'); + throw new Error(); + } + + !silent && console.log('👍 All good!\n'); +} diff --git a/explorer/frontend/deploy/tools/envs-validator/package.json b/explorer/frontend/deploy/tools/envs-validator/package.json new file mode 100644 index 000000000..1cc180395 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/schema.ts b/explorer/frontend/deploy/tools/envs-validator/schema.ts new file mode 100644 index 000000000..ebb6517b5 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/schema.ts @@ -0,0 +1,1083 @@ +/* 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 type { AddressProfileAPIConfig } from '../../../types/client/addressProfileAPIConfig'; +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, SMART_CONTRACT_LANGUAGE_FILTERS, 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, HOME_STATS_WIDGET_IDS } from '../../../types/homepage'; +import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig, HomeStatsWidgetId } from '../../../types/homepage'; +import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; +import { COLOR_THEME_IDS } from '../../../types/settings'; +import type { FontFamily } from '../../../types/ui'; +import type { AddressFormat, AddressViewId } from '../../../types/views/address'; +import { ADDRESS_FORMATS, 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 type { VerifiedContractsFilter } from '../../../types/api/contracts'; +import type { TxExternalTxsConfig } from '../../../types/client/externalTxsConfig'; + +import { replaceQuotes } from '../../../configs/app/utils'; +import * as regexp from '../../../toolkit/utils/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 getYupValidationErrorMessage = (error: unknown) => + typeof error === 'object' && + error !== null && + 'errors' in error && + Array.isArray(error.errors) ? + error.errors.join(', ') : + ''; + +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'), + }), + NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL: 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_GRAPH_LINKS_URL 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 parentChainCurrencySchema = yup + .object() + .shape({ + name: yup.string().required(), + symbol: yup.string().required(), + decimals: yup.number().required(), + }); + +const parentChainSchema = yup + .object() + .transform(replaceQuotes) + .json() + .shape({ + id: yup.number(), + name: yup.string(), + baseUrl: yup.string().test(urlTest).required(), + rpcUrls: yup.array().of(yup.string().test(urlTest)), + currency: yup + .mixed() + .test( + 'shape', + (ctx) => { + try { + parentChainCurrencySchema.validateSync(ctx.originalValue); + throw new Error('Unknown validation error'); + } catch (error: unknown) { + const message = getYupValidationErrorMessage(error); + return 'in \"currency\" property ' + (message ? `${ message }` : ''); + } + }, + (data) => { + const isUndefined = data === undefined; + return isUndefined || parentChainCurrencySchema.isValidSync(data); + }, + ), + isTestnet: yup.boolean(), + }); + +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\' '), + }), + NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED: yup + .boolean() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: 'optimistic', + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED can only be used if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' ', + value => value === undefined, + ), + }), + NEXT_PUBLIC_INTEROP_ENABLED: yup + .boolean() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: 'optimistic', + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_INTEROP_ENABLED can only be used if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' ', + value => value === undefined, + ), + }), + NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME: yup + .string() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: 'arbitrum', + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME can only be used if NEXT_PUBLIC_ROLLUP_TYPE is set to \'arbitrum\' ', + value => value === undefined, + ), + }), + NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS: yup + .boolean() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value, + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined', + value => value === undefined, + ), + }), + NEXT_PUBLIC_ROLLUP_PARENT_CHAIN: yup + .mixed() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value, + then: (schema) => { + return schema.test( + 'shape', + (ctx) => { + try { + parentChainSchema.validateSync(ctx.originalValue); + throw new Error('Unknown validation error'); + } catch (error: unknown) { + const message = getYupValidationErrorMessage(error); + return 'Invalid schema were provided for NEXT_PUBLIC_ROLLUP_TYPE' + (message ? `: ${ message }` : ''); + } + }, + (data) => { + const isUndefined = data === undefined; + return isUndefined || parentChainSchema.isValidSync(data); + } + ) + }, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_ROLLUP_PARENT_CHAIN cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined', + value => value === undefined, + ), + }), + NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE: yup + .string() + .min(60) + .max(60) + .matches(regexp.HEX_REGEXP_WITH_0X) + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value === 'arbitrum', + then: (schema) => schema, + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE can only be used if NEXT_PUBLIC_ROLLUP_TYPE is set to \'arbitrum\' '), + }), + NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL: yup + .string() + .test(urlTest) + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value === 'arbitrum' || value === 'optimistic', + then: (schema) => schema, + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL can only be used if NEXT_PUBLIC_ROLLUP_TYPE is set to \'arbitrum\' or \'optimistic\''), + }), + }); + +const celoSchema = yup + .object() + .shape({ + NEXT_PUBLIC_CELO_ENABLED: yup.boolean(), + NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK: yup + .string() + .when('NEXT_PUBLIC_CELO_ENABLED', { + is: (value: boolean) => value, + then: (schema) => schema.min(0).optional(), + otherwise: (schema) => schema.max( + -1, + 'NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK cannot not be used if NEXT_PUBLIC_CELO_ENABLED is not set to "true"', + ), + }), + }); + +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, + }); + +// DEPRECATED +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, + }), + }); + +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, + 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), + 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 fontFamilySchema: yup.ObjectSchema = yup + .object() + .transform(replaceQuotes) + .json() + .shape({ + name: yup.string().required(), + url: yup.string().test(urlTest).required(), + }); + +const heroBannerButtonStateSchema: yup.ObjectSchema = yup.object({ + background: yup.array().max(2).of(yup.string()), + text_color: yup.array().max(2).of(yup.string()), +}); + +const heroBannerSchema: yup.ObjectSchema = yup.object() + .transform(replaceQuotes) + .json() + .shape({ + background: yup.array().max(2).of(yup.string()), + text_color: yup.array().max(2).of(yup.string()), + border: yup.array().max(2).of(yup.string()), + button: yup.object({ + _default: heroBannerButtonStateSchema, + _hover: heroBannerButtonStateSchema, + _selected: heroBannerButtonStateSchema, + }), + }); + +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 multichainProviderConfigSchema: yup.ObjectSchema = yup.object({ + name: yup.string().required(), + url_template: yup.string().required(), + logo: yup.string().required(), + dapp_id: yup.string(), +}); + +const externalTxsConfigSchema: yup.ObjectSchema = yup.object({ + chain_name: yup.string().required(), + chain_logo_url: yup.string().required(), + explorer_url_template: yup.string().required(), +}); + +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(), + NEXT_PUBLIC_APP_ENV: yup.string(), + NEXT_PUBLIC_APP_INSTANCE: yup.string(), + + // 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 + .mixed() + .test( + 'shape', + 'Invalid schema were provided for NEXT_PUBLIC_NETWORK_RPC_URL, it should be either array of URLs or URL string', + (data) => { + const isUrlSchema = yup.string().test(urlTest); + const isArrayOfUrlsSchema = yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().test(urlTest)); + + return isUrlSchema.isValidSync(data) || isArrayOfUrlsSchema.isValidSync(data); + }), + 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' ]) + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value === 'arbitrum' || value === 'zkEvm', + then: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE can not be set for Arbitrum and ZkEVM rollups', + value => value === undefined, + ), + otherwise: (schema) => schema, + }), + 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)) + .test( + 'stats-api-required', + 'NEXT_PUBLIC_STATS_API_HOST is required when daily_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_CHARTS', + function(value) { + // daily_operational_txs is presented only in stats microservice + if (value?.includes('daily_operational_txs')) { + return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST); + } + return true; + } + ), + NEXT_PUBLIC_HOMEPAGE_STATS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(HOME_STATS_WIDGET_IDS)) + .test( + 'stats-api-required', + 'NEXT_PUBLIC_STATS_API_HOST is required when total_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_STATS', + function(value) { + // total_operational_txs is presented only in stats microservice + if (value?.includes('total_operational_txs')) { + return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST); + } + return true; + } + ), + NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), + NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), + NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup + .mixed() + .test( + 'shape', + (ctx) => { + try { + heroBannerSchema.validateSync(ctx.originalValue); + throw new Error('Unknown validation error'); + } catch (error: unknown) { + const message = getYupValidationErrorMessage(error); + return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : ''); + } + }, + (data) => { + const isUndefined = data === undefined; + return isUndefined || heroBannerSchema.isValidSync(data); + }), + + // 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_FORMAT: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(ADDRESS_FORMATS)), + NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX: yup + .string() + .when('NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT', { + is: (value: Array | undefined) => value && value.includes('bech32'), + then: (schema) => schema.required().min(1).max(83), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX is required if NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT contains "bech32"'), + }), + + 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_CONTRACT_LANGUAGE_FILTERS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(SMART_CONTRACT_LANGUAGE_FILTERS)), + + 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), + NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED: yup.boolean(), + NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED: yup.boolean(), + + // 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), + NEXT_PUBLIC_FONT_FAMILY_HEADING: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_HEADING', (data) => { + const isUndefined = data === undefined; + return isUndefined || fontFamilySchema.isValidSync(data); + }), + NEXT_PUBLIC_FONT_FAMILY_BODY: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_BODY', (data) => { + const isUndefined = data === undefined; + return isUndefined || fontFamilySchema.isValidSync(data); + }), + NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED: yup.boolean(), + + // 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 + .array() + .transform(replaceQuotes) + .json() + .of(multichainProviderConfigSchema), + 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_ADVANCED_FILTER_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, + ), + }), + NEXT_PUBLIC_DEX_POOLS_ENABLED: yup.boolean() + .when('NEXT_PUBLIC_CONTRACT_INFO_API_HOST', { + is: (value: string) => Boolean(value), + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_DEX_POOLS_ENABLED can only be used with NEXT_PUBLIC_CONTRACT_INFO_API_HOST', + value => value === undefined, + ), + }), + NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(), + NEXT_PUBLIC_ADDRESS_USERNAME_TAG: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_ADDRESS_USERNAME_TAG, it should have api_url_template', (data) => { + const isUndefined = data === undefined; + const valueSchema = yup.object().transform(replaceQuotes).json().shape({ + api_url_template: yup.string().required(), + tag_link_template: yup.string(), + tag_icon: yup.string(), + tag_bg_color: yup.string(), + tag_text_color: yup.string(), + }); + + return isUndefined || valueSchema.isValidSync(data); + }), + NEXT_PUBLIC_REWARDS_SERVICE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_XSTAR_SCORE_URL: yup.string().test(urlTest), + NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK: yup.string().test(urlTest), + NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG: yup.mixed().test( + 'shape', + 'Invalid schema were provided for NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG, it should have chain_name, chain_logo_url, and explorer_url_template', + (data) => { + const isUndefined = data === undefined; + const valueSchema = yup.object().transform(replaceQuotes).json().shape({ + chain_name: yup.string().required(), + chain_logo_url: yup.string().required(), + explorer_url_template: yup.string().required(), + }); + + return isUndefined || valueSchema.isValidSync(data); + }), + + // 6. External services envs + NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), + NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(), // DEPRECATED + NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), + NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), + NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(), + NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: yup.string(), + + // Misc + NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(), + }) + .concat(accountSchema) + .concat(adsBannerSchema) + .concat(marketplaceSchema) + .concat(rollupSchema) + .concat(celoSchema) + .concat(beaconChainSchema) + .concat(bridgedTokensSchema) + .concat(sentrySchema); + +export default schema; diff --git a/explorer/frontend/deploy/tools/envs-validator/test.sh b/explorer/frontend/deploy/tools/envs-validator/test.sh new file mode 100755 index 000000000..46d3ea3fc --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/.env.adbutler b/explorer/frontend/deploy/tools/envs-validator/test/.env.adbutler new file mode 100644 index 000000000..7877a7740 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/.env.adbutler_add b/explorer/frontend/deploy/tools/envs-validator/test/.env.adbutler_add new file mode 100644 index 000000000..7f1968e4b --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/.env.alt b/explorer/frontend/deploy/tools/envs-validator/test/.env.alt new file mode 100644 index 000000000..11e6bf78f --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/.env.alt @@ -0,0 +1,9 @@ +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none +NEXT_PUBLIC_API_SPEC_URL=none +NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none +NEXT_PUBLIC_HOMEPAGE_STATS=[] +NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] +NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated +NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com'] diff --git a/explorer/frontend/deploy/tools/envs-validator/test/.env.arbitrum b/explorer/frontend/deploy/tools/envs-validator/test/.env.arbitrum new file mode 100644 index 000000000..b9767581c --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/.env.arbitrum @@ -0,0 +1,8 @@ +NEXT_PUBLIC_ROLLUP_TYPE=arbitrum +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com +NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS=true +NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME=DuckChain +NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'baseUrl':'https://explorer.duckchain.io','currency':{'name':'Quack','symbol':'QUACK','decimals':18},'isTestnet':true,'id':42,'name':'DuckChain','rpcUrls':['https://rpc.duckchain.io']} + +NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE=0x00000000000000000000000000000000000000ca1de12a9905be97beaf +NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL=https://mocha.celenium.io/blob \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/envs-validator/test/.env.base b/explorer/frontend/deploy/tools/envs-validator/test/.env.base new file mode 100644 index 000000000..debb88fd4 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/.env.base @@ -0,0 +1,91 @@ +NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN=https://rollbar.com +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 +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_FONT_FAMILY_HEADING={'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'} +NEXT_PUBLIC_FONT_FAMILY_BODY={'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'} +NEXT_PUBLIC_FOOTER_LINKS=https://example.com +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d +NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false +NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false +NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED=false +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch'] +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff' +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)' +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']} +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_FORMAT=['base16'] +NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts'] +NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry'] +NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','scilla'] +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'} +NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true +NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://example.com diff --git a/explorer/frontend/deploy/tools/envs-validator/test/.env.beacon_chain b/explorer/frontend/deploy/tools/envs-validator/test/.env.beacon_chain new file mode 100644 index 000000000..f0800e9af --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/.env.celo b/explorer/frontend/deploy/tools/envs-validator/test/.env.celo new file mode 100644 index 000000000..108204420 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/.env.celo @@ -0,0 +1,2 @@ +NEXT_PUBLIC_CELO_ENABLED=true +NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=420 \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/envs-validator/test/.env.common b/explorer/frontend/deploy/tools/envs-validator/test/.env.common new file mode 100644 index 000000000..5788f392d --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/.env.external_txs b/explorer/frontend/deploy/tools/envs-validator/test/.env.external_txs new file mode 100644 index 000000000..97129b0e6 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/.env.external_txs @@ -0,0 +1 @@ +NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG={'chain_name':'Solana','chain_logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/network-icons/solana.svg','explorer_url_template':'https://solscan.io/tx/{hash}'} \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/envs-validator/test/.env.marketplace b/explorer/frontend/deploy/tools/envs-validator/test/.env.marketplace new file mode 100644 index 000000000..6cc6b1f83 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/.env.optimism b/explorer/frontend/deploy/tools/envs-validator/test/.env.optimism new file mode 100644 index 000000000..32d36f397 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/.env.optimism @@ -0,0 +1,8 @@ +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 +NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS=true +NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED=false +NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'baseUrl':'https://explorer.duckchain.io'} +NEXT_PUBLIC_INTEROP_ENABLED=true \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/featured_networks.json b/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/featured_networks.json new file mode 100644 index 000000000..83b908884 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/footer_links.json b/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/footer_links.json new file mode 100644 index 000000000..d3f7e7d43 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json b/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json new file mode 100644 index 000000000..15b31a555 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json @@ -0,0 +1,5 @@ +[ + "Swaps", + "Bridges", + "NFT" +] diff --git a/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json b/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json new file mode 100644 index 000000000..0b142b7a2 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json b/explorer/frontend/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json new file mode 100644 index 000000000..cf0f481ae --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/tsconfig.json b/explorer/frontend/deploy/tools/envs-validator/tsconfig.json new file mode 100644 index 000000000..f7911a9e1 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "target": "es2016", + "module": "CommonJS", + "moduleResolution": "node", + "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/explorer/frontend/deploy/tools/envs-validator/webpack.config.js b/explorer/frontend/deploy/tools/envs-validator/webpack.config.js new file mode 100644 index 000000000..b62c19d07 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/envs-validator/yarn.lock b/explorer/frontend/deploy/tools/envs-validator/yarn.lock new file mode 100644 index 000000000..2e8a42d34 --- /dev/null +++ b/explorer/frontend/deploy/tools/envs-validator/yarn.lock @@ -0,0 +1,989 @@ +# 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/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@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/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@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/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@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.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + 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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +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.3: + 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.21.10: + version "4.24.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +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.30001669: + version "1.0.30001673" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e" + integrity sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw== + +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.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + 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.5.41: + version "1.5.47" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7" + integrity sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ== + +enhanced-resolve@^5.0.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" + +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + 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.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +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.11, 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== + +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.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + 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.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +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.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +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.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + 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.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.36.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" + integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== + 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.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.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.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + 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.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + 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.11" + 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.10" + watchpack "^2.4.1" + 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/explorer/frontend/deploy/tools/favicon-generator/.gitignore b/explorer/frontend/deploy/tools/favicon-generator/.gitignore new file mode 100644 index 000000000..8b8c2bf2a --- /dev/null +++ b/explorer/frontend/deploy/tools/favicon-generator/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/public +.env +/output \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/favicon-generator/index.js b/explorer/frontend/deploy/tools/favicon-generator/index.js new file mode 100644 index 000000000..b552a9f6c --- /dev/null +++ b/explorer/frontend/deploy/tools/favicon-generator/index.js @@ -0,0 +1,71 @@ +/* eslint-disable no-console */ +const { favicons } = require('favicons'); +const fs = require('fs/promises'); +const path = require('path'); + +generateFavicons(); + +async function generateFavicons() { + console.log('Generating favicons...'); + const masterUrl = process.env.MASTER_URL; + try { + if (!masterUrl) { + throw new Error('FAVICON_MASTER_URL or NEXT_PUBLIC_NETWORK_ICON must be set'); + } + + const fetch = await import('node-fetch'); + const response = await fetch.default(masterUrl); + const buffer = await response.arrayBuffer(); + const source = Buffer.from(buffer); + + const configuration = { + path: '/output', + appName: 'Blockscout', + icons: { + android: true, + appleIcon: { + background: 'transparent', + }, + appleStartup: false, + favicons: true, + windows: false, + yandex: false, + }, + }; + + try { + const result = await favicons(source, configuration); + + const outputDir = path.resolve(process.cwd(), 'output'); + await fs.mkdir(outputDir, { recursive: true }); + + for (const image of result.images) { + // keep only necessary files + if (image.name === 'apple-touch-icon-180x180.png' || image.name === 'android-chrome-192x192.png' || + (!image.name.startsWith('apple-touch-icon') && !image.name.startsWith('android-chrome')) + ) { + await fs.writeFile(path.join(outputDir, image.name), image.contents); + } + + // copy android-chrome-192x192.png to logo-icon.png for marketing purposes + if (image.name === 'android-chrome-192x192.png') { + await fs.writeFile(path.join(outputDir, 'logo-icon.png'), image.contents); + } + } + + for (const file of result.files) { + if (file.name !== 'manifest.webmanifest') { + await fs.writeFile(path.join(outputDir, file.name), file.contents); + } + } + + console.log('Favicons generated successfully!'); + } catch (faviconError) { + console.error('Error generating favicons:', faviconError); + process.exit(1); + } + } catch (error) { + console.error('Error in favicon generation process:', error); + process.exit(1); + } +} diff --git a/explorer/frontend/deploy/tools/favicon-generator/package.json b/explorer/frontend/deploy/tools/favicon-generator/package.json new file mode 100644 index 000000000..f4127ccc3 --- /dev/null +++ b/explorer/frontend/deploy/tools/favicon-generator/package.json @@ -0,0 +1,20 @@ +{ + "name": "favicon-generator", + "version": "1.0.0", + "main": "index.js", + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "favicons": "^7.2.0", + "ts-loader": "^9.4.4", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "devDependencies": { + "dotenv-cli": "^7.4.2", + "node-loader": "^2.0.0", + "tsconfig-paths-webpack-plugin": "^4.1.0" + } +} diff --git a/explorer/frontend/deploy/tools/favicon-generator/yarn.lock b/explorer/frontend/deploy/tools/favicon-generator/yarn.lock new file mode 100644 index 000000000..11198f8e1 --- /dev/null +++ b/explorer/frontend/deploy/tools/favicon-generator/yarn.lock @@ -0,0 +1,1178 @@ +# 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.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@emnapi/runtime@^1.2.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" + integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== + dependencies: + tslib "^2.4.0" + +"@img/sharp-darwin-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz" + integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.0.4" + +"@img/sharp-darwin-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" + integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.0.4" + +"@img/sharp-libvips-darwin-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz" + integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== + +"@img/sharp-libvips-darwin-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" + integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== + +"@img/sharp-libvips-linux-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" + integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== + +"@img/sharp-libvips-linux-arm@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" + integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== + +"@img/sharp-libvips-linux-s390x@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" + integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== + +"@img/sharp-libvips-linux-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" + integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== + +"@img/sharp-libvips-linuxmusl-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" + integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== + +"@img/sharp-libvips-linuxmusl-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" + integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== + +"@img/sharp-linux-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" + integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.0.4" + +"@img/sharp-linux-arm@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" + integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.0.5" + +"@img/sharp-linux-s390x@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" + integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.0.4" + +"@img/sharp-linux-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" + integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.0.4" + +"@img/sharp-linuxmusl-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" + integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + +"@img/sharp-linuxmusl-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" + integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + +"@img/sharp-wasm32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" + integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== + dependencies: + "@emnapi/runtime" "^1.2.0" + +"@img/sharp-win32-ia32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" + integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== + +"@img/sharp-win32-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" + integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@types/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/json-schema@^7.0.8": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*": + version "22.7.9" + resolved "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz" + integrity sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg== + dependencies: + undici-types "~6.19.2" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + 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.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz" + 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.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.13.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + 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.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.21.10: + version "4.24.2" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +caniuse-lite@^1.0.30001669: + version "1.0.30001669" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz" + integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + 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.4" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" + 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.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +detect-libc@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +dotenv-cli@^7.4.2: + version "7.4.2" + resolved "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.4.2.tgz" + integrity sha512-SbUj8l61zIbzyhIbg0FwPJq6+wjbzdn9oEtozQpZ6kW2ihCcapKVZj49oCT3oPM+mgQm+itgvUQcG5szxVrZTA== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.3.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@^16.3.0: + version "16.4.5" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +electron-to-chromium@^1.5.41: + version "1.5.43" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz" + integrity sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: + version "5.17.1" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + 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.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +favicons@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/favicons/-/favicons-7.2.0.tgz" + integrity sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw== + dependencies: + escape-html "^1.0.3" + sharp "^0.33.1" + xml2js "^0.6.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" + 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.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json5@^2.1.2, json5@^2.2.2: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^4.0.0: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-2.0.0.tgz#9109a6d828703fd3e0aa03c1baec12a798071562" + integrity sha512-I5VN34NO4/5UYJaUBtkrODPWxbobrE4hgDqPrjB25yPkonFhCmZ146vTH+Zg417E9Iwoh1l/MbRs1apc5J295Q== + dependencies: + loader-utils "^2.0.0" + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + 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.npmjs.org/p-try/-/p-try-2.2.0.tgz" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + 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.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.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.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +sax@>=0.6.0: + version "1.4.1" + resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" + 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, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +sharp@^0.33.1: + version "0.33.5" + resolved "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz" + integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.6.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + 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.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + 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.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.36.0" + resolved "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz" + integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== + 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.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-loader@^9.4.4: + version "9.5.1" + resolved "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz" + integrity sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + source-map "^0.7.4" + +tsconfig-paths-webpack-plugin@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz" + 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.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.4.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@^5.1.4: + version "5.1.4" + resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz" + 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.10.0" + resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.88.2: + version "5.95.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + 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.11" + 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.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +xml2js@^0.6.1: + version "0.6.2" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== diff --git a/explorer/frontend/deploy/tools/feature-reporter/.gitignore b/explorer/frontend/deploy/tools/feature-reporter/.gitignore new file mode 100644 index 000000000..cefc90f67 --- /dev/null +++ b/explorer/frontend/deploy/tools/feature-reporter/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/build +index.js \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/feature-reporter/dev.sh b/explorer/frontend/deploy/tools/feature-reporter/dev.sh new file mode 100755 index 000000000..183698a81 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/feature-reporter/entry.js b/explorer/frontend/deploy/tools/feature-reporter/entry.js new file mode 100644 index 000000000..01a298903 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/feature-reporter/package.json b/explorer/frontend/deploy/tools/feature-reporter/package.json new file mode 100644 index 000000000..136886831 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/feature-reporter/tsconfig.json b/explorer/frontend/deploy/tools/feature-reporter/tsconfig.json new file mode 100644 index 000000000..86b706822 --- /dev/null +++ b/explorer/frontend/deploy/tools/feature-reporter/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "module": "CommonJS", + "moduleResolution": "node", + "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/explorer/frontend/deploy/tools/feature-reporter/webpack.config.js b/explorer/frontend/deploy/tools/feature-reporter/webpack.config.js new file mode 100644 index 000000000..41363dfee --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/deploy/tools/feature-reporter/yarn.lock b/explorer/frontend/deploy/tools/feature-reporter/yarn.lock new file mode 100644 index 000000000..7eeb603bb --- /dev/null +++ b/explorer/frontend/deploy/tools/feature-reporter/yarn.lock @@ -0,0 +1,1060 @@ +# 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.20": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@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/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity "sha1-Yo7/7q4gZKG055946B2Ht+X8e1A= sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + +"@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.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + 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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@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-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +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.3, 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.21.10: + version "4.24.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity "sha1-9YRbyRBp29Ve6J+vmCLh2IXRZYA= sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==" + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +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.30001669: + version "1.0.30001673" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e" + integrity "sha1-WqKRVXrxxxNA6AmYc2dBCqt6Wp4= sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==" + +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.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + 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.5.41: + version "1.5.47" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7" + integrity "sha1-7wdRvBmyi+juRM2EBTCd4787IMc= sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ==" + +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + 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.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +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.11, 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== + +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.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + 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.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +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.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s= sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + +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.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + 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.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.36.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" + integrity "sha1-iw2+1FmsQP97TJ/Vo6ICneEFGA4= sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==" + 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.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity "sha1-gIRvuh156CVH+2YfjRQeCUV1X+U= sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==" + dependencies: + escalade "^3.2.0" + picocolors "^1.1.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.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity "sha1-L+6u1nQS58MxhOWnnKc4+9OFZNo= sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==" + 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.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity "sha1-j9jEVPpg2tGG++NsQApVhIMHtMA= sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==" + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + 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.11" + 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.10" + watchpack "^2.4.1" + 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/explorer/frontend/deploy/tools/sitemap-generator/.gitignore b/explorer/frontend/deploy/tools/sitemap-generator/.gitignore new file mode 100644 index 000000000..30bc16279 --- /dev/null +++ b/explorer/frontend/deploy/tools/sitemap-generator/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/explorer/frontend/deploy/tools/sitemap-generator/next-sitemap.config.js b/explorer/frontend/deploy/tools/sitemap-generator/next-sitemap.config.js new file mode 100644 index 000000000..820046fc4 --- /dev/null +++ b/explorer/frontend/deploy/tools/sitemap-generator/next-sitemap.config.js @@ -0,0 +1,203 @@ +/* eslint-disable no-console */ +const path = require('path'); + +const stripTrailingSlash = (str) => str[str.length - 1] === '/' ? str.slice(0, -1) : str; + +const fetchResource = async(url, formatter) => { + console.log('🌀 [next-sitemap] Fetching resource:', url); + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 15_000); + + const res = await fetch(url, { + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (res.ok) { + const data = await res.json(); + console.log('✅ [next-sitemap] Data fetched for resource:', url); + return formatter(data); + } + } catch (error) { + if (error.name === 'AbortError') { + console.log('🚨 [next-sitemap] Request timeout for resource:', url); + } else { + console.log('🚨 [next-sitemap] Error fetching resource:', url, error); + } + } +}; + +const siteUrl = [ + process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https', + '://', + process.env.NEXT_PUBLIC_APP_HOST, + process.env.NEXT_PUBLIC_APP_PORT && ':' + process.env.NEXT_PUBLIC_APP_PORT, +].filter(Boolean).join(''); + +const apiUrl = (() => { + const baseUrl = [ + process.env.NEXT_PUBLIC_API_PROTOCOL || 'https', + '://', + process.env.NEXT_PUBLIC_API_HOST, + process.env.NEXT_PUBLIC_API_PORT && ':' + process.env.NEXT_PUBLIC_API_PORT, + ].filter(Boolean).join(''); + + const basePath = stripTrailingSlash(process.env.NEXT_PUBLIC_API_BASE_PATH || ''); + + return `${ baseUrl }${ basePath }/api/v2`; +})(); + +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl, + generateIndexSitemap: false, + generateRobotsTxt: true, + robotsTxtOptions: { + policies: [ + { + userAgent: '*', + allow: '/', + disallow: ['/auth/*', '/login', '/chakra', '/sprite', '/account/*'], + }, + ], + }, + sourceDir: path.resolve(process.cwd(), '../../../.next'), + outDir: path.resolve(process.cwd(), '../../../public'), + exclude: [ + '/account/*', + '/auth/*', + '/login', + '/sprite', + '/chakra', + ], + transform: async(config, path) => { + switch (path) { + case '/mud-worlds': + if (process.env.NEXT_PUBLIC_HAS_MUD_FRAMEWORK !== 'true') { + return null; + } + break; + case '/batches': + case '/deposits': + if (!process.env.NEXT_PUBLIC_ROLLUP_TYPE) { + return null; + } + break; + case '/withdrawals': + if (!process.env.NEXT_PUBLIC_ROLLUP_TYPE && process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN !== 'true') { + return null; + } + break; + case '/dispute-games': + if (process.env.NEXT_PUBLIC_ROLLUP_TYPE !== 'optimistic') { + return null; + } + break; + case '/blobs': + if (process.env.NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED !== 'true') { + return null; + } + break; + case '/name-domains': + if (!process.env.NEXT_PUBLIC_NAME_SERVICE_API_HOST) { + return null; + } + break; + case '/ops': + if (process.env.NEXT_PUBLIC_HAS_USER_OPS !== 'true') { + return null; + } + break; + case '/output-roots': + if (process.env.NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED !== 'true') { + return null; + } + break; + case '/interop-messages': + if (process.env.NEXT_PUBLIC_INTEROP_ENABLED !== 'true') { + return null; + } + break; + case '/pools': + if (process.env.NEXT_PUBLIC_DEX_POOLS_ENABLED !== 'true') { + return null; + } + break; + case '/advanced-filter': + if (process.env.NEXT_PUBLIC_ADVANCED_FILTER_ENABLED === 'false') { + return null; + } + break; + case '/apps': + if (process.env.NEXT_PUBLIC_MARKETPLACE_ENABLED !== 'true') { + return null; + } + break; + case '/api-docs': + if (process.env.NEXT_PUBLIC_API_SPEC_URL === 'none') { + return null; + } + break; + case '/gas-tracker': + if (process.env.NEXT_PUBLIC_GAS_TRACKER_ENABLED === 'false') { + return null; + } + break; + case '/graphql': + if (process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION === 'none') { + return null; + } + break; + case '/stats': + if (!process.env.NEXT_PUBLIC_STATS_API_HOST) { + return null; + } + break; + case '/validators': + if (!process.env.NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE) { + return null; + } + break; + } + + return { + loc: path, + changefreq: undefined, + priority: undefined, + lastmod: config.autoLastmod ? new Date().toISOString() : undefined, + alternateRefs: config.alternateRefs ?? [], + }; + }, + additionalPaths: async(config) => { + const addresses = fetchResource( + `${ apiUrl }/addresses`, + (data) => data.items.map(({ hash }) => `/address/${ hash }`), + ); + const txs = fetchResource( + `${ apiUrl }/transactions?filter=validated`, + (data) => data.items.map(({ hash }) => `/tx/${ hash }`), + ); + const blocks = fetchResource( + `${ apiUrl }/blocks?type=block`, + (data) => data.items.map(({ height }) => `/block/${ height }`), + ); + const tokens = fetchResource( + `${ apiUrl }/tokens`, + (data) => data.items.map(({ address }) => `/token/${ address }`), + ); + const contracts = fetchResource( + `${ apiUrl }/smart-contracts`, + (data) => data.items.map(({ address }) => `/address/${ address.hash }?tab=contract`), + ); + + return Promise.all([ + ...(await addresses || []), + ...(await txs || []), + ...(await blocks || []), + ...(await tokens || []), + ...(await contracts || []), + ].map(path => config.transform(config, path))); + }, +}; diff --git a/explorer/frontend/deploy/tools/sitemap-generator/package.json b/explorer/frontend/deploy/tools/sitemap-generator/package.json new file mode 100644 index 000000000..903f7a326 --- /dev/null +++ b/explorer/frontend/deploy/tools/sitemap-generator/package.json @@ -0,0 +1,12 @@ +{ + "name": "sitemap-generator", + "version": "1.0.0", + "main": "index.js", + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "next-sitemap": "4.2.3" + } +} diff --git a/explorer/frontend/deploy/tools/sitemap-generator/yarn.lock b/explorer/frontend/deploy/tools/sitemap-generator/yarn.lock new file mode 100644 index 000000000..165c1f047 --- /dev/null +++ b/explorer/frontend/deploy/tools/sitemap-generator/yarn.lock @@ -0,0 +1,147 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@corex/deepmerge@^4.0.43": + version "4.0.43" + resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-4.0.43.tgz#9bd42559ebb41cc5a7fb7cfeea5f231c20977dca" + integrity sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ== + +"@next/env@^13.4.3": + version "13.5.8" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.8.tgz#404d3b3e5881b6a0510500c6cc97e3589a2e6371" + integrity sha512-YmiG58BqyZ2FjrF2+5uZExL2BrLr8RTQzLXNDJ8pJr0O+rPlOeDPXp1p1/4OrR3avDidzZo3D8QO2cuDv1KCkw== + +"@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" + +braces@^3.0.3: + 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" + +fast-glob@^3.2.12: + 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.18.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0" + integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== + 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" + +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" + +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: + 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== + +merge2@^1.3.0: + 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.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +next-sitemap@4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/next-sitemap/-/next-sitemap-4.2.3.tgz#5db3f650351a51e84b9fd6b58c5af2f9257b5058" + integrity sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ== + dependencies: + "@corex/deepmerge" "^4.0.43" + "@next/env" "^13.4.3" + fast-glob "^3.2.12" + minimist "^1.2.8" + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +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== + +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" + +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" diff --git a/explorer/frontend/deploy/values/review-l2/values.yaml.gotmpl b/explorer/frontend/deploy/values/review-l2/values.yaml.gotmpl new file mode 100644 index 000000000..3a9173849 --- /dev/null +++ b/explorer/frontend/deploy/values/review-l2/values.yaml.gotmpl @@ -0,0 +1,79 @@ +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: review + 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_ROLLUP_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw + 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_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_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 + NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY diff --git a/explorer/frontend/deploy/values/review/values.yaml.gotmpl b/explorer/frontend/deploy/values/review/values.yaml.gotmpl new file mode 100644 index 000000000..ecc6d34ac --- /dev/null +++ b/explorer/frontend/deploy/values/review/values.yaml.gotmpl @@ -0,0 +1,89 @@ +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: review + 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_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_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']" + PROMETHEUS_METRICS_ENABLED: true + NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true + NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED: true + envFromSecret: + 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_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 + 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_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY + NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN diff --git a/explorer/frontend/docs/BUILD-TIME_ENVS.md b/explorer/frontend/docs/BUILD-TIME_ENVS.md new file mode 100644 index 000000000..6ecaa5398 --- /dev/null +++ b/explorer/frontend/docs/BUILD-TIME_ENVS.md @@ -0,0 +1,10 @@ +# 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 of 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` | +| NEXT_PUBLIC_ICON_SPRITE_HASH | `string` | Hash post-fix of the SVG sprite file (generated automatically during the sprite build) | `08be4b10` | `true` | diff --git a/explorer/frontend/docs/CONTRIBUTING.md b/explorer/frontend/docs/CONTRIBUTING.md new file mode 100644 index 000000000..0897bcf31 --- /dev/null +++ b/explorer/frontend/docs/CONTRIBUTING.md @@ -0,0 +1,219 @@ +# Contribution guide + +Thanks for showing interest to contribute to Blockscout. The following steps will get you up and running. + +  + +## Our guidelines: what we are looking for + +We welcome contributions that enhance the project and improve the overall quality of our codebase. While we appreciate the effort that goes into making contributions, we kindly ask that contributors focus on the following types of changes: +- **Feature Enhancements:** Substantial improvements or new features that add significant value to the project. +- **Bug Fixes:** Fixes for known bugs or issues that impact functionality. +- **Documentation Improvements:** Comprehensive updates to documentation that clarify usage, installation, or project structure. +- **Performance Improvements:** Changes that enhance the performance or efficiency of the application. + +Please note that we accept contributions for newly submitted issues or those labeled "Available for contribution" - all other issues are reserved for the core team of the project. + +  + +## 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 you 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 it 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 are 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 tests 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 the 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/explorer/frontend/docs/CUSTOM_BUILD.md b/explorer/frontend/docs/CUSTOM_BUILD.md new file mode 100644 index 000000000..9142e1284 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/docs/DEPRECATED_ENVS.md b/explorer/frontend/docs/DEPRECATED_ENVS.md new file mode 100644 index 000000000..171df270f --- /dev/null +++ b/explorer/frontend/docs/DEPRECATED_ENVS.md @@ -0,0 +1,14 @@ +# 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 | +| FAVICON_GENERATOR_API_KEY | `string` | RealFaviconGenerator [API key](https://realfavicongenerator.net/api/) | Required | - | `` | v1.16.0+ | v1.37.0 | We don't use RealFaviconGenerator anymore | +| 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 | +| 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+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS | diff --git a/explorer/frontend/docs/ENVS.md b/explorer/frontend/docs/ENVS.md new file mode 100644 index 000000000..a39057845 --- /dev/null +++ b/explorer/frontend/docs/ENVS.md @@ -0,0 +1,919 @@ +# 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). + +## Read before you run the app + +### Variables compulsoriness +Please note that in the tables below, the "Compulsoriness" column indicates whether the variable is required for starting up the application, except for the "App Features" section. All features are optional by definition; therefore, the "Compulsoriness" column indicates whether a certain variable is required or optional only within the context of that feature, not for the entire application. + +### 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., 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. + +### Note about escaping variables values +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) + +  + +## 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) + - [Advanced filter](ENVS.md#advanced-filter) + - [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) + - [Address profile API](ENVS.md#address-profile-api) + - [Address XStar XHS score](ENVS.md#address-xstar-xhs-score) + - [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) + - [Rollbar error monitoring](ENVS.md#rollbar-error-monitoring) + - [OpenTelemetry](ENVS.md#opentelemetry) + - [DeFi dropdown](ENVS.md#defi-dropdown) + - [Multichain balance button](ENVS.md#multichain-balance-button) + - [Get gas button](ENVS.md#get-gas-button) + - [Save on gas with GasHawk](ENVS.md#save-on-gas-with-gashawk) + - [Rewards service API](ENVS.md#rewards-service-api) + - [DEX pools](ENVS.md#dex-pools) +- [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_APP_ENV | `string` | App env (e.g development, staging, production, etc.). | - | `production` | `staging` | v1.0.x+ | +| NEXT_PUBLIC_APP_INSTANCE | `string` | Name of app instance. Used for app monitoring purposes. If not provided, it will be constructed from `NEXT_PUBLIC_APP_HOST` | - | - | `wonderful_kepler` | 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 \| Array` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference. Can contain a single string value, or an array of urls. | - | - | `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` \| `mining` | Verification type in the network. Irrelevant for Arbitrum (verification type is always `posting`) and ZkEvm (verification type is always `sequencing`) L2s | - | `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' \| 'daily_operational_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_STATS | `Array<'latest_batch' \| 'total_blocks' \| 'average_block_time' \| 'total_txs' \| 'total_operational_txs' \| 'latest_l1_state_batch' \| 'wallet_addresses' \| 'gas_tracker' \| 'btc_locked' \| 'current_epoch'>` | List of stats widgets displayed on the home page | - | For zkSync, zkEvm and Arbitrum rollups: `['latest_batch','average_block_time','total_txs','wallet_addresses','gas_tracker']`, for other cases: `['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker']` | `['total_blocks','total_txs','wallet_addresses']` | v1.35.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). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` 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). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` 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_HERO_BANNER_CONFIG | `HeroBannerConfig`, see details [below](#hero-banner-configuration-properties) | Configuration of hero banner appearance. | - | - | See [below](#hero-banner-configuration-properties) | v1.35.0+ | + +#### Hero banner configuration properties + +_Note_ Here, all values are arrays of up to two strings. The first string represents the value for the light color mode, and the second string represents the value for the dark color mode. If the array contains only one string, it will be used for both color modes. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| background | `[string, string]` | Banner background (could be a solid color, gradient or picture). The string should be a valid `background` CSS property value. | - | `['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)']` | `['lightpink','no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)']` | +| text_color | `[string, string]` | Banner text background. The string should be a valid `color` CSS property value. | - | `['white']` | `['lightpink','#DCFE76']` | +| border | `[string, string]` | Banner border. The string should be a valid `border` CSS property value. | - | - | `['1px solid yellow','4px dashed #DCFE76']` | +| button | `Partial>` | The button on the banner. It has three possible states: `_default`, `_hover`, and `_selected`. The `_selected` state reflects when the user is logged in or their wallet is connected to the app. | - | - | `{'_default':{'background':['deeppink'],'text_color':['white']}}` | + +  + +### 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) or file content string representation. It 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` \| `[{'title':'Astar(EVM)','url':'https://astar.blockscout.com/','group':'Mainnets','icon':'https://example.com/astar.svg'}]` | 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) or file content string representation. It 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` \| `[{'title':'My chain','links':[{'text':'About','url':'https://example.com/about'},{'text':'Contacts','url':'https://example.com/contacts'}]}]` | 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 variable. 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_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 | - | - | `Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the %network_title% network.` | 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 | +| --- | --- | +| `base_fee` | Base fee | +| `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" \| "nouns"` | 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), [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) and [Nouns](https://nouns.wtf) | - | `jazzicon` | `gradient_avatar` | v1.12.0+ | +| NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT | `Array<"base16" \| "bech32">` | Displayed address format, could be either `base16` standard or [`bech32`](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32) standard. If the array contains multiple values, the address format toggle will appear in the UI, allowing the user to switch between formats. The first item in the array will be the default format. | - | `'["base16"]'` | `'["bech32", "base16"]'` | v1.36.0+ | +| NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX | `string` | Human-readable prefix of `bech32` address format. | Required, if `NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT` contains "bech32" value | - | `duck` | v1.36.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+ | +| NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS | `Array<'solidity' \| 'vyper' \| 'yul' \| 'scilla'>` | Pass an array of contract languages that will be displayed as options in the filter on the verified contract page. | - | `['solidity','vyper','yul']` | `['solidity','vyper','yul','scilla']` | v1.37.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 | + +  + +#### Token views +| Variable | Type | Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED | `boolean` | Show the "Hide scam tokens" toggle in the site settings dropdown. This option controls the visibility of tokens with a poor reputation in the search results. | - | `false` | `true` | v1.38.0+ | + +  + +#### 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+ | +| NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED | `boolean` | Indicates that the [Helia verified fetch](https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch) should be used for retrieving content of NFT assets (currently limited to images) directly from IPFS network using trustless gateways. | - | `true` | `false` | v1.37.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+ | +| NEXT_PUBLIC_FONT_FAMILY_HEADING | `FontFamily`, see full description [below](#font-family-configuration-properties) | Special typeface to use in page headings (`

`, `

`, etc.) | - | - | `{'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}` | v1.35.0+ | +| NEXT_PUBLIC_FONT_FAMILY_BODY | `FontFamily`, see full description [below](#font-family-configuration-properties) | Main typeface to use in page content elements. | - | - | `{'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'}` | v1.35.0+ | +| NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED | `boolean` | Set to `true` to restrict the page content width on extra-large screens. | - | `true` | `false` | v1.34.1+ | + +#### 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` | + +#### Font family configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| name | `string` | Font family name; used to define the `font-family` CSS property. | Required | - | `Montserrat` | +| url | `string` | URL for external font. Ensure the font supports the following weights: 400, 500, 600, and 700. | Required | - | `https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap` | + +  + +## 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 entire 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_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `` | v1.0.x+ | +| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `` | v1.0.x+ | +| NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** 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 | - | - | `https://blockscout.com` | v1.0.x+ | +| NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `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+ | + +  + +### Advanced filter + +This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_ADVANCED_FILTER_ENABLED=false`. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_ADVANCED_FILTER_ENABLED | `boolean` | Set to true to enable "Advanced filter" page in the app | Required | `true` | `false` | v1.37.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`. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `hype` \| `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' \| 'scroll'` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ | +| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network. **DEPRECATED** _Use `NEXT_PUBLIC_ROLLUP_PARENT_CHAIN` instead_ | 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` | v1.33.0+ | +| NEXT_PUBLIC_INTEROP_ENABLED | `boolean` | Enables "Interop messages" page (Optimistic stack only) | - | `false` | `true` | v1.39.0+ | +| NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ | +| NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED | `boolean` | Enables "Output roots" page (Optimistic stack only) | - | `false` | `true` | v1.37.0+ | +| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME | `string` | Set to customize L1 transaction status labels in the UI (e.g., "Sent to "). This setting is applicable only for Arbitrum-based chains. **DEPRECATED** _Use `NEXT_PUBLIC_ROLLUP_PARENT_CHAIN` instead_ | - | - | `DuckChain` | v1.37.0+ | +| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN | `ParentChain`, see details [below](#parent-chain-configuration-properties) | Configuration parameters for the parent chain. | - | - | `{'baseUrl':'https://explorer.duckchain.io'}` | v1.38.0+ | +| NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE | `string` | Hex-string for creating a link to the transaction batch on the Seleneium explorer. "0x"-format and 60 symbol length. Available only for Arbitrum roll-ups. | - | - | `0x00000000000000000000000000000000000000ca1de12a9905be97beaf` | v1.38.0+ | +| NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL | `string` | URL for the Selenium explorer. It is used to create links to the Data Availability Blobs page. The URL should contain the full path without any search parameters related to the blob, as these will be constructed at runtime for each blob separately. Available only for Optimistic or Arbitrum roll-ups. | - | - | `https://mocha.celenium.io/blob` | v2.0.2+ | + +#### Parent chain configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| id | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference. | - | - | `42` | +| name | `string` | Displayed name of the chain. Set to customize L1 transaction status labels in the UI (e.g., "Sent to "). Currently, this setting is applicable only for Arbitrum-based chains. | - | - | `DuckChain` | +| baseUrl | `string` | Base url of the chain explorer. | Required | - | `https://explorer.duckchain.io` | +| rpcUrls | `Array` | Chain public RPC server urls, see [https://chainlist.org](https://chainlist.org) for the reference. | - | - | `['https://rpc.duckchain.io']` | +| currency | `{ name: string; symbol: string; decimals: number; }` | Chain currency config. | - | - | `{ name: Quack, symbol: QUA, decimals: 18 }` | +| isTestnet | `boolean` | Set to true if network is testnet. | - | - | `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+ | +| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.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+ | + +  + +### External transactions + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG | `{ chain_name: string; chain_logo_url: string; explorer_url_template: string; }` | Configuration of the external transactions links that should be added to the transaction details. | - | - | `{ chain_name: 'ethereum', chain_logo_url: 'https://example.com/logo.png', explorer_url_template: 'https://explorer.com/tx/{hash}' }` | v1.38.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+ | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `` | v1.0.x+ | + +  + +### 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+ | + +  + +### Address profile API + +This feature allows the integration of an external API to fetch user info for addresses or contracts. When configured, if the API returns a username, a public tag with a custom link will be displayed in the address page header. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_ADDRESS_USERNAME_TAG | `{api_url: string; tag_link_template: string; tag_icon: string; tag_bg_color: string; tag_text_color: string}` | Address profile API tag configuration properties. See [below](#user-profile-api-configuration-properties). | - | - | `uniswap` | v1.35.0+ | + +  + +#### Address profile API configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| api_url_template | `string` | User profile API URL. Should be a template with `{address}` variable | Required | - | `https://example-api.com/{address}` | +| tag_link_template | `string` | External link to the profile. Should be a template with `{username}` variable | - | - | `https://example.com/{address}` | +| tag_icon | `string` | Public tag icon (.svg) url | - | - | `https://example.com/icon.svg` | +| tag_bg_color | `string` | Public tag background color (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | - | `\#000000` | +| tag_text_color | `string` | Public tag text color (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | - | `\#FFFFFF` | + +  + +### Address XStar XHS score + +This feature allows the integration of an XStar API to fetch XHS score for addresses. When configured, if the API returns a score, a public tag with that score will be displayed in the address page header. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_XSTAR_SCORE_URL | `string` | XStar XHS score documentation URL for the address tag. Enables the XStar score feature. | - | - | `https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm` | v1.36.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+ | + +  + +### Celo chain + +For blockchains that use the Celo platform. _Note_, that once the Celo mainnet becomes an L2 chain, these variables will be migrated to the Rollup configuration section. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_CELO_ENABLED | `boolean` | Indicates that it is a Celo-based chain. | - | - | `true` | v1.37.0+ | +| NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK | `number` | Indicates the block number when the Celo-type chain transitioned to L2. This is used to display links to the Epoch block page from a regular block page. | - | - | `26369280` | v1.37.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' \| 'blackfort' \| 'zilliqa'` | Chain type | Required | - | `'stability'` | v1.25.0+ | + +  + +### Sentry error monitoring + +_Note_ This feature is **deprecated**. All ENV variables will be removed in the future releases. + +| 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+ | + +  + +### Rollbar error monitoring + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN | `string` | Client token for your Rollbar project | Required | - | `` | v1.37.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` | + +  + +### Save on gas with GasHawk + +The feature enables a "Save with GasHawk" button next to the "Gas used" value on the address page. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_SAVE_ON_GAS_ENABLED | `boolean` | Set to "true" to enable the feature | - | - | `true` | v1.35.0+ | + +  + +### Rewards service API + +This feature enables Blockscout Merits program. It requires that the [My account](ENVS.md#my-account) and [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) features are also enabled. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_REWARDS_SERVICE_API_HOST | `string` | API URL | - | - | `https://example.com` | v1.36.0+ | + +  + +### DEX pools + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_DEX_POOLS_ENABLED | `boolean` | Set to true to enable the feature | Required | - | `true` | v1.37.0+ | +| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | Required | - | `https://contracts-info.services.blockscout.com` | v1.0.x+ | + +  + +### Badge claim link + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK | `string` | Provide to enable the easter egg badge feature | - | - | `https://example.com` | v1.37.0+ | + +  + +## External services configuration + +### Google ReCaptcha + +For obtaining the variable values, please refer to the [reCAPTCHA documentation](https://developers.google.com/recaptcha). Please note that we currently support only **reCAPTCHA v2 in invisible mode**, read more [here](https://developers.google.com/recaptcha/docs/versions#recaptcha_v2_invisible_recaptcha_badge). + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v3 site key | - | - | `` | v1.36.x | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `` | v1.0.x+ | \ No newline at end of file diff --git a/explorer/frontend/docs/PULL_REQUEST_TEMPLATE.md b/explorer/frontend/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..d4378762b --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/eslint.config.mjs b/explorer/frontend/eslint.config.mjs new file mode 100644 index 000000000..534b16b64 --- /dev/null +++ b/explorer/frontend/eslint.config.mjs @@ -0,0 +1,452 @@ +import { includeIgnoreFile } from '@eslint/compat'; +import jsPlugin from '@eslint/js'; +import nextJsPlugin from '@next/eslint-plugin-next'; +import stylisticPlugin from '@stylistic/eslint-plugin'; +import reactQueryPlugin from '@tanstack/eslint-plugin-query'; +import importPlugin from 'eslint-plugin-import'; +import importHelpersPlugin from 'eslint-plugin-import-helpers'; +import jestPlugin from 'eslint-plugin-jest'; +import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; +import noCyrillicStringPlugin from 'eslint-plugin-no-cyrillic-string'; +import playwrightPlugin from 'eslint-plugin-playwright'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; +import * as regexpPlugin from 'eslint-plugin-regexp'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import tseslint from 'typescript-eslint'; + +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: [ + 'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button', 'ButtonGroup', 'Link', 'LinkBox', 'LinkOverlay', + 'Dialog', 'DialogRoot', 'DialogContent', 'DialogHeader', 'DialogCloseTrigger', 'DialogBody', + 'Tag', 'Switch', 'Image', 'Popover', 'PopoverTrigger', 'PopoverContent', 'PopoverBody', 'PopoverFooter', + 'DrawerRoot', 'DrawerBody', 'DrawerContent', 'DrawerOverlay', 'DrawerBackdrop', 'DrawerTrigger', 'Drawer', + 'Alert', 'AlertIcon', 'AlertTitle', 'AlertDescription', + 'Select', 'SelectRoot', 'SelectControl', 'SelectContent', 'SelectItem', 'SelectValueText', + 'Heading', 'Badge', 'Tabs', 'Show', 'Hide', 'Checkbox', 'CheckboxGroup', + 'Table', 'TableRoot', 'TableBody', 'TableHeader', 'TableRow', 'TableCell', + 'Menu', 'MenuRoot', 'MenuTrigger', 'MenuContent', 'MenuItem', 'MenuTriggerItem', 'MenuRadioItemGroup', 'MenuContextTrigger', + 'Rating', 'RatingGroup', 'Textarea', + ], + message: 'Please use corresponding component or hook from "toolkit" instead', + }, + { + name: 'next/link', + importNames: [ 'default' ], + message: 'Please use toolkit/chakra/link component instead', + }, + ], + patterns: [ + 'icons/*', + ], +}; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const gitignorePath = path.resolve(__dirname, '.gitignore'); + +/** @type {import('eslint').Linter.Config[]} */ +export default tseslint.config( + includeIgnoreFile(gitignorePath), + + { files: [ '**/*.{js,mjs,cjs,ts,jsx,tsx}', '**/*.pw.tsx' ] }, + + { ignores: [ + 'deploy/tools/', + 'public/', + '.git/', + 'next.config.js', + ] }, + + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + + { settings: { react: { version: 'detect' } } }, + + jsPlugin.configs.recommended, + + { + plugins: { + '@typescript-eslint': tseslint.plugin, + jest: jestPlugin, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + projectService: true, + }, + globals: jestPlugin.environments.globals.globals, + }, + rules: { + '@typescript-eslint/array-type': [ 'error', { + 'default': 'generic', + readonly: 'generic', + } ], + '@typescript-eslint/consistent-type-imports': [ 'error' ], + '@typescript-eslint/naming-convention': [ 'error', + { + selector: 'default', + format: [ 'camelCase' ], + leadingUnderscore: 'allow', + trailingUnderscore: 'forbid', + }, + { + selector: 'import', + leadingUnderscore: 'allow', + format: [ 'camelCase', 'PascalCase' ], + }, + { + 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: 'typeLike', + format: [ 'PascalCase' ], + }, + { + selector: 'typeParameter', + format: [ 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'variable', + format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ], + leadingUnderscore: 'allow', + }, + ], + '@typescript-eslint/no-empty-function': [ 'off' ], + '@typescript-eslint/no-unused-vars': [ 'error', { caughtErrors: 'none', ignoreRestSiblings: true } ], + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-useless-constructor': [ 'error' ], + '@typescript-eslint/no-explicit-any': [ 'error', { ignoreRestArgs: true } ], + '@typescript-eslint/no-unused-expressions': [ 'error', { + allowShortCircuit: true, + allowTernary: true, + } ], + }, + }, + { + // disable type-aware linting on JS files + files: [ '**/*.{js,mjs}' ], + ...tseslint.configs.disableTypeChecked, + }, + + { + plugins: { + react: reactPlugin, + }, + rules: { + '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': 'off', + '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-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', + }, + }, + + { + plugins: { + '@next/next': nextJsPlugin, + }, + rules: { + ...nextJsPlugin.configs.recommended.rules, + ...nextJsPlugin.configs['core-web-vitals'].rules, + }, + }, + + { + plugins: { '@tanstack/query': reactQueryPlugin }, + }, + + { + ...playwrightPlugin.configs['flat/recommended'], + files: [ '**/*.pw.tsx' ], + rules: { + ...playwrightPlugin.configs['flat/recommended'].rules, + 'playwright/no-standalone-expect': 'off', // this rules does not work correctly with extended test functions + }, + }, + + { + plugins: { 'react-hooks': reactHooksPlugin }, + ignores: [ '**/*.pw.tsx', 'playwright/**' ], + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + }, + }, + + { + files: [ '**/*.test.{ts,js,jsx,tsx}' ], + plugins: { jest: jestPlugin }, + languageOptions: { + globals: jestPlugin.environments.globals.globals, + }, + }, + + regexpPlugin.configs['flat/recommended'], + + { + plugins: { + 'import': importPlugin, + }, + rules: { + 'import/no-duplicates': 'error', + }, + }, + + { + plugins: { + 'import-helpers': importHelpersPlugin, + }, + rules: { + 'import-helpers/order-imports': [ + 'error', + { + newlinesBetween: 'always', + groups: [ + 'module', + '/types/', + [ '/^nextjs/' ], + [ + '/^configs/', + '/^data/', + '/^deploy/', + '/^icons/', + '/^jest/', + '/^lib/', + '/^mocks/', + '/^pages/', + '/^playwright/', + '/^stubs/', + '/^theme/', + '/^toolkit/', + '/^ui/', + ], + [ 'parent', 'sibling', 'index' ], + ], + alphabetize: { order: 'asc', ignoreCase: true }, + }, + ], + }, + }, + + { + plugins: { + 'no-cyrillic-string': noCyrillicStringPlugin, + }, + rules: { + 'no-cyrillic-string/no-cyrillic-string': 'error', + }, + }, + + { + plugins: { + 'jsx-a11y': jsxA11yPlugin, + }, + languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } }, + }, + + { + plugins: { + '@stylistic': stylisticPlugin, + }, + rules: { + // replacement for @typescript-eslint + '@stylistic/indent': [ 'error', 2 ], + '@stylistic/brace-style': [ 'error', '1tbs' ], + '@stylistic/member-delimiter-style': [ 'error' ], + '@stylistic/type-annotation-spacing': 'error', + + // replacement for eslint + //'@stylistic/array-bracket-spacing': [ 'error', 'always' ], + '@stylistic/arrow-spacing': [ 'error', { before: true, after: true } ], + '@stylistic/comma-dangle': [ 'error', 'always-multiline' ], + '@stylistic/comma-spacing': [ 'error' ], + '@stylistic/comma-style': [ 'error', 'last' ], + '@stylistic/curly-newline': [ 'error', { multiline: true, minElements: 1 } ], + '@stylistic/eol-last': 'error', + '@stylistic/jsx-quotes': [ 'error', 'prefer-double' ], + '@stylistic/key-spacing': [ 'error', { + beforeColon: false, + afterColon: true, + } ], + '@stylistic/keyword-spacing': 'error', + '@stylistic/linebreak-style': [ 'error', 'unix' ], + '@stylistic/lines-around-comment': [ 'error', { + beforeBlockComment: true, + allowBlockStart: true, + } ], + '@stylistic/no-mixed-operators': [ 'error', { + groups: [ + [ '&&', '||' ], + ], + } ], + '@stylistic/no-mixed-spaces-and-tabs': 'error', + '@stylistic/no-multiple-empty-lines': [ 'error', { + max: 1, + maxEOF: 0, + maxBOF: 0, + } ], + '@stylistic/no-multi-spaces': 'error', + '@stylistic/no-trailing-spaces': 'error', + '@stylistic/operator-linebreak': [ 'error', 'after' ], + '@stylistic/quote-props': [ 'error', 'as-needed', { + keywords: true, + numbers: true, + } ], + '@stylistic/quotes': [ 'error', 'single', { + allowTemplateLiterals: true, + } ], + '@stylistic/semi': [ 'error', 'always' ], + '@stylistic/space-before-function-paren': [ 'error', 'never' ], + '@stylistic/space-before-blocks': [ 'error', 'always' ], + '@stylistic/space-in-parens': [ 'error', 'never' ], + '@stylistic/space-infix-ops': 'error', + '@stylistic/space-unary-ops': 'off', + '@stylistic/template-curly-spacing': [ 'error', 'always' ], + '@stylistic/wrap-iife': [ 'error', 'inside' ], + }, + }, + + { + rules: { + // disabled in favor of @typescript-eslint and @stylistic + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', + 'no-unused-vars': 'off', + 'no-empty': [ 'error', { allowEmptyCatch: true } ], + 'no-unused-expressions': 'off', + + // this is checked by typescript compiler + 'no-redeclare': 'off', + + // rules customizations + eqeqeq: [ 'error', 'allow-null' ], + 'id-match': [ 'error', '^[\\w$]+$' ], + 'max-len': [ 'error', 160, 4 ], + 'no-console': 'error', + 'no-implicit-coercion': [ 'error', { + number: true, + 'boolean': true, + string: true, + } ], + 'no-nested-ternary': 'error', + 'no-multi-str': 'error', + 'no-spaced-func': 'error', + 'no-with': 'error', + 'object-shorthand': 'off', + 'one-var': [ 'error', 'never' ], + 'prefer-const': 'error', + + // restricted imports and properties + '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.', + } ], + }, + }, + { + files: [ + 'pages/**', + 'nextjs/**', + 'playwright/**', + 'deploy/scripts/**', + 'deploy/tools/**', + 'middleware.ts', + 'instrumentation*.ts', + '*.config.ts', + '*.config.js', + ], + rules: { + // for configs allow to consume env variables from process.env directly + 'no-restricted-properties': 'off', + }, + }, + { + files: [ + 'toolkit/chakra/**', + 'toolkit/components/**', + 'toolkit/package/**', + ], + rules: { + // for toolkit components allow to import @chakra-ui/react directly + 'no-restricted-imports': 'off', + }, + }, +); diff --git a/explorer/frontend/global.d.ts b/explorer/frontend/global.d.ts new file mode 100644 index 000000000..94cfa4677 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/icons/ABI.svg b/explorer/frontend/icons/ABI.svg new file mode 100644 index 000000000..0416e2e13 --- /dev/null +++ b/explorer/frontend/icons/ABI.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/explorer/frontend/icons/ABI_slim.svg b/explorer/frontend/icons/ABI_slim.svg new file mode 100644 index 000000000..89532207b --- /dev/null +++ b/explorer/frontend/icons/ABI_slim.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/explorer/frontend/icons/API.svg b/explorer/frontend/icons/API.svg new file mode 100644 index 000000000..2fae957e8 --- /dev/null +++ b/explorer/frontend/icons/API.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/API_slim.svg b/explorer/frontend/icons/API_slim.svg new file mode 100644 index 000000000..f4b36f08b --- /dev/null +++ b/explorer/frontend/icons/API_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/ENS.svg b/explorer/frontend/icons/ENS.svg new file mode 100644 index 000000000..9832944da --- /dev/null +++ b/explorer/frontend/icons/ENS.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/ENS_slim.svg b/explorer/frontend/icons/ENS_slim.svg new file mode 100644 index 000000000..cd999b523 --- /dev/null +++ b/explorer/frontend/icons/ENS_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/MUD.svg b/explorer/frontend/icons/MUD.svg new file mode 100644 index 000000000..8ab1229a7 --- /dev/null +++ b/explorer/frontend/icons/MUD.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/MUD_menu.svg b/explorer/frontend/icons/MUD_menu.svg new file mode 100644 index 000000000..c30c571c4 --- /dev/null +++ b/explorer/frontend/icons/MUD_menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/RPC.svg b/explorer/frontend/icons/RPC.svg new file mode 100644 index 000000000..7df2ba219 --- /dev/null +++ b/explorer/frontend/icons/RPC.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/apps.svg b/explorer/frontend/icons/apps.svg new file mode 100644 index 000000000..c0cdc1c4e --- /dev/null +++ b/explorer/frontend/icons/apps.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/apps_list.svg b/explorer/frontend/icons/apps_list.svg new file mode 100644 index 000000000..62cb5020d --- /dev/null +++ b/explorer/frontend/icons/apps_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/apps_slim.svg b/explorer/frontend/icons/apps_slim.svg new file mode 100644 index 000000000..59e2f2d81 --- /dev/null +++ b/explorer/frontend/icons/apps_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/down-right.svg b/explorer/frontend/icons/arrows/down-right.svg new file mode 100644 index 000000000..e02aa2f9a --- /dev/null +++ b/explorer/frontend/icons/arrows/down-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/east-mini.svg b/explorer/frontend/icons/arrows/east-mini.svg new file mode 100644 index 000000000..1a9d93e40 --- /dev/null +++ b/explorer/frontend/icons/arrows/east-mini.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/east.svg b/explorer/frontend/icons/arrows/east.svg new file mode 100644 index 000000000..c152a2f67 --- /dev/null +++ b/explorer/frontend/icons/arrows/east.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/north-east.svg b/explorer/frontend/icons/arrows/north-east.svg new file mode 100644 index 000000000..59aa6a4b2 --- /dev/null +++ b/explorer/frontend/icons/arrows/north-east.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/south-east.svg b/explorer/frontend/icons/arrows/south-east.svg new file mode 100644 index 000000000..16a7590f5 --- /dev/null +++ b/explorer/frontend/icons/arrows/south-east.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/up-down.svg b/explorer/frontend/icons/arrows/up-down.svg new file mode 100644 index 000000000..8f45bb257 --- /dev/null +++ b/explorer/frontend/icons/arrows/up-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/arrows/up-head.svg b/explorer/frontend/icons/arrows/up-head.svg new file mode 100644 index 000000000..375381a79 --- /dev/null +++ b/explorer/frontend/icons/arrows/up-head.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/beta.svg b/explorer/frontend/icons/beta.svg new file mode 100644 index 000000000..bba1309f3 --- /dev/null +++ b/explorer/frontend/icons/beta.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/beta_xs.svg b/explorer/frontend/icons/beta_xs.svg new file mode 100644 index 000000000..a6dc48ee4 --- /dev/null +++ b/explorer/frontend/icons/beta_xs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/blob.svg b/explorer/frontend/icons/blob.svg new file mode 100644 index 000000000..9b40d72ec --- /dev/null +++ b/explorer/frontend/icons/blob.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/blobs/image.svg b/explorer/frontend/icons/blobs/image.svg new file mode 100644 index 000000000..be08dd269 --- /dev/null +++ b/explorer/frontend/icons/blobs/image.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/explorer/frontend/icons/blobs/raw.svg b/explorer/frontend/icons/blobs/raw.svg new file mode 100644 index 000000000..8a97401ff --- /dev/null +++ b/explorer/frontend/icons/blobs/raw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/explorer/frontend/icons/blobs/text.svg b/explorer/frontend/icons/blobs/text.svg new file mode 100644 index 000000000..08ec8801b --- /dev/null +++ b/explorer/frontend/icons/blobs/text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/explorer/frontend/icons/block.svg b/explorer/frontend/icons/block.svg new file mode 100644 index 000000000..85c88a6bd --- /dev/null +++ b/explorer/frontend/icons/block.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/block_countdown.svg b/explorer/frontend/icons/block_countdown.svg new file mode 100644 index 000000000..0024e52ce --- /dev/null +++ b/explorer/frontend/icons/block_countdown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/block_slim.svg b/explorer/frontend/icons/block_slim.svg new file mode 100644 index 000000000..63302e1d8 --- /dev/null +++ b/explorer/frontend/icons/block_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/brands/blockscout.svg b/explorer/frontend/icons/brands/blockscout.svg new file mode 100644 index 000000000..0e3279de0 --- /dev/null +++ b/explorer/frontend/icons/brands/blockscout.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/brands/celenium.svg b/explorer/frontend/icons/brands/celenium.svg new file mode 100644 index 000000000..8a5d645ac --- /dev/null +++ b/explorer/frontend/icons/brands/celenium.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/icons/brands/graph.svg b/explorer/frontend/icons/brands/graph.svg new file mode 100644 index 000000000..bd3cc916d --- /dev/null +++ b/explorer/frontend/icons/brands/graph.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/brands/safe.svg b/explorer/frontend/icons/brands/safe.svg new file mode 100644 index 000000000..836951383 --- /dev/null +++ b/explorer/frontend/icons/brands/safe.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/brands/solidity_scan.svg b/explorer/frontend/icons/brands/solidity_scan.svg new file mode 100644 index 000000000..ac5747c69 --- /dev/null +++ b/explorer/frontend/icons/brands/solidity_scan.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/explorer/frontend/icons/burger.svg b/explorer/frontend/icons/burger.svg new file mode 100644 index 000000000..c6a84a911 --- /dev/null +++ b/explorer/frontend/icons/burger.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/certified.svg b/explorer/frontend/icons/certified.svg new file mode 100644 index 000000000..088a866f8 --- /dev/null +++ b/explorer/frontend/icons/certified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/check.svg b/explorer/frontend/icons/check.svg new file mode 100644 index 000000000..563e6e5a9 --- /dev/null +++ b/explorer/frontend/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/checkered_flag.svg b/explorer/frontend/icons/checkered_flag.svg new file mode 100644 index 000000000..918c29cea --- /dev/null +++ b/explorer/frontend/icons/checkered_flag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/clock-light.svg b/explorer/frontend/icons/clock-light.svg new file mode 100644 index 000000000..110cd4b79 --- /dev/null +++ b/explorer/frontend/icons/clock-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/clock.svg b/explorer/frontend/icons/clock.svg new file mode 100644 index 000000000..14a8c9405 --- /dev/null +++ b/explorer/frontend/icons/clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/close.svg b/explorer/frontend/icons/close.svg new file mode 100644 index 000000000..0631c7c76 --- /dev/null +++ b/explorer/frontend/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/coins/bitcoin.svg b/explorer/frontend/icons/coins/bitcoin.svg new file mode 100644 index 000000000..7f22b3139 --- /dev/null +++ b/explorer/frontend/icons/coins/bitcoin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/collection.svg b/explorer/frontend/icons/collection.svg new file mode 100644 index 000000000..981040af5 --- /dev/null +++ b/explorer/frontend/icons/collection.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/columns.svg b/explorer/frontend/icons/columns.svg new file mode 100644 index 000000000..d4bfc9736 --- /dev/null +++ b/explorer/frontend/icons/columns.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/contracts/proxy.svg b/explorer/frontend/icons/contracts/proxy.svg new file mode 100644 index 000000000..1b75cb210 --- /dev/null +++ b/explorer/frontend/icons/contracts/proxy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/contracts/regular.svg b/explorer/frontend/icons/contracts/regular.svg new file mode 100644 index 000000000..1bc8e0d3d --- /dev/null +++ b/explorer/frontend/icons/contracts/regular.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/contracts/regular_many.svg b/explorer/frontend/icons/contracts/regular_many.svg new file mode 100644 index 000000000..1f0b62afd --- /dev/null +++ b/explorer/frontend/icons/contracts/regular_many.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/contracts/verified.svg b/explorer/frontend/icons/contracts/verified.svg new file mode 100644 index 000000000..6dbb05843 --- /dev/null +++ b/explorer/frontend/icons/contracts/verified.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/contracts/verified_many.svg b/explorer/frontend/icons/contracts/verified_many.svg new file mode 100644 index 000000000..2a004f596 --- /dev/null +++ b/explorer/frontend/icons/contracts/verified_many.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/copy.svg b/explorer/frontend/icons/copy.svg new file mode 100644 index 000000000..7b0b154c2 --- /dev/null +++ b/explorer/frontend/icons/copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/explorer/frontend/icons/copy_check.svg b/explorer/frontend/icons/copy_check.svg new file mode 100644 index 000000000..52a40f135 --- /dev/null +++ b/explorer/frontend/icons/copy_check.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/cross.svg b/explorer/frontend/icons/cross.svg new file mode 100644 index 000000000..c9bec2cef --- /dev/null +++ b/explorer/frontend/icons/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/delete.svg b/explorer/frontend/icons/delete.svg new file mode 100644 index 000000000..13b71cdff --- /dev/null +++ b/explorer/frontend/icons/delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/dex-tracker.svg b/explorer/frontend/icons/dex-tracker.svg new file mode 100644 index 000000000..64deb3aed --- /dev/null +++ b/explorer/frontend/icons/dex-tracker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/docs.svg b/explorer/frontend/icons/docs.svg new file mode 100644 index 000000000..71a628d93 --- /dev/null +++ b/explorer/frontend/icons/docs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/donate.svg b/explorer/frontend/icons/donate.svg new file mode 100644 index 000000000..9ed7def2b --- /dev/null +++ b/explorer/frontend/icons/donate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/explorer/frontend/icons/dots.svg b/explorer/frontend/icons/dots.svg new file mode 100644 index 000000000..1ea165f92 --- /dev/null +++ b/explorer/frontend/icons/dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/edit.svg b/explorer/frontend/icons/edit.svg new file mode 100644 index 000000000..02a2737da --- /dev/null +++ b/explorer/frontend/icons/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/email.svg b/explorer/frontend/icons/email.svg new file mode 100644 index 000000000..4e184be0e --- /dev/null +++ b/explorer/frontend/icons/email.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/empty_search_result.svg b/explorer/frontend/icons/empty_search_result.svg new file mode 100644 index 000000000..f4d62eff0 --- /dev/null +++ b/explorer/frontend/icons/empty_search_result.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/icons/error-pages/403.svg b/explorer/frontend/icons/error-pages/403.svg new file mode 100644 index 000000000..d4369a0a6 --- /dev/null +++ b/explorer/frontend/icons/error-pages/403.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/error-pages/404.svg b/explorer/frontend/icons/error-pages/404.svg new file mode 100644 index 000000000..296ff701c --- /dev/null +++ b/explorer/frontend/icons/error-pages/404.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/error-pages/422.svg b/explorer/frontend/icons/error-pages/422.svg new file mode 100644 index 000000000..99225c643 --- /dev/null +++ b/explorer/frontend/icons/error-pages/422.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/error-pages/429.svg b/explorer/frontend/icons/error-pages/429.svg new file mode 100644 index 000000000..9ae110e19 --- /dev/null +++ b/explorer/frontend/icons/error-pages/429.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/error-pages/500.svg b/explorer/frontend/icons/error-pages/500.svg new file mode 100644 index 000000000..43cd887fd --- /dev/null +++ b/explorer/frontend/icons/error-pages/500.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/explorer.svg b/explorer/frontend/icons/explorer.svg new file mode 100644 index 000000000..91a3e683f --- /dev/null +++ b/explorer/frontend/icons/explorer.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/explorer/frontend/icons/files/csv.svg b/explorer/frontend/icons/files/csv.svg new file mode 100644 index 000000000..119d58cdf --- /dev/null +++ b/explorer/frontend/icons/files/csv.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/files/image.svg b/explorer/frontend/icons/files/image.svg new file mode 100644 index 000000000..9d2da4ee8 --- /dev/null +++ b/explorer/frontend/icons/files/image.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/files/json.svg b/explorer/frontend/icons/files/json.svg new file mode 100644 index 000000000..06c437efb --- /dev/null +++ b/explorer/frontend/icons/files/json.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/files/placeholder.svg b/explorer/frontend/icons/files/placeholder.svg new file mode 100644 index 000000000..129e36a3b --- /dev/null +++ b/explorer/frontend/icons/files/placeholder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/files/sol.svg b/explorer/frontend/icons/files/sol.svg new file mode 100644 index 000000000..0efa5d0b3 --- /dev/null +++ b/explorer/frontend/icons/files/sol.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/explorer/frontend/icons/files/yul.svg b/explorer/frontend/icons/files/yul.svg new file mode 100644 index 000000000..03b88fbb3 --- /dev/null +++ b/explorer/frontend/icons/files/yul.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/filter.svg b/explorer/frontend/icons/filter.svg new file mode 100644 index 000000000..c9c284e81 --- /dev/null +++ b/explorer/frontend/icons/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/flame.svg b/explorer/frontend/icons/flame.svg new file mode 100644 index 000000000..ddc5f161e --- /dev/null +++ b/explorer/frontend/icons/flame.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/games.svg b/explorer/frontend/icons/games.svg new file mode 100644 index 000000000..39c01ca56 --- /dev/null +++ b/explorer/frontend/icons/games.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/explorer/frontend/icons/gas.svg b/explorer/frontend/icons/gas.svg new file mode 100644 index 000000000..4334fe93f --- /dev/null +++ b/explorer/frontend/icons/gas.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/explorer/frontend/icons/gas_xl.svg b/explorer/frontend/icons/gas_xl.svg new file mode 100644 index 000000000..5a3913ac1 --- /dev/null +++ b/explorer/frontend/icons/gas_xl.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/gear.svg b/explorer/frontend/icons/gear.svg new file mode 100644 index 000000000..32ec463e1 --- /dev/null +++ b/explorer/frontend/icons/gear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/gear_slim.svg b/explorer/frontend/icons/gear_slim.svg new file mode 100644 index 000000000..abc14e6a7 --- /dev/null +++ b/explorer/frontend/icons/gear_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/globe-b.svg b/explorer/frontend/icons/globe-b.svg new file mode 100644 index 000000000..0b185e855 --- /dev/null +++ b/explorer/frontend/icons/globe-b.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/globe.svg b/explorer/frontend/icons/globe.svg new file mode 100644 index 000000000..e3cea4b63 --- /dev/null +++ b/explorer/frontend/icons/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/graphQL.svg b/explorer/frontend/icons/graphQL.svg new file mode 100644 index 000000000..9332276ac --- /dev/null +++ b/explorer/frontend/icons/graphQL.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/heart_filled.svg b/explorer/frontend/icons/heart_filled.svg new file mode 100644 index 000000000..64635b353 --- /dev/null +++ b/explorer/frontend/icons/heart_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/heart_outline.svg b/explorer/frontend/icons/heart_outline.svg new file mode 100644 index 000000000..34c36398f --- /dev/null +++ b/explorer/frontend/icons/heart_outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/hourglass.svg b/explorer/frontend/icons/hourglass.svg new file mode 100644 index 000000000..7ebd6d78b --- /dev/null +++ b/explorer/frontend/icons/hourglass.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/info.svg b/explorer/frontend/icons/info.svg new file mode 100644 index 000000000..040100574 --- /dev/null +++ b/explorer/frontend/icons/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/info_filled.svg b/explorer/frontend/icons/info_filled.svg new file mode 100644 index 000000000..9616f0a2d --- /dev/null +++ b/explorer/frontend/icons/info_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/integration/full.svg b/explorer/frontend/icons/integration/full.svg new file mode 100644 index 000000000..0ac50a345 --- /dev/null +++ b/explorer/frontend/icons/integration/full.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/integration/partial.svg b/explorer/frontend/icons/integration/partial.svg new file mode 100644 index 000000000..2f7a76969 --- /dev/null +++ b/explorer/frontend/icons/integration/partial.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/internal_txns.svg b/explorer/frontend/icons/internal_txns.svg new file mode 100644 index 000000000..683cc0761 --- /dev/null +++ b/explorer/frontend/icons/internal_txns.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/explorer/frontend/icons/interop.svg b/explorer/frontend/icons/interop.svg new file mode 100644 index 000000000..b5692777e --- /dev/null +++ b/explorer/frontend/icons/interop.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/key.svg b/explorer/frontend/icons/key.svg new file mode 100644 index 000000000..ae0834f52 --- /dev/null +++ b/explorer/frontend/icons/key.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/lightning.svg b/explorer/frontend/icons/lightning.svg new file mode 100644 index 000000000..03fea73d7 --- /dev/null +++ b/explorer/frontend/icons/lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/lightning_navbar.svg b/explorer/frontend/icons/lightning_navbar.svg new file mode 100644 index 000000000..9587a9c7a --- /dev/null +++ b/explorer/frontend/icons/lightning_navbar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/link.svg b/explorer/frontend/icons/link.svg new file mode 100644 index 000000000..5d072572a --- /dev/null +++ b/explorer/frontend/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/link_external.svg b/explorer/frontend/icons/link_external.svg new file mode 100644 index 000000000..dbddf710b --- /dev/null +++ b/explorer/frontend/icons/link_external.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/lock.svg b/explorer/frontend/icons/lock.svg new file mode 100644 index 000000000..763d128cb --- /dev/null +++ b/explorer/frontend/icons/lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/merits.svg b/explorer/frontend/icons/merits.svg new file mode 100644 index 000000000..91b128a2b --- /dev/null +++ b/explorer/frontend/icons/merits.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/merits_colored.svg b/explorer/frontend/icons/merits_colored.svg new file mode 100644 index 000000000..4006f7e43 --- /dev/null +++ b/explorer/frontend/icons/merits_colored.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/explorer/frontend/icons/merits_slim.svg b/explorer/frontend/icons/merits_slim.svg new file mode 100644 index 000000000..8a0623a16 --- /dev/null +++ b/explorer/frontend/icons/merits_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/merits_with_dot.svg b/explorer/frontend/icons/merits_with_dot.svg new file mode 100644 index 000000000..e216115e4 --- /dev/null +++ b/explorer/frontend/icons/merits_with_dot.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/merits_with_dot_slim.svg b/explorer/frontend/icons/merits_with_dot_slim.svg new file mode 100644 index 000000000..4b5bf8a0a --- /dev/null +++ b/explorer/frontend/icons/merits_with_dot_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/minus.svg b/explorer/frontend/icons/minus.svg new file mode 100644 index 000000000..ba1cfe240 --- /dev/null +++ b/explorer/frontend/icons/minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/monaco/cargo.svg b/explorer/frontend/icons/monaco/cargo.svg new file mode 100644 index 000000000..ff0bf0e79 --- /dev/null +++ b/explorer/frontend/icons/monaco/cargo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/explorer/frontend/icons/monaco/file.svg b/explorer/frontend/icons/monaco/file.svg new file mode 100644 index 000000000..38b4a10fc --- /dev/null +++ b/explorer/frontend/icons/monaco/file.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/monaco/folder-open.svg b/explorer/frontend/icons/monaco/folder-open.svg new file mode 100644 index 000000000..02989af8d --- /dev/null +++ b/explorer/frontend/icons/monaco/folder-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/monaco/folder.svg b/explorer/frontend/icons/monaco/folder.svg new file mode 100644 index 000000000..70e98efa4 --- /dev/null +++ b/explorer/frontend/icons/monaco/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/monaco/rust.svg b/explorer/frontend/icons/monaco/rust.svg new file mode 100644 index 000000000..0966ea4b7 --- /dev/null +++ b/explorer/frontend/icons/monaco/rust.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/monaco/solidity.svg b/explorer/frontend/icons/monaco/solidity.svg new file mode 100644 index 000000000..f1c51a4c6 --- /dev/null +++ b/explorer/frontend/icons/monaco/solidity.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/monaco/toml.svg b/explorer/frontend/icons/monaco/toml.svg new file mode 100644 index 000000000..4217e7f6b --- /dev/null +++ b/explorer/frontend/icons/monaco/toml.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/monaco/vyper.svg b/explorer/frontend/icons/monaco/vyper.svg new file mode 100644 index 000000000..cd7b34ad9 --- /dev/null +++ b/explorer/frontend/icons/monaco/vyper.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/moon-with-star.svg b/explorer/frontend/icons/moon-with-star.svg new file mode 100644 index 000000000..24c208587 --- /dev/null +++ b/explorer/frontend/icons/moon-with-star.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/moon.svg b/explorer/frontend/icons/moon.svg new file mode 100644 index 000000000..57c4ee656 --- /dev/null +++ b/explorer/frontend/icons/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/networks.svg b/explorer/frontend/icons/networks.svg new file mode 100644 index 000000000..cc62a3579 --- /dev/null +++ b/explorer/frontend/icons/networks.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/networks/icon-placeholder.svg b/explorer/frontend/icons/networks/icon-placeholder.svg new file mode 100644 index 000000000..77414f1df --- /dev/null +++ b/explorer/frontend/icons/networks/icon-placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/networks/logo-placeholder.svg b/explorer/frontend/icons/networks/logo-placeholder.svg new file mode 100644 index 000000000..6c7891fdb --- /dev/null +++ b/explorer/frontend/icons/networks/logo-placeholder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/nft_shield.svg b/explorer/frontend/icons/nft_shield.svg new file mode 100644 index 000000000..5f055f02e --- /dev/null +++ b/explorer/frontend/icons/nft_shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/open-link.svg b/explorer/frontend/icons/open-link.svg new file mode 100644 index 000000000..d0fcc28ab --- /dev/null +++ b/explorer/frontend/icons/open-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/output_roots.svg b/explorer/frontend/icons/output_roots.svg new file mode 100644 index 000000000..447e65743 --- /dev/null +++ b/explorer/frontend/icons/output_roots.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/payment_link.svg b/explorer/frontend/icons/payment_link.svg new file mode 100644 index 000000000..f97128fff --- /dev/null +++ b/explorer/frontend/icons/payment_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/plus.svg b/explorer/frontend/icons/plus.svg new file mode 100644 index 000000000..b86a62118 --- /dev/null +++ b/explorer/frontend/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/private_tags_slim.svg b/explorer/frontend/icons/private_tags_slim.svg new file mode 100644 index 000000000..538c47d05 --- /dev/null +++ b/explorer/frontend/icons/private_tags_slim.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/privattags.svg b/explorer/frontend/icons/privattags.svg new file mode 100644 index 000000000..7e0cacaa0 --- /dev/null +++ b/explorer/frontend/icons/privattags.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/profile.svg b/explorer/frontend/icons/profile.svg new file mode 100644 index 000000000..177eea965 --- /dev/null +++ b/explorer/frontend/icons/profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/publictags.svg b/explorer/frontend/icons/publictags.svg new file mode 100644 index 000000000..4c55542fc --- /dev/null +++ b/explorer/frontend/icons/publictags.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/publictags_slim.svg b/explorer/frontend/icons/publictags_slim.svg new file mode 100644 index 000000000..4de98a2d1 --- /dev/null +++ b/explorer/frontend/icons/publictags_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/qr_code.svg b/explorer/frontend/icons/qr_code.svg new file mode 100644 index 000000000..f31b981da --- /dev/null +++ b/explorer/frontend/icons/qr_code.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/refresh.svg b/explorer/frontend/icons/refresh.svg new file mode 100644 index 000000000..fef0346a5 --- /dev/null +++ b/explorer/frontend/icons/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/repeat.svg b/explorer/frontend/icons/repeat.svg new file mode 100644 index 000000000..dcd2c7a37 --- /dev/null +++ b/explorer/frontend/icons/repeat.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/restAPI.svg b/explorer/frontend/icons/restAPI.svg new file mode 100644 index 000000000..8924b2e2b --- /dev/null +++ b/explorer/frontend/icons/restAPI.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/explorer/frontend/icons/rocket.svg b/explorer/frontend/icons/rocket.svg new file mode 100644 index 000000000..46523e1b0 --- /dev/null +++ b/explorer/frontend/icons/rocket.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/rocket_xl.svg b/explorer/frontend/icons/rocket_xl.svg new file mode 100644 index 000000000..8b3f4ccdb --- /dev/null +++ b/explorer/frontend/icons/rocket_xl.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/scope.svg b/explorer/frontend/icons/scope.svg new file mode 100644 index 000000000..6337932af --- /dev/null +++ b/explorer/frontend/icons/scope.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/score/score-not-ok.svg b/explorer/frontend/icons/score/score-not-ok.svg new file mode 100644 index 000000000..9b4533f6a --- /dev/null +++ b/explorer/frontend/icons/score/score-not-ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/score/score-ok.svg b/explorer/frontend/icons/score/score-ok.svg new file mode 100644 index 000000000..dbe11836f --- /dev/null +++ b/explorer/frontend/icons/score/score-ok.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/search.svg b/explorer/frontend/icons/search.svg new file mode 100644 index 000000000..c1849adc0 --- /dev/null +++ b/explorer/frontend/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/share.svg b/explorer/frontend/icons/share.svg new file mode 100644 index 000000000..f1124e47d --- /dev/null +++ b/explorer/frontend/icons/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/sign_out.svg b/explorer/frontend/icons/sign_out.svg new file mode 100644 index 000000000..d5b68ecda --- /dev/null +++ b/explorer/frontend/icons/sign_out.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/social/coingecko.svg b/explorer/frontend/icons/social/coingecko.svg new file mode 100644 index 000000000..baf56eef3 --- /dev/null +++ b/explorer/frontend/icons/social/coingecko.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/explorer/frontend/icons/social/coinmarketcap.svg b/explorer/frontend/icons/social/coinmarketcap.svg new file mode 100644 index 000000000..0341e03c4 --- /dev/null +++ b/explorer/frontend/icons/social/coinmarketcap.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/defi_llama.svg b/explorer/frontend/icons/social/defi_llama.svg new file mode 100644 index 000000000..f7d2cc51a --- /dev/null +++ b/explorer/frontend/icons/social/defi_llama.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/social/discord.svg b/explorer/frontend/icons/social/discord.svg new file mode 100644 index 000000000..133a54e97 --- /dev/null +++ b/explorer/frontend/icons/social/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/discord_filled.svg b/explorer/frontend/icons/social/discord_filled.svg new file mode 100644 index 000000000..691efc8a4 --- /dev/null +++ b/explorer/frontend/icons/social/discord_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/facebook_filled.svg b/explorer/frontend/icons/social/facebook_filled.svg new file mode 100644 index 000000000..12afda197 --- /dev/null +++ b/explorer/frontend/icons/social/facebook_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/git.svg b/explorer/frontend/icons/social/git.svg new file mode 100644 index 000000000..caf39a4c9 --- /dev/null +++ b/explorer/frontend/icons/social/git.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/github_filled.svg b/explorer/frontend/icons/social/github_filled.svg new file mode 100644 index 000000000..e134fcaf9 --- /dev/null +++ b/explorer/frontend/icons/social/github_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/linkedin_filled.svg b/explorer/frontend/icons/social/linkedin_filled.svg new file mode 100644 index 000000000..6fcd9bedf --- /dev/null +++ b/explorer/frontend/icons/social/linkedin_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/medium_filled.svg b/explorer/frontend/icons/social/medium_filled.svg new file mode 100644 index 000000000..60f2d6303 --- /dev/null +++ b/explorer/frontend/icons/social/medium_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/opensea_filled.svg b/explorer/frontend/icons/social/opensea_filled.svg new file mode 100644 index 000000000..a6c8798cc --- /dev/null +++ b/explorer/frontend/icons/social/opensea_filled.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/explorer/frontend/icons/social/reddit_filled.svg b/explorer/frontend/icons/social/reddit_filled.svg new file mode 100644 index 000000000..a57d9f797 --- /dev/null +++ b/explorer/frontend/icons/social/reddit_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/slack_filled.svg b/explorer/frontend/icons/social/slack_filled.svg new file mode 100644 index 000000000..177f97122 --- /dev/null +++ b/explorer/frontend/icons/social/slack_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/stats.svg b/explorer/frontend/icons/social/stats.svg new file mode 100644 index 000000000..cfc239f88 --- /dev/null +++ b/explorer/frontend/icons/social/stats.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/social/telega.svg b/explorer/frontend/icons/social/telega.svg new file mode 100644 index 000000000..432bc5c76 --- /dev/null +++ b/explorer/frontend/icons/social/telega.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/social/telegram_filled.svg b/explorer/frontend/icons/social/telegram_filled.svg new file mode 100644 index 000000000..87a2ebf52 --- /dev/null +++ b/explorer/frontend/icons/social/telegram_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/twitter.svg b/explorer/frontend/icons/social/twitter.svg new file mode 100644 index 000000000..21e9812ff --- /dev/null +++ b/explorer/frontend/icons/social/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/social/twitter_filled.svg b/explorer/frontend/icons/social/twitter_filled.svg new file mode 100644 index 000000000..0d73b850a --- /dev/null +++ b/explorer/frontend/icons/social/twitter_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/star_filled.svg b/explorer/frontend/icons/star_filled.svg new file mode 100644 index 000000000..7b6312c87 --- /dev/null +++ b/explorer/frontend/icons/star_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/star_outline.svg b/explorer/frontend/icons/star_outline.svg new file mode 100644 index 000000000..05286fa1d --- /dev/null +++ b/explorer/frontend/icons/star_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/stats.svg b/explorer/frontend/icons/stats.svg new file mode 100644 index 000000000..127477d18 --- /dev/null +++ b/explorer/frontend/icons/stats.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/status/error.svg b/explorer/frontend/icons/status/error.svg new file mode 100644 index 000000000..3f7dff92c --- /dev/null +++ b/explorer/frontend/icons/status/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/status/pending.svg b/explorer/frontend/icons/status/pending.svg new file mode 100644 index 000000000..f9e5a88d5 --- /dev/null +++ b/explorer/frontend/icons/status/pending.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer/frontend/icons/status/success.svg b/explorer/frontend/icons/status/success.svg new file mode 100644 index 000000000..ce76c56a4 --- /dev/null +++ b/explorer/frontend/icons/status/success.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/status/warning.svg b/explorer/frontend/icons/status/warning.svg new file mode 100644 index 000000000..c177aea5a --- /dev/null +++ b/explorer/frontend/icons/status/warning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/sun.svg b/explorer/frontend/icons/sun.svg new file mode 100644 index 000000000..f90bf90ea --- /dev/null +++ b/explorer/frontend/icons/sun.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/swap.svg b/explorer/frontend/icons/swap.svg new file mode 100644 index 000000000..c1566be5f --- /dev/null +++ b/explorer/frontend/icons/swap.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/testnet.svg b/explorer/frontend/icons/testnet.svg new file mode 100644 index 000000000..ff01ccf7a --- /dev/null +++ b/explorer/frontend/icons/testnet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/token-placeholder.svg b/explorer/frontend/icons/token-placeholder.svg new file mode 100644 index 000000000..92307c47e --- /dev/null +++ b/explorer/frontend/icons/token-placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/token-transfers.svg b/explorer/frontend/icons/token-transfers.svg new file mode 100644 index 000000000..f3bef44d8 --- /dev/null +++ b/explorer/frontend/icons/token-transfers.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/explorer/frontend/icons/token.svg b/explorer/frontend/icons/token.svg new file mode 100644 index 000000000..933d69406 --- /dev/null +++ b/explorer/frontend/icons/token.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/explorer/frontend/icons/tokens.svg b/explorer/frontend/icons/tokens.svg new file mode 100644 index 000000000..40c10466d --- /dev/null +++ b/explorer/frontend/icons/tokens.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/tokens/xdai.svg b/explorer/frontend/icons/tokens/xdai.svg new file mode 100644 index 000000000..6a719eb2b --- /dev/null +++ b/explorer/frontend/icons/tokens/xdai.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/explorer/frontend/icons/top-accounts.svg b/explorer/frontend/icons/top-accounts.svg new file mode 100644 index 000000000..272f4742a --- /dev/null +++ b/explorer/frontend/icons/top-accounts.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/explorer/frontend/icons/transactions.svg b/explorer/frontend/icons/transactions.svg new file mode 100644 index 000000000..3cefc2141 --- /dev/null +++ b/explorer/frontend/icons/transactions.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/transactions_slim.svg b/explorer/frontend/icons/transactions_slim.svg new file mode 100644 index 000000000..412f46aa3 --- /dev/null +++ b/explorer/frontend/icons/transactions_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/txn_batches.svg b/explorer/frontend/icons/txn_batches.svg new file mode 100644 index 000000000..00dd273c2 --- /dev/null +++ b/explorer/frontend/icons/txn_batches.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/txn_batches_slim.svg b/explorer/frontend/icons/txn_batches_slim.svg new file mode 100644 index 000000000..b10a43073 --- /dev/null +++ b/explorer/frontend/icons/txn_batches_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/uniswap.svg b/explorer/frontend/icons/uniswap.svg new file mode 100644 index 000000000..7abbc79f0 --- /dev/null +++ b/explorer/frontend/icons/uniswap.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/user_op.svg b/explorer/frontend/icons/user_op.svg new file mode 100644 index 000000000..02bc70140 --- /dev/null +++ b/explorer/frontend/icons/user_op.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/user_op_slim.svg b/explorer/frontend/icons/user_op_slim.svg new file mode 100644 index 000000000..d8c64b52b --- /dev/null +++ b/explorer/frontend/icons/user_op_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/validator.svg b/explorer/frontend/icons/validator.svg new file mode 100644 index 000000000..e77bb0ba5 --- /dev/null +++ b/explorer/frontend/icons/validator.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/verification-steps/finalized.svg b/explorer/frontend/icons/verification-steps/finalized.svg new file mode 100644 index 000000000..fbae66d63 --- /dev/null +++ b/explorer/frontend/icons/verification-steps/finalized.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/verification-steps/unfinalized.svg b/explorer/frontend/icons/verification-steps/unfinalized.svg new file mode 100644 index 000000000..49fdbc7dd --- /dev/null +++ b/explorer/frontend/icons/verification-steps/unfinalized.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/icons/verified.svg b/explorer/frontend/icons/verified.svg new file mode 100644 index 000000000..8d6d5eecd --- /dev/null +++ b/explorer/frontend/icons/verified.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/explorer/frontend/icons/verified_slim.svg b/explorer/frontend/icons/verified_slim.svg new file mode 100644 index 000000000..a13930aab --- /dev/null +++ b/explorer/frontend/icons/verified_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/wallet.svg b/explorer/frontend/icons/wallet.svg new file mode 100644 index 000000000..f1765c246 --- /dev/null +++ b/explorer/frontend/icons/wallet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer/frontend/icons/wallets/coinbase.svg b/explorer/frontend/icons/wallets/coinbase.svg new file mode 100644 index 000000000..9a75b44e6 --- /dev/null +++ b/explorer/frontend/icons/wallets/coinbase.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/explorer/frontend/icons/wallets/metamask.svg b/explorer/frontend/icons/wallets/metamask.svg new file mode 100644 index 000000000..220fbc130 --- /dev/null +++ b/explorer/frontend/icons/wallets/metamask.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/icons/wallets/token-pocket.svg b/explorer/frontend/icons/wallets/token-pocket.svg new file mode 100644 index 000000000..fefaa9f26 --- /dev/null +++ b/explorer/frontend/icons/wallets/token-pocket.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/icons/watchlist.svg b/explorer/frontend/icons/watchlist.svg new file mode 100644 index 000000000..6570c0406 --- /dev/null +++ b/explorer/frontend/icons/watchlist.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/instrumentation.node.ts b/explorer/frontend/instrumentation.node.ts new file mode 100644 index 000000000..a42232f4b --- /dev/null +++ b/explorer/frontend/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 { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION, SEMRESATTRS_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions'; + +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); + +const traceExporter = new OTLPTraceExporter(); + +const sdk = new NodeSDK({ + resource: new Resource({ + [SEMRESATTRS_SERVICE_NAME]: 'blockscout_frontend', + [SEMRESATTRS_SERVICE_VERSION]: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || 'unknown_version', + [SEMRESATTRS_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/explorer/frontend/instrumentation.ts b/explorer/frontend/instrumentation.ts new file mode 100644 index 000000000..dc366667c --- /dev/null +++ b/explorer/frontend/instrumentation.ts @@ -0,0 +1,5 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NEXT_OPEN_TELEMETRY_ENABLED === 'true') { + await import('./instrumentation.node'); + } +} diff --git a/explorer/frontend/jest.config.ts b/explorer/frontend/jest.config.ts new file mode 100644 index 000000000..d718d56a7 --- /dev/null +++ b/explorer/frontend/jest.config.ts @@ -0,0 +1,40 @@ +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/explorer/frontend/jest/global-setup.ts b/explorer/frontend/jest/global-setup.ts new file mode 100644 index 000000000..1d17469be --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/jest/lib.tsx b/explorer/frontend/jest/lib.tsx new file mode 100644 index 000000000..f6119698c --- /dev/null +++ b/explorer/frontend/jest/lib.tsx @@ -0,0 +1,57 @@ +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 { Provider as ChakraProvider } from 'toolkit/chakra/provider'; + +import 'lib/setLocale'; + +const PAGE_PROPS = { + cookies: '', + referrer: '', + query: {}, + adBannerProvider: null, + apiData: null, + uuid: '123', +}; + +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/explorer/frontend/jest/mocks/next-router.ts b/explorer/frontend/jest/mocks/next-router.ts new file mode 100644 index 000000000..4213a7ab9 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/jest/setup.ts b/explorer/frontend/jest/setup.ts new file mode 100644 index 000000000..d64cff2d4 --- /dev/null +++ b/explorer/frontend/jest/setup.ts @@ -0,0 +1,55 @@ +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); + }, +}; + +// Polyfill for structuredClone +if (typeof structuredClone === 'undefined') { + global.structuredClone = (obj: T): T => { + try { + return JSON.parse(JSON.stringify(obj)) as T; + } catch (error) { + // Fallback for circular references and other special cases + return obj; + } + }; +} diff --git a/explorer/frontend/jest/utils/flushPromises.ts b/explorer/frontend/jest/utils/flushPromises.ts new file mode 100644 index 000000000..2910c7deb --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/address/bech32.ts b/explorer/frontend/lib/address/bech32.ts new file mode 100644 index 000000000..85fcfda3e --- /dev/null +++ b/explorer/frontend/lib/address/bech32.ts @@ -0,0 +1,49 @@ +import { bech32 } from '@scure/base'; + +import config from 'configs/app'; +import bytesToHex from 'lib/bytesToHex'; +import hexToBytes from 'lib/hexToBytes'; + +export const DATA_PART_REGEXP = /^[\da-z]{38}$/; +export const BECH_32_SEPARATOR = '1'; // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 + +export function toBech32Address(hash: string) { + if (config.UI.views.address.hashFormat.bech32Prefix) { + try { + const words = bech32.toWords(hexToBytes(hash)); + return bech32.encode(config.UI.views.address.hashFormat.bech32Prefix, words); + } catch (error) {} + } + + return hash; +} + +export function isBech32Address(hash: string) { + if (!config.UI.views.address.hashFormat.bech32Prefix) { + return false; + } + + if (!hash.startsWith(`${ config.UI.views.address.hashFormat.bech32Prefix }${ BECH_32_SEPARATOR }`)) { + return false; + } + + const strippedHash = hash.replace(`${ config.UI.views.address.hashFormat.bech32Prefix }${ BECH_32_SEPARATOR }`, ''); + return DATA_PART_REGEXP.test(strippedHash); +} + +export function fromBech32Address(hash: string) { + if (config.UI.views.address.hashFormat.bech32Prefix) { + try { + const { words, prefix } = bech32.decode(hash as `${ string }${ typeof BECH_32_SEPARATOR }${ string }`); + + if (prefix !== config.UI.views.address.hashFormat.bech32Prefix) { + return hash; + } + + const bytes = bech32.fromWords(words); + return bytesToHex(bytes); + } catch (error) {} + } + + return hash; +} diff --git a/explorer/frontend/lib/address/getCheckedSummedAddress.ts b/explorer/frontend/lib/address/getCheckedSummedAddress.ts new file mode 100644 index 000000000..c3779744f --- /dev/null +++ b/explorer/frontend/lib/address/getCheckedSummedAddress.ts @@ -0,0 +1,21 @@ +import { getAddress } from 'viem'; + +import config from 'configs/app'; + +const ERC1191_CHAIN_IDS = [ + '30', // RSK Mainnet + '31', // RSK Testnet +]; + +export default function getCheckedSummedAddress(address: string): string { + try { + return getAddress( + address, + // We need to pass chainId to getAddress to make it work correctly for chains that support ERC-1191 + // https://eips.ethereum.org/EIPS/eip-1191#usage--table + ERC1191_CHAIN_IDS.includes(config.chain.id ?? '') ? Number(config.chain.id) : undefined, + ); + } catch (error) { + return address; + } +} diff --git a/explorer/frontend/lib/address/parseMetaPayload.ts b/explorer/frontend/lib/address/parseMetaPayload.ts new file mode 100644 index 000000000..f8d6fbc57 --- /dev/null +++ b/explorer/frontend/lib/address/parseMetaPayload.ts @@ -0,0 +1,46 @@ +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', + 'data', + 'alertBgColor', + 'alertTextColor', + 'alertStatus', + ]; + + 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/explorer/frontend/lib/address/useAddressMetadataInfoQuery.ts b/explorer/frontend/lib/address/useAddressMetadataInfoQuery.ts new file mode 100644 index 000000000..b6114c262 --- /dev/null +++ b/explorer/frontend/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 = '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/explorer/frontend/lib/api/buildUrl.test.ts b/explorer/frontend/lib/api/buildUrl.test.ts new file mode 100644 index 000000000..c26d197a2 --- /dev/null +++ b/explorer/frontend/lib/api/buildUrl.test.ts @@ -0,0 +1,43 @@ +import buildUrl from './buildUrl'; + +test('builds URL for resource without path params', () => { + const url = buildUrl('general: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('general: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('general: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('general: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('general: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('general: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('general: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('contractInfo:token_verified_info', { chainId: '42', hash: '0x11' }); + expect(url).toBe('https://localhost:3005/api/v1/chains/42/token-infos/0x11'); +}); diff --git a/explorer/frontend/lib/api/buildUrl.ts b/explorer/frontend/lib/api/buildUrl.ts new file mode 100644 index 000000000..33bc8dca7 --- /dev/null +++ b/explorer/frontend/lib/api/buildUrl.ts @@ -0,0 +1,27 @@ +import { compile } from 'path-to-regexp'; + +import config from 'configs/app'; + +import getResourceParams from './getResourceParams'; +import isNeedProxy from './isNeedProxy'; +import type { ResourceName, ResourcePathParams } from './resources'; + +export default function buildUrl( + resourceFullName: R, + pathParams?: ResourcePathParams, + queryParams?: Record | number | boolean | null | undefined>, + noProxy?: boolean, +): string { + const { api, resource } = getResourceParams(resourceFullName); + const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : api.endpoint; + const basePath = api.basePath ?? ''; + const path = !noProxy && 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/explorer/frontend/lib/api/getResourceParams.ts b/explorer/frontend/lib/api/getResourceParams.ts new file mode 100644 index 000000000..1dca71201 --- /dev/null +++ b/explorer/frontend/lib/api/getResourceParams.ts @@ -0,0 +1,21 @@ +import type { ApiName, ApiResource } from './types'; + +import config from 'configs/app'; + +import type { ResourceName } from './resources'; +import { RESOURCES } from './resources'; + +export default function getResourceParams(resourceFullName: ResourceName) { + const [ apiName, resourceName ] = resourceFullName.split(':') as [ ApiName, string ]; + const apiConfig = config.apis[apiName]; + + if (!apiConfig) { + throw new Error(`API config for ${ apiName } not found`); + } + + return { + api: apiConfig, + apiName, + resource: RESOURCES[apiName][resourceName as keyof typeof RESOURCES[ApiName]] as ApiResource, + }; +} diff --git a/explorer/frontend/lib/api/isBodyAllowed.ts b/explorer/frontend/lib/api/isBodyAllowed.ts new file mode 100644 index 000000000..aad52fedb --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/api/isNeedProxy.ts b/explorer/frontend/lib/api/isNeedProxy.ts new file mode 100644 index 000000000..bd2089d67 --- /dev/null +++ b/explorer/frontend/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.apis.general.host; +} diff --git a/explorer/frontend/lib/api/resources.ts b/explorer/frontend/lib/api/resources.ts new file mode 100644 index 000000000..b25c1698c --- /dev/null +++ b/explorer/frontend/lib/api/resources.ts @@ -0,0 +1,127 @@ +import type { ApiName, ApiResource } from './types'; + +import type { AdminApiResourceName, AdminApiResourcePayload } from './services/admin'; +import { ADMIN_API_RESOURCES } from './services/admin'; +import { BENS_API_RESOURCES } from './services/bens'; +import type { BensApiResourceName, BensApiResourcePayload, BensApiPaginationFilters, BensApiPaginationSorting } from './services/bens'; +import { CHANNELS_API_RESOURCES } from './services/channels'; +import type { ChannelsApiResourceName, ChannelsApiResourcePayload } from './services/channels'; +import { CONTRACT_INFO_API_RESOURCES } from './services/contractInfo'; +import type { ContractInfoApiPaginationFilters, ContractInfoApiResourceName, ContractInfoApiResourcePayload } from './services/contractInfo'; +import { GENERAL_API_RESOURCES } from './services/general'; +import type { GeneralApiResourceName, GeneralApiResourcePayload, GeneralApiPaginationFilters, GeneralApiPaginationSorting } from './services/general'; +import type { MetadataApiResourceName, MetadataApiResourcePayload } from './services/metadata'; +import { METADATA_API_RESOURCES } from './services/metadata'; +import type { RewardsApiResourceName, RewardsApiResourcePayload } from './services/rewards'; +import { REWARDS_API_RESOURCES } from './services/rewards'; +import type { StatsApiResourceName, StatsApiResourcePayload } from './services/stats'; +import { STATS_API_RESOURCES } from './services/stats'; +import type { IsPaginated } from './services/utils'; +import { VISUALIZE_API_RESOURCES } from './services/visualize'; +import type { VisualizeApiResourceName, VisualizeApiResourcePayload } from './services/visualize'; + +export const RESOURCES = { + admin: ADMIN_API_RESOURCES, + bens: BENS_API_RESOURCES, + channels: CHANNELS_API_RESOURCES, + contractInfo: CONTRACT_INFO_API_RESOURCES, + general: GENERAL_API_RESOURCES, + metadata: METADATA_API_RESOURCES, + rewards: REWARDS_API_RESOURCES, + stats: STATS_API_RESOURCES, + visualize: VISUALIZE_API_RESOURCES, +} satisfies Record>; + +export const resourceKey = (x: ResourceName) => x; + +export type ResourceName = { + [K in keyof typeof RESOURCES]: `${ K & string }:${ keyof (typeof RESOURCES)[K] & string }` +}[keyof typeof RESOURCES]; + +export type ResourcePath = string; + +/* eslint-disable @stylistic/indent */ +export type ResourcePayload = +R extends AdminApiResourceName ? AdminApiResourcePayload : +R extends BensApiResourceName ? BensApiResourcePayload : +R extends ChannelsApiResourceName ? ChannelsApiResourcePayload : +R extends ContractInfoApiResourceName ? ContractInfoApiResourcePayload : +R extends GeneralApiResourceName ? GeneralApiResourcePayload : +R extends MetadataApiResourceName ? MetadataApiResourcePayload : +R extends RewardsApiResourceName ? RewardsApiResourcePayload : +R extends StatsApiResourceName ? StatsApiResourcePayload : +R extends VisualizeApiResourceName ? VisualizeApiResourcePayload : +never; +/* eslint-enable @stylistic/indent */ + +type ResourcePathParamName = Q extends `${ infer A }:${ infer R }` ? + (typeof RESOURCES)[A & keyof typeof RESOURCES][R & keyof (typeof RESOURCES)[A & keyof typeof RESOURCES]] extends { pathParams: Array } ? + (typeof RESOURCES)[A & keyof typeof RESOURCES][R & keyof (typeof RESOURCES)[A & keyof typeof RESOURCES]]['pathParams'][number] : + never : + never; + +export type ResourcePathParams = Q extends `${ infer A }:${ infer R }` ? + (typeof RESOURCES)[A & keyof typeof RESOURCES][R & keyof (typeof RESOURCES)[A & keyof typeof RESOURCES]] extends { pathParams: Array } ? + Record, string | undefined> : + never : + never; + +export interface ResourceError { + payload?: T; + status: Response['status']; + statusText: Response['statusText']; +} + +export type ResourceErrorAccount = ResourceError<{ errors: T }>; + +// PAGINATION + +/* eslint-disable @stylistic/indent */ +export type PaginationFilters = +R extends BensApiResourceName ? BensApiPaginationFilters : +R extends GeneralApiResourceName ? GeneralApiPaginationFilters : +R extends ContractInfoApiResourceName ? ContractInfoApiPaginationFilters : +never; +/* eslint-enable @stylistic/indent */ + +export const SORTING_FIELDS = [ 'sort', 'order' ]; + +/* eslint-disable @stylistic/indent */ +export type PaginationSorting = +R extends BensApiResourceName ? BensApiPaginationSorting : +R extends GeneralApiResourceName ? GeneralApiPaginationSorting : +never; +/* eslint-enable @stylistic/indent */ + +export type PaginatedResourceName = { + [A in keyof typeof RESOURCES]: { + [R in keyof (typeof RESOURCES)[A]]: (typeof RESOURCES)[A][R] extends ApiResource ? + IsPaginated<(typeof RESOURCES)[A][R]> extends true ? `${ A & string }:${ R & string }` : never : + never + }[keyof (typeof RESOURCES)[A]] +}[keyof typeof RESOURCES]; + +export type PaginatedResourceResponse = ResourcePayload; + +export type PaginatedResourceResponseItems = R extends PaginatedResourceName ? + ResourcePayload['items'] : + never; + +export type PaginatedResourceResponseNextPageParams = R extends PaginatedResourceName ? + ResourcePayload['next_page_params'] : + never; + +// TESTS +export const a: ResourcePayload<'general:api_keys'> = [ { + api_key: '123', + name: '123', +} ]; + +export const b: PaginatedResourceName = 'general:addresses'; + +export const c: PaginatedResourceResponseItems<'general:addresses'> = []; + +export const d: ResourcePathParams<'bens:address_domain'> = { + chainId: '1', + address: '123', +}; diff --git a/explorer/frontend/lib/api/services/admin.ts b/explorer/frontend/lib/api/services/admin.ts new file mode 100644 index 000000000..15c61b4eb --- /dev/null +++ b/explorer/frontend/lib/api/services/admin.ts @@ -0,0 +1,37 @@ +import type { ApiResource } from '../types'; +import type { TokenInfoApplicationConfig, TokenInfoApplications } from 'types/api/account'; +import type { MarketplaceAppOverview } from 'types/client/marketplace'; + +export const ADMIN_API_RESOURCES = { + public_tag_application: { + path: '/api/v1/chains/:chainId/metadata-submissions/tag', + pathParams: [ 'chainId' as const ], + }, + token_info_applications_config: { + path: '/api/v1/chains/:chainId/token-info-submissions/selectors', + pathParams: [ 'chainId' as const ], + }, + token_info_applications: { + path: '/api/v1/chains/:chainId/token-info-submissions{/:id}', + pathParams: [ 'chainId' as const, 'id' as const ], + }, + marketplace_dapps: { + path: '/api/v1/chains/:chainId/marketplace/dapps', + pathParams: [ 'chainId' as const ], + }, + marketplace_dapp: { + path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId', + pathParams: [ 'chainId' as const, 'dappId' as const ], + }, +} satisfies Record; + +export type AdminApiResourceName = `admin:${ keyof typeof ADMIN_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type AdminApiResourcePayload = +R extends 'admin:token_info_applications_config' ? TokenInfoApplicationConfig : +R extends 'admin:token_info_applications' ? TokenInfoApplications : +R extends 'admin:marketplace_dapps' ? Array : +R extends 'admin:marketplace_dapp' ? MarketplaceAppOverview : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/bens.ts b/explorer/frontend/lib/api/services/bens.ts new file mode 100644 index 000000000..7238b7744 --- /dev/null +++ b/explorer/frontend/lib/api/services/bens.ts @@ -0,0 +1,61 @@ +import type { ApiResource } from '../types'; +import type * as bens from '@blockscout/bens-types'; +import type { EnsAddressLookupFilters, EnsDomainLookupFilters, EnsLookupSorting } from 'types/api/ens'; + +export const BENS_API_RESOURCES = { + addresses_lookup: { + path: '/api/v1/:chainId/addresses\\:lookup', + pathParams: [ 'chainId' as const ], + filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const, 'protocols' as const ], + paginated: true, + }, + address_domain: { + path: '/api/v1/:chainId/addresses/:address', + pathParams: [ 'chainId' as const, 'address' as const ], + }, + domain_info: { + path: '/api/v1/:chainId/domains/:name', + pathParams: [ 'chainId' as const, 'name' as const ], + }, + domain_events: { + path: '/api/v1/:chainId/domains/:name/events', + pathParams: [ 'chainId' as const, 'name' as const ], + }, + domains_lookup: { + path: '/api/v1/:chainId/domains\\:lookup', + pathParams: [ 'chainId' as const ], + filterFields: [ 'name' as const, 'only_active' as const, 'protocols' as const ], + paginated: true, + }, + domain_protocols: { + path: '/api/v1/:chainId/protocols', + pathParams: [ 'chainId' as const ], + }, +} satisfies Record; + +export type BensApiResourceName = `bens:${ keyof typeof BENS_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type BensApiResourcePayload = +R extends 'bens:addresses_lookup' ? bens.LookupAddressResponse : +R extends 'bens:address_domain' ? bens.GetAddressResponse : +R extends 'bens:domain_info' ? bens.DetailedDomain : +R extends 'bens:domain_events' ? bens.ListDomainEventsResponse : +R extends 'bens:domains_lookup' ? bens.LookupDomainNameResponse : +R extends 'bens:domain_protocols' ? bens.GetProtocolsResponse : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type BensApiPaginationFilters = +R extends 'bens:addresses_lookup' ? EnsAddressLookupFilters : +R extends 'bens:domains_lookup' ? EnsDomainLookupFilters : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type BensApiPaginationSorting = +R extends 'bens:addresses_lookup' ? EnsLookupSorting : +R extends 'bens:domains_lookup' ? EnsLookupSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/channels.ts b/explorer/frontend/lib/api/services/channels.ts new file mode 100644 index 000000000..d0eba15e4 --- /dev/null +++ b/explorer/frontend/lib/api/services/channels.ts @@ -0,0 +1,16 @@ +import type { ApiResource } from '../types'; +import type { SearchResultChannel } from 'types/api/search'; + +export const CHANNELS_API_RESOURCES = { + search: { + path: '/api/channels', + }, +} satisfies Record; + +export type ChannelsApiResourceName = `channels:${keyof typeof CHANNELS_API_RESOURCES}`; + +/* eslint-disable @stylistic/indent */ +export type ChannelsApiResourcePayload = +R extends 'channels:search' ? Array : +never; +/* eslint-enable @stylistic/indent */ \ No newline at end of file diff --git a/explorer/frontend/lib/api/services/contractInfo.ts b/explorer/frontend/lib/api/services/contractInfo.ts new file mode 100644 index 000000000..e0d67bd57 --- /dev/null +++ b/explorer/frontend/lib/api/services/contractInfo.ts @@ -0,0 +1,46 @@ +import type { ApiResource } from '../types'; +import type { VerifiedAddressResponse } from 'types/api/account'; +import type { Pool, PoolsResponse } from 'types/api/pools'; +import type { TokenVerifiedInfo } from 'types/api/token'; + +export const CONTRACT_INFO_API_RESOURCES = { + address_verification: { + path: '/api/v1/chains/:chainId/verified-addresses:type', + pathParams: [ 'chainId' as const, 'type' as const ], + }, + verified_addresses: { + path: '/api/v1/chains/:chainId/verified-addresses', + pathParams: [ 'chainId' as const ], + }, + token_verified_info: { + path: '/api/v1/chains/:chainId/token-infos/:hash', + pathParams: [ 'chainId' as const, 'hash' as const ], + }, + pools: { + path: '/api/v1/chains/:chainId/pools', + pathParams: [ 'chainId' as const ], + filterFields: [ 'query' as const ], + paginated: true, + }, + pool: { + path: '/api/v1/chains/:chainId/pools/:hash', + pathParams: [ 'chainId' as const, 'hash' as const ], + }, +} satisfies Record; + +export type ContractInfoApiResourceName = `contractInfo:${ keyof typeof CONTRACT_INFO_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type ContractInfoApiResourcePayload = +R extends 'contractInfo:verified_addresses' ? VerifiedAddressResponse : +R extends 'contractInfo:token_verified_info' ? TokenVerifiedInfo : +R extends 'contractInfo:pools' ? PoolsResponse : +R extends 'contractInfo:pool' ? Pool : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type ContractInfoApiPaginationFilters = +R extends 'contractInfo:pools' ? { query: string } : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/account.ts b/explorer/frontend/lib/api/services/general/account.ts new file mode 100644 index 000000000..0a4e8f44c --- /dev/null +++ b/explorer/frontend/lib/api/services/general/account.ts @@ -0,0 +1,74 @@ +import type { ApiResource } from '../../types'; +import type { AddressTagsResponse, ApiKeys, CustomAbis, TransactionTagsResponse, UserInfo, WatchlistResponse } from 'types/api/account'; + +export const GENERAL_API_ACCOUNT_RESOURCES = { + // ACCOUNT + csrf: { + path: '/api/account/v2/get_csrf', + }, + user_info: { + path: '/api/account/v2/user/info', + }, + 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: [ ], + paginated: true, + }, + private_tags_address: { + path: '/api/account/v2/user/tags/address{/:id}', + pathParams: [ 'id' as const ], + filterFields: [ ], + paginated: true, + }, + private_tags_tx: { + path: '/api/account/v2/user/tags/transaction{/:id}', + pathParams: [ 'id' as const ], + filterFields: [ ], + paginated: true, + }, + api_keys: { + path: '/api/account/v2/user/api_keys{/:id}', + pathParams: [ 'id' as const ], + }, + + // AUTH + auth_send_otp: { + path: '/api/account/v2/send_otp', + }, + auth_confirm_otp: { + path: '/api/account/v2/confirm_otp', + }, + auth_siwe_message: { + path: '/api/account/v2/siwe_message', + }, + auth_siwe_verify: { + path: '/api/account/v2/authenticate_via_wallet', + }, + auth_link_email: { + path: '/api/account/v2/email/link', + }, + auth_link_address: { + path: '/api/account/v2/address/link', + }, + auth_logout: { + path: '/api/account/auth/logout', + }, +} satisfies Record; + +export type GeneralApiAccountResourceName = `general:${ keyof typeof GENERAL_API_ACCOUNT_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiAccountResourcePayload = +R extends 'general:user_info' ? UserInfo : +R extends 'general:custom_abi' ? CustomAbis : +R extends 'general:private_tags_address' ? AddressTagsResponse : +R extends 'general:private_tags_tx' ? TransactionTagsResponse : +R extends 'general:api_keys' ? ApiKeys : +R extends 'general:watchlist' ? WatchlistResponse : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/address.ts b/explorer/frontend/lib/api/services/general/address.ts new file mode 100644 index 000000000..e06cef624 --- /dev/null +++ b/explorer/frontend/lib/api/services/general/address.ts @@ -0,0 +1,170 @@ +import type { ApiResource } from '../../types'; +import type { + AddressCounters, + AddressBlocksValidatedResponse, + AddressTokensResponse, + AddressCollectionsResponse, + AddressEpochRewardsResponse, + AddressNFTsResponse, + AddressWithdrawalsResponse, + AddressXStarResponse, + AddressCoinBalanceHistoryChart, + AddressCoinBalanceHistoryResponse, + AddressTokenTransferResponse, + AddressInternalTxsResponse, + AddressTransactionsResponse, + AddressTabsCounters, + Address, + AddressTxsFilters, + AddressTokenTransferFilters, + AddressTokensFilter, + AddressNFTTokensFilter, +} from 'types/api/address'; +import type { AddressesMetadataSearchFilters, AddressesMetadataSearchResult, AddressesResponse } from 'types/api/addresses'; +import type { LogsResponseAddress } from 'types/api/log'; +import type { TransactionsSorting } from 'types/api/transaction'; + +export const GENERAL_API_ADDRESS_RESOURCES = { + // ADDRESSES + addresses: { + path: '/api/v2/addresses/', + filterFields: [ ], + paginated: true, + }, + addresses_metadata_search: { + path: '/api/v2/proxy/metadata/addresses', + filterFields: [ 'slug' as const, 'tag_type' as const ], + paginated: true, + }, + + // ADDRESS INFO + 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 ], + }, + address_txs: { + path: '/api/v2/addresses/:hash/transactions', + pathParams: [ 'hash' as const ], + filterFields: [ 'filter' as const ], + paginated: true, + }, + address_internal_txs: { + path: '/api/v2/addresses/:hash/internal-transactions', + pathParams: [ 'hash' as const ], + filterFields: [ 'filter' as const ], + paginated: true, + }, + address_token_transfers: { + path: '/api/v2/addresses/:hash/token-transfers', + pathParams: [ 'hash' as const ], + filterFields: [ 'filter' as const, 'type' as const, 'token' as const ], + paginated: true, + }, + address_blocks_validated: { + path: '/api/v2/addresses/:hash/blocks-validated', + pathParams: [ 'hash' as const ], + filterFields: [ ], + paginated: true, + }, + address_coin_balance: { + path: '/api/v2/addresses/:hash/coin-balance-history', + pathParams: [ 'hash' as const ], + filterFields: [ ], + paginated: true, + }, + address_coin_balance_chart: { + path: '/api/v2/addresses/:hash/coin-balance-history-by-day', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + address_logs: { + path: '/api/v2/addresses/:hash/logs', + pathParams: [ 'hash' as const ], + filterFields: [ ], + paginated: true, + }, + address_tokens: { + path: '/api/v2/addresses/:hash/tokens', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + paginated: true, + }, + address_nfts: { + path: '/api/v2/addresses/:hash/nft', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + paginated: true, + }, + address_collections: { + path: '/api/v2/addresses/:hash/nft/collections', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + paginated: true, + }, + address_withdrawals: { + path: '/api/v2/addresses/:hash/withdrawals', + pathParams: [ 'hash' as const ], + filterFields: [], + paginated: true, + }, + address_epoch_rewards: { + path: '/api/v2/addresses/:hash/election-rewards', + pathParams: [ 'hash' as const ], + filterFields: [], + paginated: true, + }, + address_xstar_score: { + path: '/api/v2/proxy/3dparty/xname/addresses/:hash', + pathParams: [ 'hash' as const ], + }, +} satisfies Record; + +export type GeneralApiAddressResourceName = `general:${ keyof typeof GENERAL_API_ADDRESS_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiAddressResourcePayload = +R extends 'general:addresses' ? AddressesResponse : +R extends 'general:addresses_metadata_search' ? AddressesMetadataSearchResult : +R extends 'general:address' ? Address : +R extends 'general:address_counters' ? AddressCounters : +R extends 'general:address_tabs_counters' ? AddressTabsCounters : +R extends 'general:address_txs' ? AddressTransactionsResponse : +R extends 'general:address_internal_txs' ? AddressInternalTxsResponse : +R extends 'general:address_token_transfers' ? AddressTokenTransferResponse : +R extends 'general:address_blocks_validated' ? AddressBlocksValidatedResponse : +R extends 'general:address_coin_balance' ? AddressCoinBalanceHistoryResponse : +R extends 'general:address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : +R extends 'general:address_logs' ? LogsResponseAddress : +R extends 'general:address_tokens' ? AddressTokensResponse : +R extends 'general:address_nfts' ? AddressNFTsResponse : +R extends 'general:address_collections' ? AddressCollectionsResponse : +R extends 'general:address_withdrawals' ? AddressWithdrawalsResponse : +R extends 'general:address_epoch_rewards' ? AddressEpochRewardsResponse : +R extends 'general:address_xstar_score' ? AddressXStarResponse : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiAddressPaginationFilters = +R extends 'general:addresses_metadata_search' ? AddressesMetadataSearchFilters : +R extends 'general:address_txs' | 'general:address_internal_txs' ? AddressTxsFilters : +R extends 'general:address_token_transfers' ? AddressTokenTransferFilters : +R extends 'general:address_tokens' ? AddressTokensFilter : +R extends 'general:address_nfts' ? AddressNFTTokensFilter : +R extends 'general:address_collections' ? AddressNFTTokensFilter : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiAddressPaginationSorting = +R extends 'general:address_txs' ? TransactionsSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/block.ts b/explorer/frontend/lib/api/services/general/block.ts new file mode 100644 index 000000000..577dc2ecb --- /dev/null +++ b/explorer/frontend/lib/api/services/general/block.ts @@ -0,0 +1,68 @@ +import type { ApiResource } from '../../types'; +import type { + BlocksResponse, + BlockTransactionsResponse, + Block, + BlockFilters, + BlockWithdrawalsResponse, + BlockCountdownResponse, + BlockEpoch, + BlockEpochElectionRewardDetailsResponse, +} from 'types/api/block'; +import type { TTxsWithBlobsFilters } from 'types/api/txsFilters'; + +export const GENERAL_API_BLOCK_RESOURCES = { + blocks: { + path: '/api/v2/blocks', + filterFields: [ 'type' as const ], + paginated: true, + }, + 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 ], + paginated: true, + }, + block_withdrawals: { + path: '/api/v2/blocks/:height_or_hash/withdrawals', + pathParams: [ 'height_or_hash' as const ], + filterFields: [], + paginated: true, + }, + block_epoch: { + path: '/api/v2/blocks/:height_or_hash/epoch', + pathParams: [ 'height_or_hash' as const ], + filterFields: [], + }, + block_election_rewards: { + path: '/api/v2/blocks/:height_or_hash/election-rewards/:reward_type', + pathParams: [ 'height_or_hash' as const, 'reward_type' as const ], + filterFields: [], + paginated: true, + }, +} satisfies Record; + +export type GeneralApiBlockResourceName = `general:${ keyof typeof GENERAL_API_BLOCK_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiBlockResourcePayload = +R extends 'general:blocks' ? BlocksResponse : +R extends 'general:block' ? Block : +R extends 'general:block_countdown' ? BlockCountdownResponse : +R extends 'general:block_txs' ? BlockTransactionsResponse : +R extends 'general:block_withdrawals' ? BlockWithdrawalsResponse : +R extends 'general:block_epoch' ? BlockEpoch : +R extends 'general:block_election_rewards' ? BlockEpochElectionRewardDetailsResponse : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiBlockPaginationFilters = +R extends 'general:blocks' ? BlockFilters : +R extends 'general:block_txs' ? TTxsWithBlobsFilters : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/contract.ts b/explorer/frontend/lib/api/services/general/contract.ts new file mode 100644 index 000000000..9598e3358 --- /dev/null +++ b/explorer/frontend/lib/api/services/general/contract.ts @@ -0,0 +1,63 @@ +import type { ApiResource } from '../../types'; +import type { + SmartContract, + SmartContractSecurityAudits, + SmartContractVerificationConfigRaw, +} from 'types/api/contract'; +import type { VerifiedContractsResponse, VerifiedContractsCounters, VerifiedContractsFilters } from 'types/api/contracts'; +import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; + +export const GENERAL_API_CONTRACT_RESOURCES = { + 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_solidity_scan_report: { + path: '/api/v2/proxy/3dparty/solidityscan/smart-contracts/:hash/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 ], + paginated: true, + }, + verified_contracts_counters: { + path: '/api/v2/smart-contracts/counters', + }, +} satisfies Record; + +export type GeneralApiContractResourceName = `general:${ keyof typeof GENERAL_API_CONTRACT_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiContractResourcePayload = +R extends 'general:contract' ? SmartContract : +R extends 'general:contract_solidity_scan_report' ? unknown : +R extends 'general:verified_contracts' ? VerifiedContractsResponse : +R extends 'general:verified_contracts_counters' ? VerifiedContractsCounters : +R extends 'general:contract_verification_config' ? SmartContractVerificationConfigRaw : +R extends 'general:contract_security_audits' ? SmartContractSecurityAudits : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiContractPaginationFilters = +R extends 'general:verified_contracts' ? VerifiedContractsFilters : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiContractPaginationSorting = +R extends 'general:verified_contracts' ? VerifiedContractsSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/index.ts b/explorer/frontend/lib/api/services/general/index.ts new file mode 100644 index 000000000..3fa1e089f --- /dev/null +++ b/explorer/frontend/lib/api/services/general/index.ts @@ -0,0 +1,97 @@ +import type { ApiResource } from '../../types'; + +import type { GeneralApiAccountResourceName, GeneralApiAccountResourcePayload } from './account'; +import { GENERAL_API_ACCOUNT_RESOURCES } from './account'; +import type { + GeneralApiAddressPaginationFilters, + GeneralApiAddressPaginationSorting, + GeneralApiAddressResourceName, + GeneralApiAddressResourcePayload, +} from './address'; +import { GENERAL_API_ADDRESS_RESOURCES } from './address'; +import type { + GeneralApiBlockPaginationFilters, + GeneralApiBlockResourceName, GeneralApiBlockResourcePayload } from './block'; +import { GENERAL_API_BLOCK_RESOURCES } from './block'; +import { GENERAL_API_CONTRACT_RESOURCES } from './contract'; +import type { + GeneralApiContractPaginationFilters, + GeneralApiContractPaginationSorting, + GeneralApiContractResourceName, + GeneralApiContractResourcePayload, +} from './contract'; +import type { + GeneralApiMiscPaginationFilters, + GeneralApiMiscPaginationSorting, + GeneralApiMiscResourceName, + GeneralApiMiscResourcePayload, +} from './misc'; +import { GENERAL_API_MISC_RESOURCES } from './misc'; +import type { + GeneralApiRollupPaginationFilters, + GeneralApiRollupPaginationSorting, + GeneralApiRollupResourceName, + GeneralApiRollupResourcePayload, +} from './rollup'; +import { GENERAL_API_ROLLUP_RESOURCES } from './rollup'; +import type { + GeneralApiTokenPaginationFilters, + GeneralApiTokenPaginationSorting, + GeneralApiTokenResourceName, + GeneralApiTokenResourcePayload, +} from './token'; +import { GENERAL_API_TOKEN_RESOURCES } from './token'; +import type { GeneralApiTxResourceName, GeneralApiTxResourcePayload, GeneralApiTxPaginationFilters } from './tx'; +import { GENERAL_API_TX_RESOURCES } from './tx'; +import type { GeneralApiV1ResourceName, GeneralApiV1ResourcePayload } from './v1'; +import { GENERAL_API_V1_RESOURCES } from './v1'; + +export const GENERAL_API_RESOURCES = { + ...GENERAL_API_ACCOUNT_RESOURCES, + ...GENERAL_API_ADDRESS_RESOURCES, + ...GENERAL_API_BLOCK_RESOURCES, + ...GENERAL_API_CONTRACT_RESOURCES, + ...GENERAL_API_MISC_RESOURCES, + ...GENERAL_API_ROLLUP_RESOURCES, + ...GENERAL_API_TOKEN_RESOURCES, + ...GENERAL_API_TX_RESOURCES, + ...GENERAL_API_V1_RESOURCES, +} satisfies Record; + +export type GeneralApiResourceName = `general:${ keyof typeof GENERAL_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiResourcePayload = +R extends GeneralApiAccountResourceName ? GeneralApiAccountResourcePayload : +R extends GeneralApiAddressResourceName ? GeneralApiAddressResourcePayload : +R extends GeneralApiBlockResourceName ? GeneralApiBlockResourcePayload : +R extends GeneralApiContractResourceName ? GeneralApiContractResourcePayload : +R extends GeneralApiMiscResourceName ? GeneralApiMiscResourcePayload : +R extends GeneralApiRollupResourceName ? GeneralApiRollupResourcePayload : +R extends GeneralApiTokenResourceName ? GeneralApiTokenResourcePayload : +R extends GeneralApiTxResourceName ? GeneralApiTxResourcePayload : +R extends GeneralApiV1ResourceName ? GeneralApiV1ResourcePayload : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiPaginationFilters = +R extends GeneralApiAddressResourceName ? GeneralApiAddressPaginationFilters : +R extends GeneralApiBlockResourceName ? GeneralApiBlockPaginationFilters : +R extends GeneralApiContractResourceName ? GeneralApiContractPaginationFilters : +R extends GeneralApiMiscResourceName ? GeneralApiMiscPaginationFilters : +R extends GeneralApiRollupResourceName ? GeneralApiRollupPaginationFilters : +R extends GeneralApiTokenResourceName ? GeneralApiTokenPaginationFilters : +R extends GeneralApiTxResourceName ? GeneralApiTxPaginationFilters : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiPaginationSorting = +R extends GeneralApiAddressResourceName ? GeneralApiAddressPaginationSorting : +R extends GeneralApiContractResourceName ? GeneralApiContractPaginationSorting : +R extends GeneralApiMiscResourceName ? GeneralApiMiscPaginationSorting : +R extends GeneralApiRollupResourceName ? GeneralApiRollupPaginationSorting : +R extends GeneralApiTokenResourceName ? GeneralApiTokenPaginationSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/misc.ts b/explorer/frontend/lib/api/services/general/misc.ts new file mode 100644 index 000000000..1751f9f9b --- /dev/null +++ b/explorer/frontend/lib/api/services/general/misc.ts @@ -0,0 +1,299 @@ +import type { ApiResource } from '../../types'; +import type { AdvancedFilterParams, AdvancedFilterResponse, AdvancedFilterMethodsResponse } from 'types/api/advancedFilter'; +import type { + ArbitrumL2TxnBatchesItem, + ArbitrumLatestDepositsResponse, +} from 'types/api/arbitrumL2'; +import type { Blob } from 'types/api/blobs'; +import type { Block } from 'types/api/block'; +import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; +import type { BackendVersionConfig, CsvExportConfig } from 'types/api/configs'; +import type { IndexingStatus } from 'types/api/indexingStatus'; +import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves'; +import type { + OptimisticL2DepositsItem, +} from 'types/api/optimisticL2'; +import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; +import type { HomeStats } from 'types/api/stats'; +import type { + Transaction, +} from 'types/api/transaction'; +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; +import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps'; +import type { + ValidatorsStabilityCountersResponse, + ValidatorsStabilityFilters, + ValidatorsStabilityResponse, + ValidatorsStabilitySorting, + ValidatorsBlackfortCountersResponse, + ValidatorsBlackfortResponse, + ValidatorsBlackfortSorting, + ValidatorsZilliqaResponse, + ValidatorZilliqa, +} from 'types/api/validators'; +import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; +import type { + ZkEvmL2TxnBatchesItem, +} from 'types/api/zkEvmL2'; + +export const GENERAL_API_MISC_RESOURCES = { + // WITHDRAWALS + withdrawals: { + path: '/api/v2/withdrawals', + filterFields: [], + paginated: true, + }, + withdrawals_counters: { + path: '/api/v2/withdrawals/counters', + }, + + // 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_optimistic_deposits: { + path: '/api/v2/main-page/optimism-deposits', + }, + homepage_arbitrum_deposits: { + path: '/api/v2/main-page/arbitrum/messages/to-rollup', + }, + homepage_txs: { + path: '/api/v2/main-page/transactions', + }, + homepage_zkevm_l2_batches: { + path: '/api/v2/main-page/zkevm/batches/confirmed', + }, + homepage_arbitrum_l2_batches: { + path: '/api/v2/main-page/arbitrum/batches/committed', + }, + 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', + }, + homepage_arbitrum_latest_batch: { + path: '/api/v2/main-page/arbitrum/batches/latest-number', + }, + + // SEARCH + quick_search: { + path: '/api/v2/search/quick', + filterFields: [ 'q' ], + }, + search: { + path: '/api/v2/search', + filterFields: [ 'q' ], + paginated: true, + }, + search_check_redirect: { + path: '/api/v2/search/check-redirect', + }, + + // NOVES-FI + noves_transaction: { + path: '/api/v2/proxy/3dparty/noves-fi/transactions/:hash', + pathParams: [ 'hash' as const ], + }, + noves_address_history: { + path: '/api/v2/proxy/3dparty/noves-fi/addresses/:address/transactions', + pathParams: [ 'address' as const ], + filterFields: [], + paginated: true, + }, + noves_describe_txs: { + path: '/api/v2/proxy/3dparty/noves-fi/transaction-descriptions', + }, + + // USER OPS + user_ops: { + path: '/api/v2/proxy/account-abstraction/operations', + filterFields: [ 'transaction_hash' as const, 'sender' as const ], + paginated: true, + }, + 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_stability: { + path: '/api/v2/validators/stability', + filterFields: [ 'address_hash' as const, 'state_filter' as const ], + paginated: true, + }, + validators_stability_counters: { + path: '/api/v2/validators/stability/counters', + }, + validators_blackfort: { + path: '/api/v2/validators/blackfort', + filterFields: [], + paginated: true, + }, + validators_blackfort_counters: { + path: '/api/v2/validators/blackfort/counters', + }, + validators_zilliqa: { + path: '/api/v2/validators/zilliqa', + filterFields: [], + paginated: true, + }, + validator_zilliqa: { + path: '/api/v2/validators/zilliqa/:bls_public_key', + pathParams: [ 'bls_public_key' as const ], + filterFields: [], + }, + + // BLOBS + blob: { + path: '/api/v2/blobs/:hash', + pathParams: [ 'hash' as const ], + }, + + // ADVANCED FILTER + advanced_filter: { + path: '/api/v2/advanced-filters', + filterFields: [ + 'transaction_types' as const, + 'methods' as const, + 'methods_names' as const /* frontend only */, + 'age_from' as const, + 'age_to' as const, + 'age' as const /* frontend only */, + 'from_address_hashes_to_include' as const, + 'from_address_hashes_to_exclude' as const, + 'to_address_hashes_to_include' as const, + 'to_address_hashes_to_exclude' as const, + 'address_relation' as const, + 'amount_from' as const, + 'amount_to' as const, + 'token_contract_address_hashes_to_include' as const, + 'token_contract_symbols_to_include' as const /* frontend only */, + 'token_contract_address_hashes_to_exclude' as const, + 'token_contract_symbols_to_exclude' as const /* frontend only */, + 'block_number' as const, + 'transaction_index' as const, + 'internal_transaction_index' as const, + 'token_transfer_index' as const, + ], + paginated: true, + }, + advanced_filter_methods: { + path: '/api/v2/advanced-filters/methods', + filterFields: [ 'q' as const ], + }, + advanced_filter_csv: { + path: '/api/v2/advanced-filters/csv', + }, + + // CONFIGS + config_backend_version: { + path: '/api/v2/config/backend-version', + }, + config_csv_export: { + path: '/api/v2/config/csv-export', + }, + + // 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', + }, +} satisfies Record; + +export type GeneralApiMiscResourceName = `general:${ keyof typeof GENERAL_API_MISC_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiMiscResourcePayload = +R extends 'general:stats' ? HomeStats : +R extends 'general:stats_charts_txs' ? ChartTransactionResponse : +R extends 'general:stats_charts_market' ? ChartMarketResponse : +R extends 'general:stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse : +R extends 'general:homepage_blocks' ? Array : +R extends 'general:homepage_txs' ? Array : +R extends 'general:homepage_txs_watchlist' ? Array : +R extends 'general:homepage_optimistic_deposits' ? Array : +R extends 'general:homepage_arbitrum_deposits' ? ArbitrumLatestDepositsResponse : +R extends 'general:homepage_zkevm_l2_batches' ? { items: Array } : +R extends 'general:homepage_arbitrum_l2_batches' ? { items: Array } : +R extends 'general:homepage_indexing_status' ? IndexingStatus : +R extends 'general:homepage_zkevm_latest_batch' ? number : +R extends 'general:homepage_zksync_latest_batch' ? number : +R extends 'general:homepage_arbitrum_latest_batch' ? number : +R extends 'general:quick_search' ? Array : +R extends 'general:search' ? SearchResult : +R extends 'general:search_check_redirect' ? SearchRedirectResult : +R extends 'general:config_backend_version' ? BackendVersionConfig : +R extends 'general:config_csv_export' ? CsvExportConfig : +R extends 'general:blob' ? Blob : +R extends 'general:validators_stability' ? ValidatorsStabilityResponse : +R extends 'general:validators_stability_counters' ? ValidatorsStabilityCountersResponse : +R extends 'general:validators_blackfort' ? ValidatorsBlackfortResponse : +R extends 'general:validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse : +R extends 'general:validators_zilliqa' ? ValidatorsZilliqaResponse : +R extends 'general:validator_zilliqa' ? ValidatorZilliqa : +R extends 'general:user_ops' ? UserOpsResponse : +R extends 'general:user_op' ? UserOp : +R extends 'general:user_ops_account' ? UserOpsAccount : +R extends 'general:user_op_interpretation' ? TxInterpretationResponse : +R extends 'general:noves_transaction' ? NovesResponseData : +R extends 'general:noves_address_history' ? NovesAccountHistoryResponse : +R extends 'general:noves_describe_txs' ? NovesDescribeTxsResponse : +R extends 'general:withdrawals' ? WithdrawalsResponse : +R extends 'general:withdrawals_counters' ? WithdrawalsCounters : +R extends 'general:advanced_filter' ? AdvancedFilterResponse : +R extends 'general:advanced_filter_methods' ? AdvancedFilterMethodsResponse : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiMiscPaginationFilters = +R extends 'general:search' ? SearchResultFilters : +R extends 'general:user_ops' ? UserOpsFilters : +R extends 'general:validators_stability' ? ValidatorsStabilityFilters : +R extends 'general:advanced_filter' ? AdvancedFilterParams : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiMiscPaginationSorting = +R extends 'general:validators_stability' ? ValidatorsStabilitySorting : +R extends 'general:validators_blackfort' ? ValidatorsBlackfortSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/rollup.ts b/explorer/frontend/lib/api/services/general/rollup.ts new file mode 100644 index 000000000..480abd15a --- /dev/null +++ b/explorer/frontend/lib/api/services/general/rollup.ts @@ -0,0 +1,403 @@ +import type { ApiResource } from '../../types'; +import type { + AddressMudTables, + AddressMudTablesFilter, + AddressMudRecords, + AddressMudRecordsFilter, + AddressMudRecordsSorting, + AddressMudRecord, +} from 'types/api/address'; +import type { + ArbitrumL2MessagesResponse, + ArbitrumL2TxnBatch, + ArbitrumL2TxnBatchesResponse, + ArbitrumL2BatchTxs, + ArbitrumL2BatchBlocks, + ArbitrumL2TxnWithdrawalsResponse, + ArbitrumL2MessageClaimResponse, +} from 'types/api/arbitrumL2'; +import type { + SmartContractMudSystemsResponse, + SmartContractMudSystemInfo, +} from 'types/api/contract'; +import type { InteropMessageListResponse } from 'types/api/interop'; +import type { MudWorldsResponse } from 'types/api/mudWorlds'; +import type { + OptimisticL2DepositsResponse, + OptimisticL2OutputRootsResponse, + OptimisticL2TxnBatchesResponse, + OptimisticL2WithdrawalsResponse, + OptimisticL2DisputeGamesResponse, + OptimismL2TxnBatch, + OptimismL2BatchTxs, + OptimismL2BatchBlocks, +} from 'types/api/optimisticL2'; +import type { + ScrollL2BatchesResponse, + ScrollL2TxnBatch, + ScrollL2TxnBatchTxs, + ScrollL2TxnBatchBlocks, + ScrollL2MessagesResponse, +} from 'types/api/scrollL2'; +import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; +import type { + ZkEvmL2DepositsResponse, + ZkEvmL2TxnBatch, + ZkEvmL2TxnBatchesResponse, + ZkEvmL2TxnBatchTxs, + ZkEvmL2WithdrawalsResponse, +} from 'types/api/zkEvmL2'; +import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2'; + +export const GENERAL_API_ROLLUP_RESOURCES = { + // OPTIMISTIC + optimistic_l2_deposits: { + path: '/api/v2/optimism/deposits', + filterFields: [], + paginated: true, + }, + optimistic_l2_deposits_count: { + path: '/api/v2/optimism/deposits/count', + }, + optimistic_l2_withdrawals: { + path: '/api/v2/optimism/withdrawals', + filterFields: [], + paginated: true, + }, + optimistic_l2_withdrawals_count: { + path: '/api/v2/optimism/withdrawals/count', + }, + optimistic_l2_output_roots: { + path: '/api/v2/optimism/output-roots', + filterFields: [], + paginated: true, + }, + optimistic_l2_output_roots_count: { + path: '/api/v2/optimism/output-roots/count', + }, + optimistic_l2_txn_batches: { + path: '/api/v2/optimism/batches', + filterFields: [], + paginated: true, + }, + optimistic_l2_txn_batches_count: { + path: '/api/v2/optimism/batches/count', + }, + optimistic_l2_txn_batch: { + path: '/api/v2/optimism/batches/:number', + pathParams: [ 'number' as const ], + }, + optimistic_l2_txn_batch_celestia: { + path: '/api/v2/optimism/batches/da/celestia/:height/:commitment', + pathParams: [ 'height' as const, 'commitment' as const ], + }, + optimistic_l2_txn_batch_txs: { + path: '/api/v2/transactions/optimism-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + paginated: true, + }, + optimistic_l2_txn_batch_blocks: { + path: '/api/v2/blocks/optimism-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + paginated: true, + }, + optimistic_l2_dispute_games: { + path: '/api/v2/optimism/games', + filterFields: [], + paginated: true, + }, + optimistic_l2_dispute_games_count: { + path: '/api/v2/optimism/games/count', + }, + + // OPTIMISTIC INTEROP + optimistic_l2_interop_messages: { + path: '/api/v2/optimism/interop/messages', + filterFields: [], + paginated: true, + }, + optimistic_l2_interop_messages_count: { + path: '/api/v2/optimism/interop/messages/count', + }, + + // MUD + mud_worlds: { + path: '/api/v2/mud/worlds', + filterFields: [], + paginated: true, + }, + mud_tables: { + path: '/api/v2/mud/worlds/:hash/tables', + pathParams: [ 'hash' as const ], + filterFields: [ 'q' as const ], + paginated: true, + }, + mud_tables_count: { + path: '/api/v2/mud/worlds/:hash/tables/count', + pathParams: [ 'hash' as const ], + }, + 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 ], + paginated: true, + }, + 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 ], + }, + mud_systems: { + path: '/api/v2/mud/worlds/:hash/systems', + pathParams: [ 'hash' as const ], + }, + mud_system_info: { + path: '/api/v2/mud/worlds/:hash/systems/:system_address', + pathParams: [ 'hash' as const, 'system_address' as const ], + }, + + // ARBITRUM + arbitrum_l2_messages: { + path: '/api/v2/arbitrum/messages/:direction', + pathParams: [ 'direction' as const ], + filterFields: [], + paginated: true, + }, + 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: [], + paginated: true, + }, + 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_celestia: { + path: '/api/v2/arbitrum/batches/da/celestia/:height/:commitment', + pathParams: [ 'height' as const, 'commitment' as const ], + }, + arbitrum_l2_txn_batch_txs: { + path: '/api/v2/transactions/arbitrum-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + paginated: true, + }, + arbitrum_l2_txn_batch_blocks: { + path: '/api/v2/blocks/arbitrum-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + paginated: true, + }, + arbitrum_l2_txn_withdrawals: { + path: '/api/v2/arbitrum/messages/withdrawals/:hash', + pathParams: [ 'hash' as const ], + filterFields: [], + }, + arbitrum_l2_message_claim: { + path: '/api/v2/arbitrum/messages/claim/:id', + pathParams: [ 'id' as const ], + filterFields: [], + }, + + // zkSync + zksync_l2_txn_batches: { + path: '/api/v2/zksync/batches', + filterFields: [], + paginated: true, + }, + 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: [], + paginated: true, + }, + + // zkEvm + zkevm_l2_deposits: { + path: '/api/v2/zkevm/deposits', + filterFields: [], + paginated: true, + }, + zkevm_l2_deposits_count: { + path: '/api/v2/zkevm/deposits/count', + }, + zkevm_l2_withdrawals: { + path: '/api/v2/zkevm/withdrawals', + filterFields: [], + paginated: true, + }, + zkevm_l2_withdrawals_count: { + path: '/api/v2/zkevm/withdrawals/count', + }, + zkevm_l2_txn_batches: { + path: '/api/v2/zkevm/batches', + filterFields: [], + paginated: true, + }, + 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: [], + paginated: true, + }, + + // SHIBARIUM + shibarium_deposits: { + path: '/api/v2/shibarium/deposits', + filterFields: [], + paginated: true, + }, + shibarium_deposits_count: { + path: '/api/v2/shibarium/deposits/count', + }, + shibarium_withdrawals: { + path: '/api/v2/shibarium/withdrawals', + filterFields: [], + paginated: true, + }, + shibarium_withdrawals_count: { + path: '/api/v2/shibarium/withdrawals/count', + }, + + // SCROLL + scroll_l2_deposits: { + path: '/api/v2/scroll/deposits', + filterFields: [], + paginated: true, + }, + scroll_l2_deposits_count: { + path: '/api/v2/scroll/deposits/count', + }, + scroll_l2_withdrawals: { + path: '/api/v2/scroll/withdrawals', + filterFields: [], + paginated: true, + }, + scroll_l2_withdrawals_count: { + path: '/api/v2/scroll/withdrawals/count', + }, + scroll_l2_txn_batches: { + path: '/api/v2/scroll/batches', + filterFields: [], + paginated: true, + }, + scroll_l2_txn_batches_count: { + path: '/api/v2/scroll/batches/count', + }, + scroll_l2_txn_batch: { + path: '/api/v2/scroll/batches/:number', + pathParams: [ 'number' as const ], + }, + scroll_l2_txn_batch_txs: { + path: '/api/v2/transactions/scroll-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + paginated: true, + }, + scroll_l2_txn_batch_blocks: { + path: '/api/v2/blocks/scroll-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + paginated: true, + }, +} satisfies Record; + +export type GeneralApiRollupResourceName = `general:${ keyof typeof GENERAL_API_ROLLUP_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiRollupResourcePayload = +R extends 'general:optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse : +R extends 'general:optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse : +R extends 'general:optimistic_l2_deposits' ? OptimisticL2DepositsResponse : +R extends 'general:optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse : +R extends 'general:optimistic_l2_txn_batches_count' ? number : +R extends 'general:optimistic_l2_txn_batch' ? OptimismL2TxnBatch : +R extends 'general:optimistic_l2_txn_batch_celestia' ? OptimismL2TxnBatch : +R extends 'general:optimistic_l2_txn_batch_txs' ? OptimismL2BatchTxs : +R extends 'general:optimistic_l2_txn_batch_blocks' ? OptimismL2BatchBlocks : +R extends 'general:optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse : +R extends 'general:optimistic_l2_output_roots_count' ? number : +R extends 'general:optimistic_l2_withdrawals_count' ? number : +R extends 'general:optimistic_l2_deposits_count' ? number : +R extends 'general:optimistic_l2_dispute_games_count' ? number : +R extends 'general:optimistic_l2_interop_messages' ? InteropMessageListResponse : +R extends 'general:optimistic_l2_interop_messages_count' ? number : +R extends 'general:shibarium_withdrawals' ? ShibariumWithdrawalsResponse : +R extends 'general:shibarium_deposits' ? ShibariumDepositsResponse : +R extends 'general:shibarium_withdrawals_count' ? number : +R extends 'general:shibarium_deposits_count' ? number : +R extends 'general:arbitrum_l2_messages' ? ArbitrumL2MessagesResponse : +R extends 'general:arbitrum_l2_messages_count' ? number : +R extends 'general:arbitrum_l2_txn_batches' ? ArbitrumL2TxnBatchesResponse : +R extends 'general:arbitrum_l2_txn_batches_count' ? number : +R extends 'general:arbitrum_l2_txn_batch' ? ArbitrumL2TxnBatch : +R extends 'general:arbitrum_l2_txn_batch_celestia' ? ArbitrumL2TxnBatch : +R extends 'general:arbitrum_l2_txn_batch_txs' ? ArbitrumL2BatchTxs : +R extends 'general:arbitrum_l2_txn_batch_blocks' ? ArbitrumL2BatchBlocks : +R extends 'general:arbitrum_l2_txn_withdrawals' ? ArbitrumL2TxnWithdrawalsResponse : +R extends 'general:arbitrum_l2_message_claim' ? ArbitrumL2MessageClaimResponse : +R extends 'general:zkevm_l2_deposits' ? ZkEvmL2DepositsResponse : +R extends 'general:zkevm_l2_deposits_count' ? number : +R extends 'general:zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse : +R extends 'general:zkevm_l2_withdrawals_count' ? number : +R extends 'general:zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse : +R extends 'general:zkevm_l2_txn_batches_count' ? number : +R extends 'general:zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : +R extends 'general:zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : +R extends 'general:zksync_l2_txn_batches' ? ZkSyncBatchesResponse : +R extends 'general:zksync_l2_txn_batches_count' ? number : +R extends 'general:zksync_l2_txn_batch' ? ZkSyncBatch : +R extends 'general:zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs : +R extends 'general:scroll_l2_txn_batch_txs' ? ScrollL2TxnBatchTxs : +R extends 'general:scroll_l2_txn_batch_blocks' ? ScrollL2TxnBatchBlocks : +R extends 'general:scroll_l2_txn_batches' ? ScrollL2BatchesResponse : +R extends 'general:scroll_l2_txn_batches_count' ? number : +R extends 'general:scroll_l2_txn_batch' ? ScrollL2TxnBatch : +R extends 'general:scroll_l2_deposits' ? ScrollL2MessagesResponse : +R extends 'general:scroll_l2_deposits_count' ? number : +R extends 'general:scroll_l2_withdrawals' ? ScrollL2MessagesResponse : +R extends 'general:scroll_l2_withdrawals_count' ? number : +R extends 'general:mud_worlds' ? MudWorldsResponse : +R extends 'general:mud_tables' ? AddressMudTables : +R extends 'general:mud_tables_count' ? number : +R extends 'general:mud_records' ? AddressMudRecords : +R extends 'general:mud_record' ? AddressMudRecord : +R extends 'general:mud_systems' ? SmartContractMudSystemsResponse : +R extends 'general:mud_system_info' ? SmartContractMudSystemInfo : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiRollupPaginationFilters = +R extends 'general:mud_tables' ? AddressMudTablesFilter : +R extends 'general:mud_records' ? AddressMudRecordsFilter : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiRollupPaginationSorting = +R extends 'general:mud_records' ? AddressMudRecordsSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/token.ts b/explorer/frontend/lib/api/services/general/token.ts new file mode 100644 index 000000000..9c8f62b37 --- /dev/null +++ b/explorer/frontend/lib/api/services/general/token.ts @@ -0,0 +1,122 @@ +import type { ApiResource } from '../../types'; +import type { + TokenCounters, + TokenInfo, + TokenHolders, + TokenInventoryResponse, + TokenInstance, + TokenInstanceTransfersCount, + TokenInventoryFilters, +} from 'types/api/token'; +import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens'; +import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; + +export const GENERAL_API_TOKEN_RESOURCES = { + // TOKEN + token: { + path: '/api/v2/tokens/:hash', + pathParams: [ 'hash' as const ], + }, + 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: [], + paginated: true, + }, + token_transfers: { + path: '/api/v2/tokens/:hash/transfers', + pathParams: [ 'hash' as const ], + filterFields: [], + paginated: true, + }, + token_inventory: { + path: '/api/v2/tokens/:hash/instances', + pathParams: [ 'hash' as const ], + filterFields: [ 'holder_address_hash' as const ], + paginated: true, + }, + tokens: { + path: '/api/v2/tokens', + filterFields: [ 'q' as const, 'type' as const ], + paginated: true, + }, + tokens_bridged: { + path: '/api/v2/tokens/bridged', + filterFields: [ 'q' as const, 'chain_ids' as const ], + paginated: true, + }, + + // 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: [], + paginated: true, + }, + token_instance_holders: { + path: '/api/v2/tokens/:hash/instances/:id/holders', + pathParams: [ 'hash' as const, 'id' as const ], + filterFields: [], + paginated: true, + }, + token_instance_refresh_metadata: { + path: '/api/v2/tokens/:hash/instances/:id/refetch-metadata', + pathParams: [ 'hash' as const, 'id' as const ], + filterFields: [], + }, + + // TOKEN TRANSFERS + token_transfers_all: { + path: '/api/v2/token-transfers', + filterFields: [ 'type' as const ], + paginated: true, + }, +} satisfies Record; + +export type GeneralApiTokenResourceName = `general:${ keyof typeof GENERAL_API_TOKEN_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiTokenResourcePayload = +R extends 'general:token' ? TokenInfo : +R extends 'general:token_counters' ? TokenCounters : +R extends 'general:token_transfers' ? TokenTransferResponse : +R extends 'general:token_holders' ? TokenHolders : +R extends 'general:token_instance' ? TokenInstance : +R extends 'general:token_instance_transfers_count' ? TokenInstanceTransfersCount : +R extends 'general:token_instance_transfers' ? TokenInstanceTransferResponse : +R extends 'general:token_instance_holders' ? TokenHolders : +R extends 'general:token_inventory' ? TokenInventoryResponse : +R extends 'general:tokens' ? TokensResponse : +R extends 'general:tokens_bridged' ? TokensResponse : +R extends 'general:token_transfers_all' ? TokenTransferResponse : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiTokenPaginationFilters = +R extends 'general:token_transfers' ? TokenTransferFilters : +R extends 'general:token_inventory' ? TokenInventoryFilters : +R extends 'general:tokens' ? TokensFilters : +R extends 'general:tokens_bridged' ? TokensBridgedFilters : +R extends 'general:token_transfers_all' ? TokenTransferFilters : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiTokenPaginationSorting = +R extends 'general:tokens' ? TokensSorting : +R extends 'general:tokens_bridged' ? TokensSorting : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/tx.ts b/explorer/frontend/lib/api/services/general/tx.ts new file mode 100644 index 000000000..4cd14dd03 --- /dev/null +++ b/explorer/frontend/lib/api/services/general/tx.ts @@ -0,0 +1,129 @@ +import type { ApiResource } from '../../types'; +import type { TxBlobs } from 'types/api/blobs'; +import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; +import type { LogsResponseTx } from 'types/api/log'; +import type { RawTracesResponse } from 'types/api/rawTrace'; +import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; +import type { + TransactionsResponseValidated, + TransactionsResponsePending, + Transaction, + TransactionsResponseWatchlist, + 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'; + +export const GENERAL_API_TX_RESOURCES = { + txs_stats: { + path: '/api/v2/transactions/stats', + }, + txs_validated: { + path: '/api/v2/transactions', + filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], + paginated: true, + }, + txs_pending: { + path: '/api/v2/transactions', + filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], + paginated: true, + }, + txs_with_blobs: { + path: '/api/v2/transactions', + filterFields: [ 'type' as const ], + paginated: true, + }, + txs_watchlist: { + path: '/api/v2/transactions/watchlist', + filterFields: [ ], + paginated: true, + }, + txs_execution_node: { + path: '/api/v2/transactions/execution-node/:hash', + pathParams: [ 'hash' as const ], + filterFields: [ ], + paginated: true, + }, + 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: [ ], + paginated: true, + }, + tx_logs: { + path: '/api/v2/transactions/:hash/logs', + pathParams: [ 'hash' as const ], + filterFields: [ ], + paginated: true, + }, + tx_token_transfers: { + path: '/api/v2/transactions/:hash/token-transfers', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + paginated: true, + }, + 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: [], + paginated: true, + }, + tx_blobs: { + path: '/api/v2/transactions/:hash/blobs', + pathParams: [ 'hash' as const ], + paginated: true, + }, + tx_interpretation: { + path: '/api/v2/transactions/:hash/summary', + pathParams: [ 'hash' as const ], + }, + tx_external_transactions: { + path: '/api/v2/transactions/:hash/external-transactions', + pathParams: [ 'hash' as const ], + }, + internal_txs: { + path: '/api/v2/internal-transactions', + paginated: true, + }, +} satisfies Record; + +export type GeneralApiTxResourceName = `general:${ keyof typeof GENERAL_API_TX_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiTxResourcePayload = +R extends 'general:txs_stats' ? TransactionsStats : +R extends 'general:txs_validated' ? TransactionsResponseValidated : +R extends 'general:txs_pending' ? TransactionsResponsePending : +R extends 'general:txs_with_blobs' ? TransactionsResponseWithBlobs : +R extends 'general:txs_watchlist' ? TransactionsResponseWatchlist : +R extends 'general:txs_execution_node' ? TransactionsResponseValidated : +R extends 'general:tx_internal_txs' ? InternalTransactionsResponse : +R extends 'general:tx' ? Transaction : +R extends 'general:tx_logs' ? LogsResponseTx : +R extends 'general:tx_token_transfers' ? TokenTransferResponse : +R extends 'general:tx_raw_trace' ? RawTracesResponse : +R extends 'general:tx_state_changes' ? TxStateChanges : +R extends 'general:tx_blobs' ? TxBlobs : +R extends 'general:tx_interpretation' ? TxInterpretationResponse : +R extends 'general:tx_external_transactions' ? Array : +R extends 'general:internal_txs' ? InternalTransactionsResponse : +never; +/* eslint-enable @stylistic/indent */ + +/* eslint-disable @stylistic/indent */ +export type GeneralApiTxPaginationFilters = +R extends 'general:txs_validated' | 'general:txs_pending' ? TTxsFilters : +R extends 'general:txs_with_blobs' ? TTxsWithBlobsFilters : +R extends 'general:tx_token_transfers' ? TokenTransferFilters : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/general/v1.ts b/explorer/frontend/lib/api/services/general/v1.ts new file mode 100644 index 000000000..4f78e96b9 --- /dev/null +++ b/explorer/frontend/lib/api/services/general/v1.ts @@ -0,0 +1,34 @@ +import type { ApiResource } from '../../types'; +import type { BlockCountdownResponse } from 'types/api/block'; + +export const GENERAL_API_V1_RESOURCES = { + 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', + }, + csv_export_epoch_rewards: { + path: '/api/v1/celo-election-rewards-csv', + }, + graphql: { + path: '/api/v1/graphql', + }, + block_countdown: { + path: '/api', + }, +} satisfies Record; + +export type GeneralApiV1ResourceName = `general:${ keyof typeof GENERAL_API_V1_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type GeneralApiV1ResourcePayload = +R extends 'general:block_countdown' ? BlockCountdownResponse : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/metadata.ts b/explorer/frontend/lib/api/services/metadata.ts new file mode 100644 index 000000000..9061793a9 --- /dev/null +++ b/explorer/frontend/lib/api/services/metadata.ts @@ -0,0 +1,23 @@ +import type { ApiResource } from '../types'; +import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; + +export const METADATA_API_RESOURCES = { + info: { + path: '/api/v1/metadata', + }, + tags_search: { + path: '/api/v1/tags:search', + }, + public_tag_types: { + path: '/api/v1/public-tag-types', + }, +} satisfies Record; + +export type MetadataApiResourceName = `metadata:${ keyof typeof METADATA_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type MetadataApiResourcePayload = +R extends 'metadata:info' ? AddressMetadataInfo : +R extends 'metadata:public_tag_types' ? PublicTagTypesResponse : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/rewards.ts b/explorer/frontend/lib/api/services/rewards.ts new file mode 100644 index 000000000..ae26c2948 --- /dev/null +++ b/explorer/frontend/lib/api/services/rewards.ts @@ -0,0 +1,83 @@ +import type { ApiResource } from '../types'; +import type * as rewards from '@blockscout/points-types'; + +export const REWARDS_API_RESOURCES = { + config: { + path: '/api/v1/config', + }, + check_ref_code: { + path: '/api/v1/auth/code/:code', + pathParams: [ 'code' as const ], + }, + nonce: { + path: '/api/v1/auth/nonce', + }, + check_user: { + path: '/api/v1/auth/user/:address', + pathParams: [ 'address' as const ], + }, + login: { + path: '/api/v1/auth/login', + }, + logout: { + path: '/api/v1/auth/logout', + }, + user_balances: { + path: '/api/v1/user/balances', + }, + user_daily_check: { + path: '/api/v1/user/daily/check', + }, + user_daily_claim: { + path: '/api/v1/user/daily/claim', + }, + user_referrals: { + path: '/api/v1/user/referrals', + }, + user_check_activity_pass: { + path: '/api/v1/activity/check-pass', + filterFields: [ 'address' as const ], + }, + user_activity: { + path: '/api/v1/user/activity/rewards', + }, + user_activity_track_tx: { + path: '/api/v1/user/activity/track/transaction', + }, + user_activity_track_tx_confirm: { + path: '/api/v1/activity/track/transaction/confirm', + }, + user_activity_track_contract: { + path: '/api/v1/user/activity/track/contract', + }, + user_activity_track_contract_confirm: { + path: '/api/v1/activity/track/contract/confirm', + }, + user_activity_track_usage: { + path: '/api/v1/user/activity/track/usage', + }, + instances: { + path: '/api/v1/instances', + }, +} satisfies Record; + +export type RewardsApiResourceName = `rewards:${ keyof typeof REWARDS_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type RewardsApiResourcePayload = +R extends 'rewards:config' ? rewards.GetConfigResponse : +R extends 'rewards:check_ref_code' ? rewards.AuthCodeResponse : +R extends 'rewards:nonce' ? rewards.AuthNonceResponse : +R extends 'rewards:check_user' ? rewards.AuthUserResponse : +R extends 'rewards:login' ? rewards.AuthLoginResponse : +R extends 'rewards:user_balances' ? rewards.GetUserBalancesResponse : +R extends 'rewards:user_daily_check' ? rewards.DailyRewardCheckResponse : +R extends 'rewards:user_daily_claim' ? rewards.DailyRewardClaimResponse : +R extends 'rewards:user_referrals' ? rewards.GetReferralDataResponse : +R extends 'rewards:user_check_activity_pass' ? rewards.CheckActivityPassResponse : +R extends 'rewards:user_activity' ? rewards.GetActivityRewardsResponse : +R extends 'rewards:user_activity_track_tx' ? rewards.PreSubmitTransactionResponse : +R extends 'rewards:user_activity_track_contract' ? rewards.PreVerifyContractResponse : +R extends 'rewards:instances' ? rewards.GetInstancesResponse : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/stats.ts b/explorer/frontend/lib/api/services/stats.ts new file mode 100644 index 000000000..f666fec83 --- /dev/null +++ b/explorer/frontend/lib/api/services/stats.ts @@ -0,0 +1,37 @@ +import type { ApiResource } from '../types'; +import type * as stats from '@blockscout/stats-types'; + +export const STATS_API_RESOURCES = { + counters: { + path: '/api/v1/counters', + }, + lines: { + path: '/api/v1/lines', + }, + line: { + path: '/api/v1/lines/:id', + pathParams: [ 'id' as const ], + }, + pages_main: { + path: '/api/v1/pages/main', + }, + pages_transactions: { + path: '/api/v1/pages/transactions', + }, + pages_contracts: { + path: '/api/v1/pages/contracts', + }, +} satisfies Record; + +export type StatsApiResourceName = `stats:${ keyof typeof STATS_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type StatsApiResourcePayload = +R extends 'stats:counters' ? stats.Counters : +R extends 'stats:lines' ? stats.LineCharts : +R extends 'stats:line' ? stats.LineChart : +R extends 'stats:pages_main' ? stats.MainPageStats : +R extends 'stats:pages_transactions' ? stats.TransactionsPageStats : +R extends 'stats:pages_contracts' ? stats.ContractsPageStats : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/services/utils.ts b/explorer/frontend/lib/api/services/utils.ts new file mode 100644 index 000000000..c4cf663c5 --- /dev/null +++ b/explorer/frontend/lib/api/services/utils.ts @@ -0,0 +1,3 @@ +import type { ApiResource } from '../types'; + +export type IsPaginated = R extends { paginated: true } ? true : false; diff --git a/explorer/frontend/lib/api/services/visualize.ts b/explorer/frontend/lib/api/services/visualize.ts new file mode 100644 index 000000000..8c9a6b3fe --- /dev/null +++ b/explorer/frontend/lib/api/services/visualize.ts @@ -0,0 +1,16 @@ +import type { ApiResource } from '../types'; +import type * as visualizer from '@blockscout/visualizer-types'; + +export const VISUALIZE_API_RESOURCES = { + solidity_contract: { + path: '/api/v1/solidity\\:visualize-contracts', + }, +} satisfies Record; + +export type VisualizeApiResourceName = `visualize:${ keyof typeof VISUALIZE_API_RESOURCES }`; + +/* eslint-disable @stylistic/indent */ +export type VisualizeApiResourcePayload = +R extends 'visualize:solidity_contract' ? visualizer.VisualizeResponse : +never; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/api/types.ts b/explorer/frontend/lib/api/types.ts new file mode 100644 index 000000000..1b8d0b6f3 --- /dev/null +++ b/explorer/frontend/lib/api/types.ts @@ -0,0 +1,18 @@ +export interface ApiResource { + path: string; + pathParams?: Array; + filterFields?: Array; + paginated?: boolean; + headers?: RequestInit['headers']; +} + +export type ApiName = + | 'admin' + | 'bens' + | 'channels' + | 'contractInfo' + | 'general' + | 'metadata' + | 'rewards' + | 'stats' + | 'visualize'; diff --git a/explorer/frontend/lib/api/useApiFetch.tsx b/explorer/frontend/lib/api/useApiFetch.tsx new file mode 100644 index 000000000..1c8fa6ebf --- /dev/null +++ b/explorer/frontend/lib/api/useApiFetch.tsx @@ -0,0 +1,65 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { omit, pickBy } from 'es-toolkit'; +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 getResourceParams from './getResourceParams'; +import type { ResourceName, ResourcePathParams } from './resources'; + +export interface Params { + pathParams?: ResourcePathParams; + queryParams?: Record | number | boolean | undefined | null>; + fetchParams?: Pick; + logError?: boolean; +} + +export default function useApiFetch() { + const fetch = useFetch(); + const queryClient = useQueryClient(); + const { token: csrfToken } = queryClient.getQueryData(getResourceKey('general:csrf')) || {}; + + return React.useCallback(( + resourceName: R, + { pathParams, queryParams, fetchParams, logError }: Params = {}, + ) => { + const apiToken = cookies.get(cookies.NAMES.API_TOKEN); + + const { api, apiName, resource } = getResourceParams(resourceName); + const url = buildUrl(resourceName, pathParams, queryParams); + const withBody = isBodyAllowed(fetchParams?.method); + const headers = pickBy({ + 'x-endpoint': api.endpoint && apiName !== 'general' && isNeedProxy() ? api.endpoint : undefined, + Authorization: [ 'admin', 'contractInfo' ].includes(apiName) ? 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, + ...(fetchParams ? omit(fetchParams, [ 'headers' ]) : {}), + }, + { + resource: resource.path, + logError, + }, + ); + }, [ fetch, csrfToken ]); +} diff --git a/explorer/frontend/lib/api/useApiInfiniteQuery.tsx b/explorer/frontend/lib/api/useApiInfiniteQuery.tsx new file mode 100644 index 000000000..9f44b2714 --- /dev/null +++ b/explorer/frontend/lib/api/useApiInfiniteQuery.tsx @@ -0,0 +1,41 @@ +import type { InfiniteData, QueryKey, UseInfiniteQueryResult, UseInfiniteQueryOptions } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +import type { PaginatedResourceName, ResourceError, ResourcePayload } from './resources'; +import useApiFetch from './useApiFetch'; +import type { Params as ApiFetchParams } from './useApiFetch'; +import { getResourceKey } from './useApiQuery'; + +type TQueryData = ResourcePayload; +type TError = ResourceError; +type TPageParam = ApiFetchParams['queryParams'] | null; + +export interface Params { + resourceName: R; + // eslint-disable-next-line max-len + queryOptions?: Omit, TError, InfiniteData>, TQueryData, QueryKey, TPageParam>, 'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'>; + pathParams?: ApiFetchParams['pathParams']; +} + +type ReturnType = UseInfiniteQueryResult>, ResourceError>; + +export default function useApiInfiniteQuery({ + resourceName, + queryOptions, + pathParams, +}: Params): ReturnType { + const apiFetch = useApiFetch(); + + return useInfiniteQuery, TError, InfiniteData>, QueryKey, TPageParam>({ + queryKey: getResourceKey(resourceName, { pathParams }), + queryFn: (context) => { + const queryParams = 'pageParam' in context ? (context.pageParam || undefined) : undefined; + return apiFetch(resourceName, { pathParams, queryParams }) as Promise>; + }, + initialPageParam: null, + getNextPageParam: (lastPage) => { + return lastPage.next_page_params as TPageParam; + }, + ...queryOptions, + }); +} diff --git a/explorer/frontend/lib/api/useApiQuery.tsx b/explorer/frontend/lib/api/useApiQuery.tsx new file mode 100644 index 000000000..6cd58c024 --- /dev/null +++ b/explorer/frontend/lib/api/useApiQuery.tsx @@ -0,0 +1,41 @@ +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +import type { Params as FetchParams } from 'lib/hooks/useFetch'; + +import type { ResourceError, ResourceName, ResourcePathParams, ResourcePayload } from './resources'; +import useApiFetch from './useApiFetch'; + +export interface Params> { + pathParams?: ResourcePathParams; + queryParams?: Record | number | boolean | undefined>; + fetchParams?: Pick; + queryOptions?: Partial, ResourceError, D>, 'queryFn'>>; + logError?: boolean; +} + +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, logError }: Params = {}, +) { + const apiFetch = useApiFetch(); + + return useQuery, ResourceError, D>({ + queryKey: queryOptions?.queryKey || getResourceKey(resource, { pathParams, queryParams }), + queryFn: async({ signal }) => { + // 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, logError, fetchParams: { ...fetchParams, signal } }) as Promise>; + }, + ...queryOptions, + }); +} diff --git a/explorer/frontend/lib/api/useQueryClientConfig.tsx b/explorer/frontend/lib/api/useQueryClientConfig.tsx new file mode 100644 index 000000000..4b7c16cdd --- /dev/null +++ b/explorer/frontend/lib/api/useQueryClientConfig.tsx @@ -0,0 +1,52 @@ +import { QueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; +import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; + +import type { ResourceName } from './resources'; + +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, query) => { + const status = getErrorObjStatusCode(error); + + // we don't catch error only for "Too many requests" response + if (status !== 429) { + return false; + } + + const EXTERNAL_API_RESOURCES: Array = [ + 'general:contract_solidity_scan_report', + 'general:address_xstar_score', + 'general:noves_transaction', + 'general:noves_address_history', + 'general:noves_describe_txs', + // these resources are not proxied by the backend + 'safe_transaction_api', + 'gas_hawk_saving_potential', + ]; + const isExternalApiResource = EXTERNAL_API_RESOURCES.some((resource) => query.queryKey[0] === resource); + + return !isExternalApiResource; + }, + }, + }, + })); + + return queryClient; +} diff --git a/explorer/frontend/lib/bigint/compareBns.ts b/explorer/frontend/lib/bigint/compareBns.ts new file mode 100644 index 000000000..34a86239c --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/bigint/sumBnReducer.ts b/explorer/frontend/lib/bigint/sumBnReducer.ts new file mode 100644 index 000000000..6057853a9 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/blob/guessDataType.ts b/explorer/frontend/lib/blob/guessDataType.ts new file mode 100644 index 000000000..ea149236a --- /dev/null +++ b/explorer/frontend/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 = hexToBytes(data); + const filteredBytes = removeNonSignificantZeroBytes(bytes); + + return filetype(filteredBytes)[0]; +} diff --git a/explorer/frontend/lib/blob/index.ts b/explorer/frontend/lib/blob/index.ts new file mode 100644 index 000000000..ab178e823 --- /dev/null +++ b/explorer/frontend/lib/blob/index.ts @@ -0,0 +1 @@ +export { default as guessDataType } from './guessDataType'; diff --git a/explorer/frontend/lib/blob/removeNonSignificantZeroBytes.ts b/explorer/frontend/lib/blob/removeNonSignificantZeroBytes.ts new file mode 100644 index 000000000..9b2528747 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/block/getBlockReward.ts b/explorer/frontend/lib/block/getBlockReward.ts new file mode 100644 index 000000000..dfe255823 --- /dev/null +++ b/explorer/frontend/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.transaction_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/explorer/frontend/lib/block/getBlockTotalReward.ts b/explorer/frontend/lib/block/getBlockTotalReward.ts new file mode 100644 index 000000000..5fcf34f4f --- /dev/null +++ b/explorer/frontend/lib/block/getBlockTotalReward.ts @@ -0,0 +1,13 @@ +import BigNumber from 'bignumber.js'; + +import type { Block } from 'types/api/block'; + +import { WEI, ZERO } from 'toolkit/utils/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/explorer/frontend/lib/bytesToBase64.ts b/explorer/frontend/lib/bytesToBase64.ts new file mode 100644 index 000000000..60b23ad43 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/bytesToHex.ts b/explorer/frontend/lib/bytesToHex.ts new file mode 100644 index 000000000..be2a1c275 --- /dev/null +++ b/explorer/frontend/lib/bytesToHex.ts @@ -0,0 +1,8 @@ +export default function bytesToBase64(bytes: Uint8Array) { + let result = ''; + for (const byte of bytes) { + result += Number(byte).toString(16).padStart(2, '0'); + } + + return `0x${ result }`; +} diff --git a/explorer/frontend/lib/capitalizeFirstLetter.ts b/explorer/frontend/lib/capitalizeFirstLetter.ts new file mode 100644 index 000000000..ae054d942 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/contexts/addressHighlight.tsx b/explorer/frontend/lib/contexts/addressHighlight.tsx new file mode 100644 index 000000000..085e676ec --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/contexts/app.tsx b/explorer/frontend/lib/contexts/app.tsx new file mode 100644 index 000000000..63c18f5f4 --- /dev/null +++ b/explorer/frontend/lib/contexts/app.tsx @@ -0,0 +1,30 @@ +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, + uuid: '', +}); + +export function AppContextProvider({ children, pageProps }: Props) { + return ( + + { children } + + ); +} + +export function useAppContext() { + return useContext>(AppContext); +} diff --git a/explorer/frontend/lib/contexts/marketplace.tsx b/explorer/frontend/lib/contexts/marketplace.tsx new file mode 100644 index 000000000..61cde1262 --- /dev/null +++ b/explorer/frontend/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; +}; + +export 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/explorer/frontend/lib/contexts/rewards.tsx b/explorer/frontend/lib/contexts/rewards.tsx new file mode 100644 index 000000000..a42cbec38 --- /dev/null +++ b/explorer/frontend/lib/contexts/rewards.tsx @@ -0,0 +1,298 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; +import { useToggle } from '@uidotdev/usehooks'; +import { useRouter } from 'next/router'; +import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react'; +import { useSignMessage, useSwitchChain } from 'wagmi'; + +import type * as rewards from '@blockscout/points-types'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import useApiFetch from 'lib/api/useApiFetch'; +import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; +import * as cookies from 'lib/cookies'; +import decodeJWT from 'lib/decodeJWT'; +import getErrorMessage from 'lib/errors/getErrorMessage'; +import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import removeQueryParam from 'lib/router/removeQueryParam'; +import useAccount from 'lib/web3/useAccount'; +import { toaster } from 'toolkit/chakra/toaster'; +import { YEAR } from 'toolkit/utils/consts'; +import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; + +const feature = config.features.rewards; + +type ContextQueryResult = + Pick>, 'data' | 'isLoading' | 'refetch' | 'isPending' | 'isFetching' | 'isError'>; + +type TRewardsContext = { + balancesQuery: ContextQueryResult; + dailyRewardQuery: ContextQueryResult; + referralsQuery: ContextQueryResult; + rewardsConfigQuery: ContextQueryResult; + checkUserQuery: ContextQueryResult; + apiToken: string | undefined; + isInitialized: boolean; + isLoginModalOpen: boolean; + openLoginModal: () => void; + closeLoginModal: () => void; + saveApiToken: (token: string | undefined) => void; + login: (refCode: string) => Promise<{ isNewUser: boolean; reward?: string; invalidRefCodeError?: boolean }>; + claim: () => Promise; +}; + +const defaultQueryResult = { + data: undefined, + isLoading: false, + isPending: false, + isFetching: false, + isError: false, + refetch: () => Promise.resolve({} as never), +}; + +const initialState = { + balancesQuery: defaultQueryResult, + dailyRewardQuery: defaultQueryResult, + referralsQuery: defaultQueryResult, + rewardsConfigQuery: defaultQueryResult, + checkUserQuery: defaultQueryResult, + apiToken: undefined, + isInitialized: false, + isLoginModalOpen: false, + openLoginModal: () => {}, + closeLoginModal: () => {}, + saveApiToken: () => {}, + login: async() => ({ isNewUser: false }), + claim: async() => {}, +}; + +const RewardsContext = createContext(initialState); + +// Message to sign for the rewards program +function getMessageToSign(address: string, nonce: string, isLogin?: boolean, refCode?: string) { + const signInText = 'Sign-In for the Blockscout Merits program.'; + const signUpText = 'Sign-Up for the Blockscout Merits program. I accept Terms of Service: https://merits.blockscout.com/terms. I love capybaras.'; + const referralText = refCode ? ` Referral code: ${ refCode }` : ''; + const body = isLogin ? signInText : signUpText + referralText; + + const urlObj = window.location.hostname === 'localhost' && config.apis.rewards ? + new URL(config.apis.rewards.endpoint) : + window.location; + + return [ + `${ urlObj.hostname } wants you to sign in with your Ethereum account:`, + address, + '', + body, + '', + `URI: ${ urlObj.origin }`, + 'Version: 1', + `Chain ID: ${ config.chain.id }`, + `Nonce: ${ nonce }`, + `Issued At: ${ new Date().toISOString() }`, + `Expiration Time: ${ new Date(Date.now() + YEAR).toISOString() }`, + ].join('\n'); +} + +// Get the registered address from the JWT token +function getRegisteredAddress(token: string) { + const decodedToken = decodeJWT(token); + return decodedToken?.payload.sub; +} + +type Props = { + children: React.ReactNode; +}; + +export function RewardsContextProvider({ children }: Props) { + const router = useRouter(); + const queryClient = useQueryClient(); + const apiFetch = useApiFetch(); + const { address } = useAccount(); + const { signMessageAsync } = useSignMessage(); + const { switchChainAsync } = useSwitchChain(); + const profileQuery = useProfileQuery(); + + const [ isLoginModalOpen, setIsLoginModalOpen ] = useToggle(false); + const [ isInitialized, setIsInitialized ] = useToggle(false); + const [ apiToken, setApiToken ] = React.useState(); + + // Initialize state with the API token from cookies + useEffect(() => { + if (!profileQuery.isLoading) { + const token = cookies.get(cookies.NAMES.REWARDS_API_TOKEN); + const registeredAddress = getRegisteredAddress(token || ''); + if (registeredAddress === profileQuery.data?.address_hash) { + setApiToken(token); + } + setIsInitialized(true); + } + }, [ setIsInitialized, profileQuery ]); + + // Save the API token to cookies and state + const saveApiToken = useCallback((token: string | undefined) => { + if (token) { + cookies.set(cookies.NAMES.REWARDS_API_TOKEN, token, { expires: 365 }); + } else { + cookies.remove(cookies.NAMES.REWARDS_API_TOKEN); + } + setApiToken(token); + }, []); + + const [ queryOptions, fetchParams ] = useMemo(() => [ + { enabled: Boolean(apiToken) && feature.isEnabled }, + { headers: { Authorization: `Bearer ${ apiToken }` } }, + ], [ apiToken ]); + + const balancesQuery = useApiQuery('rewards:user_balances', { queryOptions, fetchParams }); + const dailyRewardQuery = useApiQuery('rewards:user_daily_check', { queryOptions, fetchParams }); + const referralsQuery = useApiQuery('rewards:user_referrals', { queryOptions, fetchParams }); + const rewardsConfigQuery = useApiQuery('rewards:config', { queryOptions: { enabled: feature.isEnabled } }); + const checkUserQuery = useApiQuery('rewards:check_user', { queryOptions: { enabled: feature.isEnabled }, pathParams: { address } }); + + // Reset queries when the API token is removed + useEffect(() => { + if (isInitialized && !apiToken) { + queryClient.resetQueries({ queryKey: getResourceKey('rewards:user_balances'), exact: true }); + queryClient.resetQueries({ queryKey: getResourceKey('rewards:user_daily_check'), exact: true }); + queryClient.resetQueries({ queryKey: getResourceKey('rewards:user_referrals'), exact: true }); + } + }, [ isInitialized, apiToken, queryClient ]); + + // Handle 401 error + useEffect(() => { + if (apiToken && balancesQuery.error?.status === 401) { + saveApiToken(undefined); + } + }, [ balancesQuery.error, apiToken, saveApiToken ]); + + // Check if the profile address is the same as the registered address + useEffect(() => { + const registeredAddress = getRegisteredAddress(apiToken || ''); + if (registeredAddress && !profileQuery.isLoading && profileQuery.data?.address_hash !== registeredAddress) { + setApiToken(undefined); + } + }, [ apiToken, profileQuery, setApiToken ]); + + // Handle referral code in the URL + useEffect(() => { + const refCode = getQueryParamString(router.query.ref); + if (refCode && isInitialized) { + cookies.set(cookies.NAMES.REWARDS_REFERRAL_CODE, refCode); + removeQueryParam(router, 'ref'); + if (!apiToken) { + setIsLoginModalOpen(true); + } + } + }, [ router, apiToken, isInitialized, setIsLoginModalOpen ]); + + const errorToast = useCallback((error: unknown) => { + const apiError = getErrorObjPayload<{ message: string }>(error); + toaster.error({ + title: 'Error', + description: apiError?.message || getErrorMessage(error) || 'Something went wrong. Try again later.', + }); + }, [ ]); + + // Login to the rewards program + const login = useCallback(async(refCode: string) => { + try { + if (!address) { + throw new Error(); + } + const [ nonceResponse, checkCodeResponse ] = await Promise.all([ + apiFetch('rewards:nonce') as Promise, + refCode ? + apiFetch('rewards:check_ref_code', { pathParams: { code: refCode } }) as Promise : + Promise.resolve({ valid: true, reward: undefined }), + ]); + if (!checkCodeResponse.valid) { + return { + invalidRefCodeError: true, + isNewUser: false, + }; + } + await switchChainAsync({ chainId: Number(config.chain.id) }); + const message = getMessageToSign(address, nonceResponse.nonce, checkUserQuery.data?.exists, refCode); + const signature = await signMessageAsync({ message }); + const loginResponse = await apiFetch('rewards:login', { + fetchParams: { + method: 'POST', + body: { + nonce: nonceResponse.nonce, + message, + signature, + }, + }, + }) as rewards.AuthLoginResponse; + saveApiToken(loginResponse.token); + return { + isNewUser: loginResponse.created, + reward: checkCodeResponse.reward, + }; + } catch (_error) { + errorToast(_error); + throw _error; + } + }, [ address, apiFetch, checkUserQuery.data?.exists, switchChainAsync, signMessageAsync, saveApiToken, errorToast ]); + + // Claim daily reward + const claim = useCallback(async() => { + try { + await apiFetch('rewards:user_daily_claim', { + fetchParams: { + method: 'POST', + ...fetchParams, + }, + }) as rewards.DailyRewardClaimResponse; + } catch (_error) { + errorToast(_error); + throw _error; + } + }, [ apiFetch, errorToast, fetchParams ]); + + const openLoginModal = React.useCallback(() => { + setIsLoginModalOpen(true); + }, [ setIsLoginModalOpen ]); + + const closeLoginModal = React.useCallback(() => { + setIsLoginModalOpen(false); + }, [ setIsLoginModalOpen ]); + + const value = useMemo(() => { + if (!feature.isEnabled) { + return initialState; + } + return { + balancesQuery, + dailyRewardQuery, + referralsQuery, + rewardsConfigQuery, + checkUserQuery, + apiToken, + saveApiToken, + isInitialized, + isLoginModalOpen, + openLoginModal, + closeLoginModal, + login, + claim, + }; + }, [ + balancesQuery, dailyRewardQuery, checkUserQuery, + apiToken, login, claim, referralsQuery, rewardsConfigQuery, isInitialized, saveApiToken, + isLoginModalOpen, openLoginModal, closeLoginModal, + ]); + + return ( + + { children } + + ); +} + +export function useRewardsContext() { + return useContext(RewardsContext); +} diff --git a/explorer/frontend/lib/contexts/scrollDirection.tsx b/explorer/frontend/lib/contexts/scrollDirection.tsx new file mode 100644 index 000000000..f99a72c25 --- /dev/null +++ b/explorer/frontend/lib/contexts/scrollDirection.tsx @@ -0,0 +1,57 @@ +import { throttle, clamp } from 'es-toolkit'; +import React from 'react'; + +const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null); +import { isBrowser } from 'toolkit/utils/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/explorer/frontend/lib/contexts/settings.tsx b/explorer/frontend/lib/contexts/settings.tsx new file mode 100644 index 000000000..054248acf --- /dev/null +++ b/explorer/frontend/lib/contexts/settings.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { ADDRESS_FORMATS, type AddressFormat } from 'types/views/address'; + +import * as cookies from 'lib/cookies'; + +import { useAppContext } from './app'; + +interface SettingsProviderProps { + children: React.ReactNode; +} + +interface TSettingsContext { + addressFormat: AddressFormat; + toggleAddressFormat: () => void; +} + +export const SettingsContext = React.createContext(null); + +export function SettingsContextProvider({ children }: SettingsProviderProps) { + const { cookies: appCookies } = useAppContext(); + const initialAddressFormat = cookies.get(cookies.NAMES.ADDRESS_FORMAT, appCookies); + + const [ addressFormat, setAddressFormat ] = React.useState( + initialAddressFormat && ADDRESS_FORMATS.includes(initialAddressFormat as AddressFormat) ? initialAddressFormat as AddressFormat : 'base16', + ); + + const toggleAddressFormat = React.useCallback(() => { + setAddressFormat(prev => { + const nextValue = prev === 'base16' ? 'bech32' : 'base16'; + cookies.set(cookies.NAMES.ADDRESS_FORMAT, nextValue); + return nextValue; + }); + }, []); + + const value = React.useMemo(() => { + return { + addressFormat, + toggleAddressFormat, + }; + }, [ addressFormat, toggleAddressFormat ]); + + return ( + + { children } + + ); +} + +export function useSettingsContext(disabled?: boolean) { + const context = React.useContext(SettingsContext); + if (context === undefined || disabled) { + return null; + } + return context; +} diff --git a/explorer/frontend/lib/contracts/formatLanguageName.tsx b/explorer/frontend/lib/contracts/formatLanguageName.tsx new file mode 100644 index 000000000..e1e9c6d23 --- /dev/null +++ b/explorer/frontend/lib/contracts/formatLanguageName.tsx @@ -0,0 +1,3 @@ +export default function formatLanguageName(language: string) { + return language.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); +} diff --git a/explorer/frontend/lib/contracts/licenses.ts b/explorer/frontend/lib/contracts/licenses.ts new file mode 100644 index 000000000..123149e29 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/cookies.ts b/explorer/frontend/lib/cookies.ts new file mode 100644 index 000000000..ebb62aaf0 --- /dev/null +++ b/explorer/frontend/lib/cookies.ts @@ -0,0 +1,45 @@ +import Cookies from 'js-cookie'; + +import { isBrowser } from 'toolkit/utils/isBrowser'; + +export enum NAMES { + NAV_BAR_COLLAPSED = 'nav_bar_collapsed', + API_TOKEN = '_explorer_key', + REWARDS_API_TOKEN = 'rewards_api_token', + REWARDS_REFERRAL_CODE = 'rewards_ref_code', + TXS_SORT = 'txs_sort', + COLOR_MODE = 'chakra-ui-color-mode', + COLOR_MODE_HEX = 'chakra-ui-color-mode-hex', + ADDRESS_IDENTICON_TYPE = 'address_identicon_type', + ADDRESS_FORMAT = 'address_format', + INDEXING_ALERT = 'indexing_alert', + ADBLOCK_DETECTED = 'adblock_detected', + MIXPANEL_DEBUG = '_mixpanel_debug', + ADDRESS_NFT_DISPLAY_TYPE = 'address_nft_display_type', + UUID = 'uuid', + SHOW_SCAM_TOKENS = 'show_scam_tokens', +} + +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: NAMES, value: string, attributes: Cookies.CookieAttributes = {}) { + attributes.path = '/'; + + return Cookies.set(name, value, attributes); +} + +export function remove(name: NAMES, attributes: Cookies.CookieAttributes = {}) { + return Cookies.remove(name, attributes); +} + +export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) { + return cookieString.split(`${ name }=`)[1]?.split(';')[0]; +} diff --git a/explorer/frontend/lib/date/dayjs.ts b/explorer/frontend/lib/date/dayjs.ts new file mode 100644 index 000000000..70424c17f --- /dev/null +++ b/explorer/frontend/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 'toolkit/utils/htmlEntities'; + +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/explorer/frontend/lib/decodeJWT.ts b/explorer/frontend/lib/decodeJWT.ts new file mode 100644 index 000000000..d94e21d4d --- /dev/null +++ b/explorer/frontend/lib/decodeJWT.ts @@ -0,0 +1,47 @@ +interface JWTHeader { + alg: string; + typ?: string; + [key: string]: unknown; +} + +interface JWTPayload { + [key: string]: unknown; +} + +const base64UrlDecode = (str: string): string => { + // Replace characters according to Base64Url standard + str = str.replace(/-/g, '+').replace(/_/g, '/'); + + // Add padding '=' characters for correct decoding + const pad = str.length % 4; + if (pad) { + str += '='.repeat(4 - pad); + } + + // Decode from Base64 to string + const decodedStr = atob(str); + + return decodedStr; +}; + +export default function decodeJWT(token: string): { header: JWTHeader; payload: JWTPayload; signature: string } | null { + try { + const parts = token.split('.'); + + if (parts.length !== 3) { + throw new Error('Invalid JWT format'); + } + + const [ encodedHeader, encodedPayload, signature ] = parts; + + const headerJson = base64UrlDecode(encodedHeader); + const payloadJson = base64UrlDecode(encodedPayload); + + const header = JSON.parse(headerJson) as JWTHeader; + const payload = JSON.parse(payloadJson) as JWTPayload; + + return { header, payload, signature }; + } catch (error) { + return null; + } +} diff --git a/explorer/frontend/lib/delay.ts b/explorer/frontend/lib/delay.ts new file mode 100644 index 000000000..efd2e216a --- /dev/null +++ b/explorer/frontend/lib/delay.ts @@ -0,0 +1,3 @@ +export default function delay(time: number) { + return new Promise((resolve) => window.setTimeout(resolve, time)); +} diff --git a/explorer/frontend/lib/downloadBlob.ts b/explorer/frontend/lib/downloadBlob.ts new file mode 100644 index 000000000..1a20dc623 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/getErrorCause.ts b/explorer/frontend/lib/errors/getErrorCause.ts new file mode 100644 index 000000000..5b553a701 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/getErrorCauseStatusCode.ts b/explorer/frontend/lib/errors/getErrorCauseStatusCode.ts new file mode 100644 index 000000000..f9884f03c --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/getErrorMessage.ts b/explorer/frontend/lib/errors/getErrorMessage.ts new file mode 100644 index 000000000..5e15f29a1 --- /dev/null +++ b/explorer/frontend/lib/errors/getErrorMessage.ts @@ -0,0 +1,6 @@ +import getErrorObj from './getErrorObj'; + +export default function getErrorMessage(error: unknown): string | undefined { + const errorObj = getErrorObj(error); + return errorObj && 'message' in errorObj && typeof errorObj.message === 'string' ? errorObj.message : undefined; +} diff --git a/explorer/frontend/lib/errors/getErrorObj.ts b/explorer/frontend/lib/errors/getErrorObj.ts new file mode 100644 index 000000000..7a06fdc6c --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/getErrorObjPayload.ts b/explorer/frontend/lib/errors/getErrorObjPayload.ts new file mode 100644 index 000000000..524979fe4 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/getErrorObjStatusCode.ts b/explorer/frontend/lib/errors/getErrorObjStatusCode.ts new file mode 100644 index 000000000..a672b9d7a --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/getErrorProp.ts b/explorer/frontend/lib/errors/getErrorProp.ts new file mode 100644 index 000000000..1e60b06b9 --- /dev/null +++ b/explorer/frontend/lib/errors/getErrorProp.ts @@ -0,0 +1,8 @@ +import getErrorObj from './getErrorObj'; + +export default function getErrorProp(error: unknown, prop: string): T | undefined { + const errorObj = getErrorObj(error); + return errorObj && prop in errorObj ? + (errorObj[prop as keyof typeof errorObj] as T) : + undefined; +} diff --git a/explorer/frontend/lib/errors/getResourceErrorPayload.tsx b/explorer/frontend/lib/errors/getResourceErrorPayload.tsx new file mode 100644 index 000000000..bbe3981a3 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/errors/throwOnAbsentParamError.ts b/explorer/frontend/lib/errors/throwOnAbsentParamError.ts new file mode 100644 index 000000000..db286c891 --- /dev/null +++ b/explorer/frontend/lib/errors/throwOnAbsentParamError.ts @@ -0,0 +1,7 @@ +export const ABSENT_PARAM_ERROR_MESSAGE = 'Required param not provided'; + +export default function throwOnAbsentParamError(param: unknown) { + if (!param) { + throw new Error(ABSENT_PARAM_ERROR_MESSAGE, { cause: { status: 404 } }); + } +} diff --git a/explorer/frontend/lib/errors/throwOnResourceLoadError.ts b/explorer/frontend/lib/errors/throwOnResourceLoadError.ts new file mode 100644 index 000000000..64075213c --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/escapeRegExp.ts b/explorer/frontend/lib/escapeRegExp.ts new file mode 100644 index 000000000..a92ec8003 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/getCurrencyValue.ts b/explorer/frontend/lib/getCurrencyValue.ts new file mode 100644 index 000000000..2588aedb2 --- /dev/null +++ b/explorer/frontend/lib/getCurrencyValue.ts @@ -0,0 +1,32 @@ +import BigNumber from 'bignumber.js'; + +import { ZERO } from 'toolkit/utils/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/explorer/frontend/lib/getErrorMessage.ts b/explorer/frontend/lib/getErrorMessage.ts new file mode 100644 index 000000000..b9fa1c9e7 --- /dev/null +++ b/explorer/frontend/lib/getErrorMessage.ts @@ -0,0 +1,3 @@ +export default function getErrorMessage(error: Record> | undefined, field: string) { + return error?.[field]?.join(', '); +} diff --git a/explorer/frontend/lib/getFilterValueFromQuery.ts b/explorer/frontend/lib/getFilterValueFromQuery.ts new file mode 100644 index 000000000..77d3c9989 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/getFilterValuesFromQuery.ts b/explorer/frontend/lib/getFilterValuesFromQuery.ts new file mode 100644 index 000000000..1ba9243d6 --- /dev/null +++ b/explorer/frontend/lib/getFilterValuesFromQuery.ts @@ -0,0 +1,11 @@ +import getValuesArrayFromQuery from './getValuesArrayFromQuery'; + +export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined) { + const valArray = getValuesArrayFromQuery(val); + + if (!valArray) { + return; + } + + return valArray.filter(el => filterValues.includes(el as unknown as FilterType)) as unknown as Array; +} diff --git a/explorer/frontend/lib/getItemIndex.ts b/explorer/frontend/lib/getItemIndex.ts new file mode 100644 index 000000000..6103c23dd --- /dev/null +++ b/explorer/frontend/lib/getItemIndex.ts @@ -0,0 +1,5 @@ +const DEFAULT_PAGE_SIZE = 50; + +export default function getItemIndex(index: number, page: number, pageSize: number = DEFAULT_PAGE_SIZE) { + return (page - 1) * pageSize + index + 1; +}; diff --git a/explorer/frontend/lib/getValueWithUnit.tsx b/explorer/frontend/lib/getValueWithUnit.tsx new file mode 100644 index 000000000..74be427a2 --- /dev/null +++ b/explorer/frontend/lib/getValueWithUnit.tsx @@ -0,0 +1,23 @@ +import BigNumber from 'bignumber.js'; + +import type { Unit } from 'types/unit'; + +import { WEI, GWEI } from 'toolkit/utils/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/explorer/frontend/lib/getValuesArrayFromQuery.ts b/explorer/frontend/lib/getValuesArrayFromQuery.ts new file mode 100644 index 000000000..647eb03cb --- /dev/null +++ b/explorer/frontend/lib/getValuesArrayFromQuery.ts @@ -0,0 +1,18 @@ +export default function getValuesArrayFromQuery(val: string | Array | undefined) { + if (val === undefined) { + return; + } + + const valArray = []; + if (typeof val === 'string') { + valArray.push(...val.split(',')); + } + if (Array.isArray(val)) { + if (!val.length) { + return; + } + val.forEach(el => valArray.push(...el.split(','))); + } + + return valArray; +} diff --git a/explorer/frontend/lib/growthbook/consts.ts b/explorer/frontend/lib/growthbook/consts.ts new file mode 100644 index 000000000..b687eedbb --- /dev/null +++ b/explorer/frontend/lib/growthbook/consts.ts @@ -0,0 +1,2 @@ +export const STORAGE_KEY = 'growthbook:experiments'; +export const STORAGE_LIMIT = 20; diff --git a/explorer/frontend/lib/growthbook/init.ts b/explorer/frontend/lib/growthbook/init.ts new file mode 100644 index 000000000..2b51d7e9a --- /dev/null +++ b/explorer/frontend/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 initGrowthBook = (uuid: string) => { + 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: uuid, + 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/explorer/frontend/lib/growthbook/useFeatureValue.ts b/explorer/frontend/lib/growthbook/useFeatureValue.ts new file mode 100644 index 000000000..7dde7e852 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/growthbook/useLoadFeatures.ts b/explorer/frontend/lib/growthbook/useLoadFeatures.ts new file mode 100644 index 000000000..193928d5a --- /dev/null +++ b/explorer/frontend/lib/growthbook/useLoadFeatures.ts @@ -0,0 +1,20 @@ +import type { GrowthBook } from '@growthbook/growthbook-react'; +import React from 'react'; + +import { SECOND } from 'toolkit/utils/consts'; + +export default function useLoadFeatures(growthBook: GrowthBook | undefined) { + React.useEffect(() => { + if (!growthBook) { + return; + } + + growthBook.setAttributes({ + ...growthBook.getAttributes(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + language: window.navigator.language, + }); + + growthBook.loadFeatures({ timeout: SECOND }); + }, [ growthBook ]); +} diff --git a/explorer/frontend/lib/hexToAddress.ts b/explorer/frontend/lib/hexToAddress.ts new file mode 100644 index 000000000..4ff126edd --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/hexToBase64.ts b/explorer/frontend/lib/hexToBase64.ts new file mode 100644 index 000000000..6fced2dc3 --- /dev/null +++ b/explorer/frontend/lib/hexToBase64.ts @@ -0,0 +1,8 @@ +import bytesToBase64 from './bytesToBase64'; +import hexToBytes from './hexToBytes'; + +export default function hexToBase64(hex: string) { + const bytes = hexToBytes(hex); + + return bytesToBase64(bytes); +} diff --git a/explorer/frontend/lib/hexToBytes.ts b/explorer/frontend/lib/hexToBytes.ts new file mode 100644 index 000000000..d42c93193 --- /dev/null +++ b/explorer/frontend/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 new Uint8Array(bytes); +} diff --git a/explorer/frontend/lib/hexToDecimal.ts b/explorer/frontend/lib/hexToDecimal.ts new file mode 100644 index 000000000..43bf1be78 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/hexToUtf8.ts b/explorer/frontend/lib/hexToUtf8.ts new file mode 100644 index 000000000..95e40ba09 --- /dev/null +++ b/explorer/frontend/lib/hexToUtf8.ts @@ -0,0 +1,8 @@ +import hexToBytes from 'lib/hexToBytes'; + +export default function hexToUtf8(hex: string) { + const utf8decoder = new TextDecoder(); + const bytes = hexToBytes(hex); + + return utf8decoder.decode(bytes); +} diff --git a/explorer/frontend/lib/highlightText.ts b/explorer/frontend/lib/highlightText.ts new file mode 100644 index 000000000..85bfe9106 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/hooks/useAdblockDetect.tsx b/explorer/frontend/lib/hooks/useAdblockDetect.tsx new file mode 100644 index 000000000..2f9856fd3 --- /dev/null +++ b/explorer/frontend/lib/hooks/useAdblockDetect.tsx @@ -0,0 +1,45 @@ +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 'toolkit/utils/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', + 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/explorer/frontend/lib/hooks/useAddressProfileApiQuery.tsx b/explorer/frontend/lib/hooks/useAddressProfileApiQuery.tsx new file mode 100644 index 000000000..ef39555d4 --- /dev/null +++ b/explorer/frontend/lib/hooks/useAddressProfileApiQuery.tsx @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query'; +import * as v from 'valibot'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import useFetch from 'lib/hooks/useFetch'; + +const feature = config.features.addressProfileAPI; + +type AddressInfoApiQueryResponse = v.InferOutput; + +const AddressInfoSchema = v.object({ + user_profile: v.object({ + username: v.union([ v.string(), v.null() ]), + }), +}); + +const ERROR_NAME = 'Invalid response schema'; + +export default function useAddressProfileApiQuery(hash: string | undefined, isEnabled = true) { + const fetch = useFetch(); + + return useQuery, AddressInfoApiQueryResponse>({ + queryKey: [ 'username_api', hash ], + queryFn: async() => { + if (!feature.isEnabled || !hash) { + return Promise.reject(); + } + + return fetch(feature.apiUrlTemplate.replace('{address}', hash)); + }, + enabled: isEnabled && Boolean(hash), + refetchOnMount: false, + select: (response) => { + const parsedResponse = v.safeParse(AddressInfoSchema, response); + + if (!parsedResponse.success) { + throw Error(ERROR_NAME); + } + + return parsedResponse.output; + }, + }); +} diff --git a/explorer/frontend/lib/hooks/useClientRect.tsx b/explorer/frontend/lib/hooks/useClientRect.tsx new file mode 100644 index 000000000..44033e30c --- /dev/null +++ b/explorer/frontend/lib/hooks/useClientRect.tsx @@ -0,0 +1,37 @@ +import { debounce } from 'es-toolkit'; +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/explorer/frontend/lib/hooks/useDebounce.tsx b/explorer/frontend/lib/hooks/useDebounce.tsx new file mode 100644 index 000000000..5dfc71c14 --- /dev/null +++ b/explorer/frontend/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/explorer/frontend/lib/hooks/useFetch.tsx b/explorer/frontend/lib/hooks/useFetch.tsx new file mode 100644 index 000000000..ea1977f25 --- /dev/null +++ b/explorer/frontend/lib/hooks/useFetch.tsx @@ -0,0 +1,97 @@ +import React from 'react'; + +import isBodyAllowed from 'lib/api/isBodyAllowed'; +import type { ResourceError, ResourcePath } from 'lib/api/resources'; +import { useRollbar } from 'lib/rollbar'; + +export interface Params { + method?: RequestInit['method']; + headers?: RequestInit['headers']; + signal?: RequestInit['signal']; + body?: Record | FormData; + credentials?: RequestCredentials; +} + +interface Meta { + resource?: ResourcePath; + logError?: boolean; +} + +export default function useFetch() { + const rollbar = useRollbar(); + + 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 => { + + const isJson = response.headers.get('content-type')?.includes('application/json'); + + if (!response.ok) { + const error = { + status: response.status, + statusText: response.statusText, + }; + + if (meta?.logError && rollbar) { + rollbar.warn('Client fetch failed', { + resource: meta?.resource, + status_code: error.status, + status_text: error.statusText, + }); + } + + if (!isJson) { + return response.text().then( + (textError) => Promise.reject({ + payload: textError, + status: response.status, + statusText: response.statusText, + }), + ); + } + + return response.json().then( + (jsonError) => Promise.reject({ + payload: jsonError as Error, + status: response.status, + statusText: response.statusText, + }), + () => { + return Promise.reject(error); + }, + ); + + } else { + if (isJson) { + return response.json() as Promise; + } + + return Promise.resolve() as Promise; + } + }); + }, [ rollbar ]); +} diff --git a/explorer/frontend/lib/hooks/useGetCsrfToken.tsx b/explorer/frontend/lib/hooks/useGetCsrfToken.tsx new file mode 100644 index 000000000..db3c74b7e --- /dev/null +++ b/explorer/frontend/lib/hooks/useGetCsrfToken.tsx @@ -0,0 +1,38 @@ +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(); + + return useQuery({ + queryKey: getResourceKey('general:csrf'), + queryFn: async() => { + if (!isNeedProxy()) { + const url = buildUrl('general:csrf'); + const apiResponse = await fetch(url, { credentials: 'include' }); + const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf'); + + if (!csrfFromHeader) { + // I am not sure should we log this error or not + // so I commented it out for now + // rollbar?.warn('Client fetch failed', { + // 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/explorer/frontend/lib/hooks/useGradualIncrement.tsx b/explorer/frontend/lib/hooks/useGradualIncrement.tsx new file mode 100644 index 000000000..e1bab30e2 --- /dev/null +++ b/explorer/frontend/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(0); + const timeoutId = React.useRef(0); + const delay = React.useRef(0); + + const incrementDelayed = React.useCallback(() => { + if (queue.current === 0) { + return; + } + + queue.current--; + setNum(prev => prev + 1); + timeoutId.current = 0; + }, []); + + const increment = React.useCallback((inc: number) => { + if (inc < 1) { + return; + } + + queue.current += inc; + + if (!timeoutId.current) { + timeoutId.current = window.setTimeout(incrementDelayed, 0); + } + }, [ incrementDelayed ]); + + React.useEffect(() => { + if (queue.current > 0 && !timeoutId.current) { + if (!delay.current) { + delay.current = DURATION / queue.current; + } else if (delay.current > DURATION / queue.current) { + // in case if queue size is increased since last DOM update + delay.current = DURATION / queue.current; + } + timeoutId.current = window.setTimeout(incrementDelayed, delay.current); + } + }, [ incrementDelayed, num ]); + + React.useEffect(() => { + return () => { + window.clearTimeout(timeoutId.current); + }; + }, []); + + return [ num, increment ]; +} diff --git a/explorer/frontend/lib/hooks/useGraphLinks.tsx b/explorer/frontend/lib/hooks/useGraphLinks.tsx new file mode 100644 index 000000000..8943f38ba --- /dev/null +++ b/explorer/frontend/lib/hooks/useGraphLinks.tsx @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import useFetch from 'lib/hooks/useFetch'; + +const feature = config.features.marketplace; + +export default function useGraphLinks() { + const fetch = useFetch(); + + return useQuery, Record>>({ + queryKey: [ 'graph-links' ], + queryFn: async() => fetch((feature.isEnabled && feature.graphLinksUrl) ? feature.graphLinksUrl : '', undefined, { resource: 'graph-links' }), + enabled: feature.isEnabled && Boolean(feature.graphLinksUrl), + staleTime: Infinity, + placeholderData: {}, + }); +} diff --git a/explorer/frontend/lib/hooks/useInitialList.tsx b/explorer/frontend/lib/hooks/useInitialList.tsx new file mode 100644 index 000000000..e8f5aab9a --- /dev/null +++ b/explorer/frontend/lib/hooks/useInitialList.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +type Id = string | number; + +export interface Params { + data: Array; + idFn: (item: T) => Id; + enabled: boolean; +} + +export default function useInitialList({ data, idFn, enabled }: Params) { + const [ list, setList ] = React.useState>([]); + + React.useEffect(() => { + if (enabled) { + setList(data.map(idFn)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ enabled ]); + + const isNew = React.useCallback((data: T) => { + return !list.includes(idFn(data)); + }, [ list, idFn ]); + + const getAnimationProp = React.useCallback((data: T) => { + return isNew(data) ? 'fade-in 500ms linear' : undefined; + }, [ isNew ]); + + return React.useMemo(() => { + return { + list, + isNew, + getAnimationProp, + }; + }, [ list, isNew, getAnimationProp ]); +} diff --git a/explorer/frontend/lib/hooks/useIsInitialLoading.tsx b/explorer/frontend/lib/hooks/useIsInitialLoading.tsx new file mode 100644 index 000000000..de07b15fd --- /dev/null +++ b/explorer/frontend/lib/hooks/useIsInitialLoading.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export default function useIsInitialLoading(isLoading: boolean | undefined) { + const [ isInitialLoading, setIsInitialLoading ] = React.useState(Boolean(isLoading)); + + React.useEffect(() => { + if (!isLoading) { + setIsInitialLoading(false); + } + }, [ isLoading ]); + + return isInitialLoading; +} diff --git a/explorer/frontend/lib/hooks/useIsMobile.tsx b/explorer/frontend/lib/hooks/useIsMobile.tsx new file mode 100644 index 000000000..f8a8dcd7e --- /dev/null +++ b/explorer/frontend/lib/hooks/useIsMobile.tsx @@ -0,0 +1,5 @@ +import { useBreakpointValue } from '@chakra-ui/react'; + +export default function useIsMobile(ssr = true) { + return useBreakpointValue({ base: true, lg: false }, { ssr }); +} diff --git a/explorer/frontend/lib/hooks/useIsMounted.tsx b/explorer/frontend/lib/hooks/useIsMounted.tsx new file mode 100644 index 000000000..d14880ae1 --- /dev/null +++ b/explorer/frontend/lib/hooks/useIsMounted.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function useIsMounted() { + const [ isMounted, setIsMounted ] = React.useState(false); + + React.useEffect(() => { + setIsMounted(true); + }, [ ]); + + return isMounted; +} diff --git a/explorer/frontend/lib/hooks/useIsSafeAddress.tsx b/explorer/frontend/lib/hooks/useIsSafeAddress.tsx new file mode 100644 index 000000000..7888050a6 --- /dev/null +++ b/explorer/frontend/lib/hooks/useIsSafeAddress.tsx @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; + +import config from 'configs/app'; +import useFetch from 'lib/hooks/useFetch'; + +const feature = config.features.safe; + +export default function useIsSafeAddress(hash: string | undefined): boolean { + const fetch = useFetch(); + + const { data } = useQuery({ + queryKey: [ 'safe_transaction_api', hash ], + queryFn: async() => { + if (!feature.isEnabled || !hash) { + return Promise.reject(); + } + + return fetch(`${ feature.apiUrl }/${ hash }`); + }, + enabled: feature.isEnabled && Boolean(hash), + refetchOnMount: false, + }); + + return Boolean(data); +} diff --git a/explorer/frontend/lib/hooks/useIssueUrl.tsx b/explorer/frontend/lib/hooks/useIssueUrl.tsx new file mode 100644 index 000000000..0ac696746 --- /dev/null +++ b/explorer/frontend/lib/hooks/useIssueUrl.tsx @@ -0,0 +1,32 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; + +export default function useIssueUrl(backendVersion: string | undefined) { + const [ isLoading, setIsLoading ] = React.useState(true); + const router = useRouter(); + + React.useEffect(() => { + setIsLoading(false); + }, [ ]); + + return React.useMemo(() => { + if (isLoading) { + return ''; + } + + const searchParams = new URLSearchParams({ + template: 'bug_report.yml', + labels: 'triage', + link: window.location.href, + 'backend-version': backendVersion || '', + 'frontend-version': [ config.UI.footer.frontendVersion, config.UI.footer.frontendCommit ].filter(Boolean).join('+'), + 'additional-information': `**User Agent:** ${ window.navigator.userAgent }`, + }); + return `https://github.com/blockscout/blockscout/issues/new/?${ searchParams.toString() }`; + // we need to update link whenever page url changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ backendVersion, isLoading, router.asPath ]); + +} diff --git a/explorer/frontend/lib/hooks/useLazyRenderedList.tsx b/explorer/frontend/lib/hooks/useLazyRenderedList.tsx new file mode 100644 index 000000000..4ebd630b1 --- /dev/null +++ b/explorer/frontend/lib/hooks/useLazyRenderedList.tsx @@ -0,0 +1,23 @@ +import { clamp } from 'es-toolkit'; +import React from 'react'; +import { useInView } from 'react-intersection-observer'; + +const STEP = 10; +const MIN_ITEMS_NUM = 50; + +export default function useLazyRenderedList(list: Array, isEnabled: boolean, minItemsNum: number = MIN_ITEMS_NUM) { + const [ renderedItemsNum, setRenderedItemsNum ] = React.useState(minItemsNum); + const { ref, inView } = useInView({ + rootMargin: '200px', + triggerOnce: false, + skip: !isEnabled || list.length <= minItemsNum, + }); + + React.useEffect(() => { + if (inView) { + setRenderedItemsNum((prev) => clamp(prev + STEP, 0, list.length)); + } + }, [ inView, list.length ]); + + return { cutRef: ref, renderedItemsNum }; +} diff --git a/explorer/frontend/lib/hooks/useNavItems.tsx b/explorer/frontend/lib/hooks/useNavItems.tsx new file mode 100644 index 000000000..b0d30d0a9 --- /dev/null +++ b/explorer/frontend/lib/hooks/useNavItems.tsx @@ -0,0 +1,349 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/navigation'; + +import config from 'configs/app'; +import { rightLineArrow } from 'toolkit/utils/htmlEntities'; + +interface ReturnType { + mainNavItems: Array; + accountNavItems: Array; +} + +export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem { + return 'subItems' in item; +} + +export function isInternalItem(item: NavItem): item is NavItemInternal { + return 'nextRoute' in item; +} + +export default function useNavItems(): ReturnType { + const router = useRouter(); + const pathname = router.pathname; + + return React.useMemo(() => { + let blockchainNavItems: Array | Array> = []; + + const topAccounts: NavItem | null = !config.UI.views.address.hiddenViews?.top_accounts ? { + text: 'Top accounts', + nextRoute: { pathname: '/accounts' as const }, + icon: 'top-accounts', + isActive: pathname === '/accounts', + } : null; + const blocks: NavItem | null = { + text: 'Blocks', + nextRoute: { pathname: '/blocks' as const }, + icon: 'block', + isActive: pathname === '/blocks' || pathname === '/block/[height_or_hash]', + }; + const txs: NavItem | null = { + text: 'Transactions', + nextRoute: { pathname: '/txs' as const }, + icon: 'transactions', + isActive: pathname === '/txs' || pathname === '/tx/[hash]', + }; + const internalTxs: NavItem | null = { + text: 'Internal transactions', + nextRoute: { pathname: '/internal-txs' as const }, + icon: 'internal_txns', + isActive: pathname === '/internal-txs', + }; + const userOps: NavItem | null = config.features.userOps.isEnabled ? { + text: 'User operations', + nextRoute: { pathname: '/ops' as const }, + icon: 'user_op', + isActive: pathname === '/ops' || pathname === '/op/[hash]', + } : null; + + const verifiedContracts: NavItem | null = + { + text: 'Verified contracts', + nextRoute: { pathname: '/verified-contracts' as const }, + icon: 'verified', + isActive: pathname === '/verified-contracts', + }; + const ensLookup = config.features.nameService.isEnabled ? { + text: 'Name services lookup', + nextRoute: { pathname: '/name-domains' as const }, + icon: 'ENS', + isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]', + } : null; + const validators = config.features.validators.isEnabled ? { + text: 'Validators', + nextRoute: { pathname: '/validators' as const }, + icon: 'validator', + isActive: pathname === '/validators' || pathname === '/validators/[id]', + } : null; + const rollupDeposits = { + text: `Deposits (L1${ rightLineArrow }L2)`, + nextRoute: { pathname: '/deposits' as const }, + icon: 'arrows/south-east', + isActive: pathname === '/deposits', + }; + const rollupWithdrawals = { + text: `Withdrawals (L2${ rightLineArrow }L1)`, + nextRoute: { pathname: '/withdrawals' as const }, + icon: 'arrows/north-east', + isActive: pathname === '/withdrawals', + }; + const rollupTxnBatches = { + text: 'Txn batches', + nextRoute: { pathname: '/batches' as const }, + icon: 'txn_batches', + isActive: pathname === '/batches', + }; + const rollupOutputRoots = { + text: 'Output roots', + nextRoute: { pathname: '/output-roots' as const }, + icon: 'output_roots', + isActive: pathname === '/output-roots', + }; + const rollupDisputeGames = config.features.faultProofSystem.isEnabled ? { + text: 'Dispute games', + nextRoute: { pathname: '/dispute-games' as const }, + icon: 'games', + isActive: pathname === '/dispute-games', + } : null; + const mudWorlds = config.features.mudFramework.isEnabled ? { + text: 'MUD worlds', + nextRoute: { pathname: '/mud-worlds' as const }, + icon: 'MUD_menu', + isActive: pathname === '/mud-worlds', + } : null; + + const rollupFeature = config.features.rollup; + + const rollupInteropMessages = rollupFeature.isEnabled && rollupFeature.interopEnabled ? { + text: 'Interop messages', + nextRoute: { pathname: '/interop-messages' as const }, + icon: 'interop', + isActive: pathname === '/interop-messages', + } : null; + + if (rollupFeature.isEnabled && ( + rollupFeature.type === 'optimistic' || + rollupFeature.type === 'arbitrum' || + rollupFeature.type === 'zkEvm' || + rollupFeature.type === 'scroll' + )) { + blockchainNavItems = [ + [ + txs, + internalTxs, + rollupDeposits, + rollupWithdrawals, + rollupInteropMessages, + ].filter(Boolean), + [ + blocks, + rollupTxnBatches, + rollupDisputeGames, + rollupFeature.outputRootsEnabled ? rollupOutputRoots : undefined, + ].filter(Boolean), + [ + userOps, + topAccounts, + mudWorlds, + validators, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { + blockchainNavItems = [ + [ + txs, + internalTxs, + rollupDeposits, + rollupWithdrawals, + ], + [ + blocks, + userOps, + topAccounts, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else if (rollupFeature.isEnabled && rollupFeature.type === 'zkSync') { + blockchainNavItems = [ + [ + txs, + internalTxs, + userOps, + blocks, + rollupTxnBatches, + ].filter(Boolean), + [ + topAccounts, + validators, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else { + blockchainNavItems = [ + txs, + internalTxs, + userOps, + blocks, + topAccounts, + validators, + verifiedContracts, + ensLookup, + config.features.beaconChain.isEnabled && { + text: 'Withdrawals', + nextRoute: { pathname: '/withdrawals' as const }, + icon: 'arrows/north-east', + isActive: pathname === '/withdrawals', + }, + ].filter(Boolean); + } + + const tokensNavItems = [ + { + text: 'Tokens', + nextRoute: { pathname: '/tokens' as const }, + icon: 'token', + isActive: pathname === '/tokens' || pathname.startsWith('/token/'), + }, + { + text: 'Token transfers', + nextRoute: { pathname: '/token-transfers' as const }, + icon: 'token-transfers', + isActive: pathname === '/token-transfers', + }, + config.features.pools.isEnabled && { + text: 'DEX tracker', + nextRoute: { pathname: '/pools' as const }, + icon: 'dex-tracker', + isActive: pathname === '/pools' || pathname.startsWith('/pool/'), + }, + ].filter(Boolean); + + const apiNavItems: Array = [ + config.features.restApiDocs.isEnabled ? { + text: 'REST API', + nextRoute: { pathname: '/api-docs' as const }, + icon: 'restAPI', + isActive: pathname === '/api-docs', + } : null, + config.features.graphqlApiDocs.isEnabled ? { + text: 'GraphQL', + nextRoute: { pathname: '/graphiql' as const }, + icon: 'graphQL', + isActive: pathname === '/graphiql', + } : null, + !config.UI.navigation.hiddenLinks?.rpc_api && { + text: 'RPC API', + icon: 'RPC', + url: 'https://docs.blockscout.com/for-users/api/rpc-endpoints', + }, + !config.UI.navigation.hiddenLinks?.eth_rpc_api && { + text: 'Eth RPC API', + icon: 'RPC', + url: ' https://docs.blockscout.com/for-users/api/eth-rpc', + }, + ].filter(Boolean); + + const otherNavItems: Array | Array> = [ + { + text: 'Verify contract', + nextRoute: { pathname: '/contract-verification' as const }, + isActive: pathname.startsWith('/contract-verification'), + }, + config.features.gasTracker.isEnabled && { + text: 'Gas tracker', + nextRoute: { pathname: '/gas-tracker' as const }, + isActive: pathname.startsWith('/gas-tracker'), + }, + config.features.publicTagsSubmission.isEnabled && { + text: 'Submit public tag', + nextRoute: { pathname: '/public-tags/submit' as const }, + isActive: pathname.startsWith('/public-tags/submit'), + }, + rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && { + text: 'Txn withdrawals', + nextRoute: { pathname: '/txn-withdrawals' as const }, + isActive: pathname.startsWith('/txn-withdrawals'), + }, + ...config.UI.navigation.otherLinks, + ].filter(Boolean); + + const mainNavItems: ReturnType['mainNavItems'] = [ + { + text: 'Blockchain', + icon: 'globe-b', + isActive: blockchainNavItems.flat().some(item => isInternalItem(item) && item.isActive), + subItems: blockchainNavItems, + }, + { + text: 'Tokens', + icon: 'token', + isActive: tokensNavItems.flat().some(item => isInternalItem(item) && item.isActive), + subItems: tokensNavItems, + }, + config.features.marketplace.isEnabled ? { + text: 'DApps', + nextRoute: { pathname: '/apps' as const }, + icon: 'apps', + isActive: pathname.startsWith('/app'), + } : null, + config.features.stats.isEnabled ? { + text: 'Charts & stats', + nextRoute: { pathname: '/stats' as const }, + icon: 'stats', + isActive: pathname.startsWith('/stats'), + } : null, + apiNavItems.length > 0 && { + text: 'API', + icon: 'restAPI', + isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive), + subItems: apiNavItems, + }, + { + text: 'Other', + icon: 'gear', + isActive: otherNavItems.flat().some(item => isInternalItem(item) && item.isActive), + subItems: otherNavItems, + }, + ].filter(Boolean); + + const accountNavItems: ReturnType['accountNavItems'] = [ + { + text: 'Watch list', + nextRoute: { pathname: '/account/watchlist' as const }, + icon: 'watchlist', + isActive: pathname === '/account/watchlist', + }, + { + text: 'Private tags', + nextRoute: { pathname: '/account/tag-address' as const }, + icon: 'privattags', + isActive: pathname === '/account/tag-address', + }, + { + text: 'API keys', + nextRoute: { pathname: '/account/api-key' as const }, + icon: 'API', + isActive: pathname === '/account/api-key', + }, + { + text: 'Custom ABI', + nextRoute: { pathname: '/account/custom-abi' as const }, + icon: 'ABI', + isActive: pathname === '/account/custom-abi', + }, + config.features.addressVerification.isEnabled && { + text: 'Verified addrs', + nextRoute: { pathname: '/account/verified-addresses' as const }, + icon: 'verified', + isActive: pathname === '/account/verified-addresses', + }, + ].filter(Boolean); + + return { mainNavItems, accountNavItems }; + }, [ pathname ]); +} diff --git a/explorer/frontend/lib/hooks/useNotifyOnNavigation.tsx b/explorer/frontend/lib/hooks/useNotifyOnNavigation.tsx new file mode 100644 index 000000000..a3b7a7c92 --- /dev/null +++ b/explorer/frontend/lib/hooks/useNotifyOnNavigation.tsx @@ -0,0 +1,24 @@ +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +export default function useNotifyOnNavigation() { + const router = useRouter(); + const pathname = usePathname(); + const tab = getQueryParamString(router.query.tab); + + React.useEffect(() => { + if (config.features.metasuites.isEnabled) { + window.postMessage({ source: 'APP_ROUTER', type: 'PATHNAME_CHANGED' }, window.location.origin); + } + }, [ pathname ]); + + React.useEffect(() => { + if (config.features.metasuites.isEnabled) { + window.postMessage({ source: 'APP_ROUTER', type: 'TAB_CHANGED' }, window.location.origin); + } + }, [ tab ]); +} diff --git a/explorer/frontend/lib/hooks/usePreventFocusAfterModalClosing.tsx b/explorer/frontend/lib/hooks/usePreventFocusAfterModalClosing.tsx new file mode 100644 index 000000000..ec9e22fd1 --- /dev/null +++ b/explorer/frontend/lib/hooks/usePreventFocusAfterModalClosing.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +// prevent set focus on button when closing modal +export default function usePreventFocusAfterModalClosing() { + return React.useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []); +} diff --git a/explorer/frontend/lib/hooks/useRewardsActivity.tsx b/explorer/frontend/lib/hooks/useRewardsActivity.tsx new file mode 100644 index 000000000..2b03808f5 --- /dev/null +++ b/explorer/frontend/lib/hooks/useRewardsActivity.tsx @@ -0,0 +1,114 @@ +import { useCallback, useRef, useEffect } from 'react'; + +import type { PreSubmitTransactionResponse } from '@blockscout/points-types'; + +import config from 'configs/app'; +import useApiFetch from 'lib/api/useApiFetch'; +import useApiQuery from 'lib/api/useApiQuery'; +import { useRewardsContext } from 'lib/contexts/rewards'; +import { MINUTE } from 'toolkit/utils/consts'; +import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; + +const feature = config.features.rewards; +const LAST_EXPLORE_TIME_KEY = 'rewards_activity_last_explore_time'; + +type RewardsActivityEndpoint = + | 'rewards:user_activity_track_tx' + | 'rewards:user_activity_track_tx_confirm' + | 'rewards:user_activity_track_contract' + | 'rewards:user_activity_track_contract_confirm' + | 'rewards:user_activity_track_usage'; + +export default function useRewardsActivity() { + const { apiToken } = useRewardsContext(); + const apiFetch = useApiFetch(); + const lastExploreTime = useRef(0); + + const profileQuery = useProfileQuery(); + const checkActivityPassQuery = useApiQuery('rewards:user_check_activity_pass', { + queryOptions: { + enabled: feature.isEnabled && Boolean(apiToken) && Boolean(profileQuery.data?.address_hash), + }, + queryParams: { + address: profileQuery.data?.address_hash ?? '', + }, + }); + + useEffect(() => { + try { + const storedTime = window.localStorage.getItem(LAST_EXPLORE_TIME_KEY); + if (storedTime) { + lastExploreTime.current = Number(storedTime); + } + } catch {} + }, []); + + const makeRequest = useCallback(async(endpoint: RewardsActivityEndpoint, params: Record) => { + if (!apiToken || !checkActivityPassQuery.data?.is_valid) { + return; + } + + try { + return await apiFetch(endpoint, { + fetchParams: { + method: 'POST', + body: params, + headers: { Authorization: `Bearer ${ apiToken }` }, + }, + }); + } catch {} + }, [ apiFetch, checkActivityPassQuery.data, apiToken ]); + + const trackTransaction = useCallback(async(from: string, to: string) => { + return ( + await makeRequest('rewards:user_activity_track_tx', { + from_address: from, + to_address: to, + chain_id: config.chain.id ?? '', + }) + ) as PreSubmitTransactionResponse | undefined; + }, [ makeRequest ]); + + const trackTransactionConfirm = useCallback((hash: string, token: string) => + makeRequest('rewards:user_activity_track_tx_confirm', { tx_hash: hash, token }), + [ makeRequest ], + ); + + const trackContract = useCallback(async(address: string) => + makeRequest('rewards:user_activity_track_contract', { + address, + chain_id: config.chain.id ?? '', + }), + [ makeRequest ], + ); + + const trackUsage = useCallback((action: string) => { + // check here because this function is called on page load + if (!apiToken || !checkActivityPassQuery.data?.is_valid) { + return; + } + + if (action === 'explore') { + const now = Date.now(); + if (now - lastExploreTime.current < 5 * MINUTE) { + return; + } + lastExploreTime.current = now; + try { + window.localStorage.setItem(LAST_EXPLORE_TIME_KEY, String(now)); + } catch {} + } + + return makeRequest('rewards:user_activity_track_usage', { + action, + chain_id: config.chain.id ?? '', + }); + }, [ makeRequest, apiToken, checkActivityPassQuery.data ]); + + return { + trackTransaction, + trackTransactionConfirm, + trackContract, + trackUsage, + }; +} diff --git a/explorer/frontend/lib/hooks/useTimeAgoIncrement.tsx b/explorer/frontend/lib/hooks/useTimeAgoIncrement.tsx new file mode 100644 index 000000000..080f3037f --- /dev/null +++ b/explorer/frontend/lib/hooks/useTimeAgoIncrement.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import dayjs from 'lib/date/dayjs'; +import { DAY, HOUR, MINUTE, SECOND } from 'toolkit/utils/consts'; + +function getUnits(diff: number) { + if (diff < MINUTE) { + return [ SECOND, MINUTE ]; + } + + if (diff < HOUR) { + return [ MINUTE, HOUR ]; + } + + if (diff < DAY) { + return [ HOUR, DAY ]; + } + + return [ DAY, 2 * DAY ]; +} + +function getUpdateParams(ts: string | number) { + const timeDiff = Date.now() - new Date(ts).getTime(); + const [ unit, higherUnit ] = getUnits(timeDiff); + + if (unit === DAY) { + return { interval: DAY }; + } + + const leftover = unit - timeDiff % unit; + + return { + startTimeout: unit === SECOND ? + 0 : + // here we assume that in current dayjs locale time difference is rounded by Math.round function + // so we have to update displayed value whenever time comes over the middle of the unit interval + // since it will be rounded to the upper bound + (leftover < unit / 2 ? leftover + unit / 2 : leftover - unit / 2) + SECOND, + endTimeout: higherUnit - timeDiff + SECOND, + interval: unit, + }; +} + +export default function useTimeAgoIncrement(ts: string | number | null, isEnabled?: boolean) { + const [ value, setValue ] = React.useState(ts ? dayjs(ts).fromNow() : null); + + React.useEffect(() => { + if (ts !== null) { + const timeouts: Array = []; + const intervals: Array = []; + + const startIncrement = () => { + const { startTimeout, interval, endTimeout } = getUpdateParams(ts); + if (!startTimeout && !endTimeout) { + return; + } + + let intervalId: number; + + const startTimeoutId = window.setTimeout(() => { + setValue(dayjs(ts).fromNow()); + + intervalId = window.setInterval(() => { + setValue(dayjs(ts).fromNow()); + }, interval); + + intervals.push(intervalId); + }, startTimeout); + + const endTimeoutId = window.setTimeout(() => { + window.clearInterval(intervalId); + startIncrement(); + }, endTimeout); + + timeouts.push(startTimeoutId); + timeouts.push(endTimeoutId); + }; + + isEnabled && startIncrement(); + + !isEnabled && setValue(dayjs(ts).fromNow()); + + return () => { + timeouts.forEach(window.clearTimeout); + intervals.forEach(window.clearInterval); + }; + } + }, [ isEnabled, ts ]); + + return value; +} diff --git a/explorer/frontend/lib/hooks/useUpdateValueEffect.tsx b/explorer/frontend/lib/hooks/useUpdateValueEffect.tsx new file mode 100644 index 000000000..a7d7da2aa --- /dev/null +++ b/explorer/frontend/lib/hooks/useUpdateValueEffect.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +// run effect only if value is updated since initial mount +const useUpdateValueEffect = (effect: () => void, value: string) => { + const mountedRef = React.useRef(false); + const valueRef = React.useRef(); + const isChangedRef = React.useRef(false); + + React.useEffect(() => { + mountedRef.current = true; + valueRef.current = value; + + return () => { + mountedRef.current = false; + valueRef.current = undefined; + isChangedRef.current = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + if (mountedRef.current && (value !== valueRef.current || isChangedRef.current)) { + isChangedRef.current = true; + return effect(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ value ]); +}; + +export default useUpdateValueEffect; diff --git a/explorer/frontend/lib/isMetaKey.tsx b/explorer/frontend/lib/isMetaKey.tsx new file mode 100644 index 000000000..ce9b0a9d2 --- /dev/null +++ b/explorer/frontend/lib/isMetaKey.tsx @@ -0,0 +1,5 @@ +import type React from 'react'; + +export default function isMetaKey(event: React.KeyboardEvent) { + return event.metaKey || event.getModifierState('Meta'); +} diff --git a/explorer/frontend/lib/metadata/__snapshots__/generate.test.ts.snap b/explorer/frontend/lib/metadata/__snapshots__/generate.test.ts.snap new file mode 100644 index 000000000..7c18d3240 --- /dev/null +++ b/explorer/frontend/lib/metadata/__snapshots__/generate.test.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generates correct metadata for: dynamic route 1`] = ` +{ + "canonical": undefined, + "description": "View transaction 0x12345 on Blockscout (Blockscout) Explorer", + "opengraph": { + "description": "", + "imageUrl": "", + "title": "Blockscout transaction 0x12345 | Blockscout", + }, + "title": "Blockscout transaction 0x12345 | Blockscout", +} +`; + +exports[`generates correct metadata for: dynamic route with API data 1`] = ` +{ + "canonical": undefined, + "description": "0x12345, balances and analytics on the Blockscout (Blockscout) Explorer", + "opengraph": { + "description": "", + "imageUrl": "", + "title": "Blockscout USDT token details | Blockscout", + }, + "title": "Blockscout USDT token details | Blockscout", +} +`; + +exports[`generates correct metadata for: static route 1`] = ` +{ + "canonical": "http://localhost:3000/txs", + "description": "Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the Blockscout (Blockscout) Explorer network.", + "opengraph": { + "description": "", + "imageUrl": "http://localhost:3000/static/og_image.png", + "title": "Blockscout transactions - Blockscout explorer | Blockscout", + }, + "title": "Blockscout transactions - Blockscout explorer | Blockscout", +} +`; diff --git a/explorer/frontend/lib/metadata/compileValue.ts b/explorer/frontend/lib/metadata/compileValue.ts new file mode 100644 index 000000000..63442394a --- /dev/null +++ b/explorer/frontend/lib/metadata/compileValue.ts @@ -0,0 +1,16 @@ +export default function compileValue(template: string, params: Record | undefined>) { + const PLACEHOLDER_REGEX = /%(\w+)%/g; + return template.replaceAll(PLACEHOLDER_REGEX, (match, p1) => { + const value = params[p1]; + + if (Array.isArray(value)) { + return value.join(', '); + } + + if (value === undefined) { + return ''; + } + + return value; + }); +} diff --git a/explorer/frontend/lib/metadata/generate.test.ts b/explorer/frontend/lib/metadata/generate.test.ts new file mode 100644 index 000000000..709365038 --- /dev/null +++ b/explorer/frontend/lib/metadata/generate.test.ts @@ -0,0 +1,47 @@ +import type { ApiData } from './types'; + +import type { Route } from 'nextjs-routes'; + +import generate from './generate'; + +interface TestCase { + title: string; + route: { + pathname: Pathname; + query?: Route['query']; + }; + apiData?: ApiData; +} + +const TEST_CASES = [ + { + title: 'static route', + route: { + pathname: '/txs', + }, + } as TestCase<'/txs'>, + { + title: 'dynamic route', + route: { + pathname: '/tx/[hash]', + query: { hash: '0x12345' }, + }, + } as TestCase<'/tx/[hash]'>, + { + title: 'dynamic route with API data', + route: { + pathname: '/token/[hash]', + query: { hash: '0x12345' }, + }, + apiData: { symbol_or_name: 'USDT' }, + } as TestCase<'/token/[hash]'>, +]; + +describe('generates correct metadata for:', () => { + TEST_CASES.forEach((testCase) => { + it(`${ testCase.title }`, () => { + const result = generate(testCase.route, testCase.apiData); + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/explorer/frontend/lib/metadata/generate.ts b/explorer/frontend/lib/metadata/generate.ts new file mode 100644 index 000000000..c3d4ed15c --- /dev/null +++ b/explorer/frontend/lib/metadata/generate.ts @@ -0,0 +1,39 @@ +import type { ApiData, Metadata } from './types'; +import type { RouteParams } from 'nextjs/types'; + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; +import getNetworkTitle from 'lib/networks/getNetworkTitle'; +import { currencyUnits } from 'lib/units'; + +import compileValue from './compileValue'; +import getCanonicalUrl from './getCanonicalUrl'; +import getPageOgType from './getPageOgType'; +import * as templates from './templates'; + +export default function generate(route: RouteParams, apiData: ApiData = null): Metadata { + const params = { + ...route.query, + ...apiData, + network_name: config.chain.name, + network_title: getNetworkTitle(), + network_gwei: currencyUnits.gwei, + }; + + const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); + const description = compileValue(templates.description.make(route.pathname, Boolean(apiData)), params); + + const pageOgType = getPageOgType(route.pathname); + + return { + title: title, + description, + opengraph: { + title: title, + description: pageOgType !== 'Regular page' ? config.meta.og.description : '', + imageUrl: pageOgType !== 'Regular page' ? config.meta.og.imageUrl : '', + }, + canonical: getCanonicalUrl(route.pathname), + }; +} diff --git a/explorer/frontend/lib/metadata/getCanonicalUrl.ts b/explorer/frontend/lib/metadata/getCanonicalUrl.ts new file mode 100644 index 000000000..2a868419a --- /dev/null +++ b/explorer/frontend/lib/metadata/getCanonicalUrl.ts @@ -0,0 +1,24 @@ +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const CANONICAL_ROUTES: Array = [ + '/', + '/txs', + '/ops', + '/verified-contracts', + '/name-domains', + '/withdrawals', + '/tokens', + '/stats', + '/api-docs', + '/graphiql', + '/gas-tracker', + '/apps', +]; + +export default function getCanonicalUrl(pathname: Route['pathname']) { + if (CANONICAL_ROUTES.includes(pathname)) { + return config.app.baseUrl + pathname; + } +} diff --git a/explorer/frontend/lib/metadata/getPageOgType.ts b/explorer/frontend/lib/metadata/getPageOgType.ts new file mode 100644 index 000000000..70a299cf9 --- /dev/null +++ b/explorer/frontend/lib/metadata/getPageOgType.ts @@ -0,0 +1,81 @@ +import type { Route } from 'nextjs-routes'; + +type OGPageType = 'Homepage' | 'Root page' | 'Regular page'; + +const OG_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Root page', + '/internal-txs': 'Root page', + '/txs/kettle/[hash]': 'Regular page', + '/tx/[hash]': 'Regular page', + '/blocks': 'Root page', + '/block/[height_or_hash]': 'Regular page', + '/block/countdown': 'Regular page', + '/block/countdown/[height]': 'Regular page', + '/accounts': 'Root page', + '/accounts/label/[slug]': 'Root page', + '/address/[hash]': 'Regular page', + '/verified-contracts': 'Root page', + '/contract-verification': 'Root page', + '/address/[hash]/contract-verification': 'Regular page', + '/tokens': 'Root page', + '/token/[hash]': 'Regular page', + '/token/[hash]/instance/[id]': 'Regular page', + '/apps': 'Root page', + '/apps/[id]': 'Regular page', + '/stats': 'Root page', + '/stats/[id]': 'Regular page', + '/api-docs': 'Regular page', + '/graphiql': 'Regular page', + '/search-results': 'Regular page', + '/auth/profile': 'Root page', + '/account/merits': 'Regular page', + '/account/watchlist': 'Regular page', + '/account/api-key': 'Regular page', + '/account/custom-abi': 'Regular page', + '/account/tag-address': 'Regular page', + '/account/verified-addresses': 'Root page', + '/public-tags/submit': 'Regular page', + '/withdrawals': 'Root page', + '/txn-withdrawals': 'Root page', + '/visualize/sol2uml': 'Regular page', + '/csv-export': 'Regular page', + '/deposits': 'Root page', + '/output-roots': 'Root page', + '/dispute-games': 'Root page', + '/batches': 'Root page', + '/batches/[number]': 'Regular page', + '/batches/celestia/[height]/[commitment]': 'Regular page', + '/blobs/[hash]': 'Regular page', + '/ops': 'Root page', + '/op/[hash]': 'Regular page', + '/404': 'Regular page', + '/name-domains': 'Root page', + '/name-domains/[name]': 'Regular page', + '/validators': 'Root page', + '/validators/[id]': 'Regular page', + '/gas-tracker': 'Root page', + '/mud-worlds': 'Root page', + '/token-transfers': 'Root page', + '/advanced-filter': 'Root page', + '/pools': 'Root page', + '/pools/[hash]': 'Regular page', + '/interop-messages': 'Root page', + + // service routes, added only to make typescript happy + '/login': 'Regular page', + '/sprite': 'Regular page', + '/chakra': 'Regular page', + '/api/metrics': 'Regular page', + '/api/monitoring/invalid-api-schema': 'Regular page', + '/api/log': 'Regular page', + '/api/media-type': 'Regular page', + '/api/proxy': 'Regular page', + '/api/csrf': 'Regular page', + '/api/healthz': 'Regular page', + '/api/config': 'Regular page', +}; + +export default function getPageOgType(pathname: Route['pathname']) { + return OG_TYPE_DICT[pathname]; +} diff --git a/explorer/frontend/lib/metadata/index.ts b/explorer/frontend/lib/metadata/index.ts new file mode 100644 index 000000000..903bd988e --- /dev/null +++ b/explorer/frontend/lib/metadata/index.ts @@ -0,0 +1,3 @@ +export { default as generate } from './generate'; +export { default as update } from './update'; +export * from './types'; diff --git a/explorer/frontend/lib/metadata/templates/description.ts b/explorer/frontend/lib/metadata/templates/description.ts new file mode 100644 index 000000000..b90e9e6fe --- /dev/null +++ b/explorer/frontend/lib/metadata/templates/description.ts @@ -0,0 +1,88 @@ +/* eslint-disable max-len */ +import type { Route } from 'nextjs-routes'; + +// equal og:description +const DEFAULT_TEMPLATE = 'Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the %network_title% network.'; + +// FIXME all page descriptions will be updated later +const TEMPLATE_MAP: Record = { + '/': DEFAULT_TEMPLATE, + '/txs': DEFAULT_TEMPLATE, + '/internal-txs': DEFAULT_TEMPLATE, + '/txs/kettle/[hash]': DEFAULT_TEMPLATE, + '/tx/[hash]': 'View transaction %hash% on %network_title%', + '/blocks': DEFAULT_TEMPLATE, + '/block/[height_or_hash]': 'View the transactions, token transfers, and uncles for block %height_or_hash%', + '/block/countdown': DEFAULT_TEMPLATE, + '/block/countdown/[height]': DEFAULT_TEMPLATE, + '/accounts': DEFAULT_TEMPLATE, + '/accounts/label/[slug]': DEFAULT_TEMPLATE, + '/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%', + '/verified-contracts': DEFAULT_TEMPLATE, + '/contract-verification': DEFAULT_TEMPLATE, + '/address/[hash]/contract-verification': 'View the account balance, transactions, and other data for %hash% on the %network_title%', + '/tokens': DEFAULT_TEMPLATE, + '/token/[hash]': '%hash%, balances and analytics on the %network_title%', + '/token/[hash]/instance/[id]': '%hash%, balances and analytics on the %network_title%', + '/apps': DEFAULT_TEMPLATE, + '/apps/[id]': DEFAULT_TEMPLATE, + '/stats': DEFAULT_TEMPLATE, + '/stats/[id]': DEFAULT_TEMPLATE, + '/api-docs': DEFAULT_TEMPLATE, + '/graphiql': DEFAULT_TEMPLATE, + '/search-results': DEFAULT_TEMPLATE, + '/auth/profile': DEFAULT_TEMPLATE, + '/account/merits': DEFAULT_TEMPLATE, + '/account/watchlist': DEFAULT_TEMPLATE, + '/account/api-key': DEFAULT_TEMPLATE, + '/account/custom-abi': DEFAULT_TEMPLATE, + '/account/tag-address': DEFAULT_TEMPLATE, + '/account/verified-addresses': DEFAULT_TEMPLATE, + '/public-tags/submit': 'Propose a new public tag for your address, contract or set of contracts for your dApp. Our team will review and approve your submission. Public tags are incredible tool which helps users identify contracts and addresses.', + '/withdrawals': DEFAULT_TEMPLATE, + '/txn-withdrawals': DEFAULT_TEMPLATE, + '/visualize/sol2uml': DEFAULT_TEMPLATE, + '/csv-export': DEFAULT_TEMPLATE, + '/deposits': DEFAULT_TEMPLATE, + '/output-roots': DEFAULT_TEMPLATE, + '/dispute-games': DEFAULT_TEMPLATE, + '/batches': DEFAULT_TEMPLATE, + '/batches/[number]': DEFAULT_TEMPLATE, + '/batches/celestia/[height]/[commitment]': DEFAULT_TEMPLATE, + '/blobs/[hash]': DEFAULT_TEMPLATE, + '/ops': DEFAULT_TEMPLATE, + '/op/[hash]': DEFAULT_TEMPLATE, + '/404': DEFAULT_TEMPLATE, + '/name-domains': DEFAULT_TEMPLATE, + '/name-domains/[name]': DEFAULT_TEMPLATE, + '/validators': DEFAULT_TEMPLATE, + '/validators/[id]': DEFAULT_TEMPLATE, + '/gas-tracker': 'Explore real-time %network_title% gas fees with Blockscout\'s advanced gas fee tracker. Get accurate %network_gwei% estimates and track transaction costs live.', + '/mud-worlds': DEFAULT_TEMPLATE, + '/token-transfers': DEFAULT_TEMPLATE, + '/advanced-filter': DEFAULT_TEMPLATE, + '/pools': DEFAULT_TEMPLATE, + '/pools/[hash]': DEFAULT_TEMPLATE, + '/interop-messages': DEFAULT_TEMPLATE, + + // service routes, added only to make typescript happy + '/login': DEFAULT_TEMPLATE, + '/sprite': DEFAULT_TEMPLATE, + '/chakra': DEFAULT_TEMPLATE, + '/api/metrics': DEFAULT_TEMPLATE, + '/api/monitoring/invalid-api-schema': DEFAULT_TEMPLATE, + '/api/log': DEFAULT_TEMPLATE, + '/api/media-type': DEFAULT_TEMPLATE, + '/api/proxy': DEFAULT_TEMPLATE, + '/api/csrf': DEFAULT_TEMPLATE, + '/api/healthz': DEFAULT_TEMPLATE, + '/api/config': DEFAULT_TEMPLATE, +}; + +const TEMPLATE_MAP_ENHANCED: Partial> = { + '/stats/[id]': '%description%', +}; + +export function make(pathname: Route['pathname'], isEnriched = false) { + return (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname] ?? ''; +} diff --git a/explorer/frontend/lib/metadata/templates/index.ts b/explorer/frontend/lib/metadata/templates/index.ts new file mode 100644 index 000000000..159fca123 --- /dev/null +++ b/explorer/frontend/lib/metadata/templates/index.ts @@ -0,0 +1,2 @@ +export * as title from './title'; +export * as description from './description'; diff --git a/explorer/frontend/lib/metadata/templates/title.ts b/explorer/frontend/lib/metadata/templates/title.ts new file mode 100644 index 000000000..1adfae911 --- /dev/null +++ b/explorer/frontend/lib/metadata/templates/title.ts @@ -0,0 +1,92 @@ +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const TEMPLATE_MAP: Record = { + '/': '%network_name% blockchain explorer - View %network_name% stats', + '/txs': '%network_name% transactions - %network_name% explorer', + '/internal-txs': '%network_name% internal transactions - %network_name% explorer', + '/txs/kettle/[hash]': '%network_name% kettle %hash% transactions', + '/tx/[hash]': '%network_name% transaction %hash%', + '/blocks': '%network_name% blocks', + '/block/[height_or_hash]': '%network_name% block %height_or_hash%', + '/block/countdown': '%network_name% block countdown index', + '/block/countdown/[height]': '%network_name% block %height% countdown', + '/accounts': '%network_name% top accounts', + '/accounts/label/[slug]': '%network_name% addresses search by label', + '/address/[hash]': '%network_name% address details for %hash%', + '/verified-contracts': 'Verified %network_name% contracts lookup - %network_name% explorer', + '/contract-verification': '%network_name% verify contract', + '/address/[hash]/contract-verification': '%network_name% contract verification for %hash%', + '/tokens': 'Tokens list - %network_name% explorer', + '/token/[hash]': '%network_name% token details', + '/token/[hash]/instance/[id]': '%network_name% NFT instance', + '/apps': '%network_name% DApps - Explore top apps', + '/apps/[id]': '%network_name% marketplace app', + '/stats': '%network_name% stats - %network_name% network insights', + '/stats/[id]': '%network_name% stats - %id% chart', + '/api-docs': '%network_name% API docs - %network_name% developer tools', + '/graphiql': 'GraphQL for %network_name% - %network_name% data query', + '/search-results': '%network_name% search result for %q%', + '/auth/profile': '%network_name% - my profile', + '/account/merits': '%network_name% - Merits', + '/account/watchlist': '%network_name% - watchlist', + '/account/api-key': '%network_name% - API keys', + '/account/custom-abi': '%network_name% - custom ABI', + '/account/tag-address': '%network_name% - private tags', + '/account/verified-addresses': '%network_name% - my verified addresses', + '/public-tags/submit': '%network_name% - public tag requests', + '/withdrawals': '%network_name% withdrawals - track on %network_name% explorer', + '/txn-withdrawals': '%network_name% L2 to L1 message relayer', + '/visualize/sol2uml': '%network_name% Solidity UML diagram', + '/csv-export': '%network_name% export data to CSV', + '/deposits': '%network_name% deposits (L1 > L2)', + '/output-roots': '%network_name% output roots', + '/dispute-games': '%network_name% dispute games', + '/batches': '%network_name% txn batches', + '/batches/[number]': '%network_name% L2 txn batch %number%', + '/batches/celestia/[height]/[commitment]': '%network_name% L2 txn batch %height% %commitment%', + '/blobs/[hash]': '%network_name% blob %hash% details', + '/ops': 'User operations on %network_name% - %network_name% explorer', + '/op/[hash]': '%network_name% user operation %hash%', + '/404': '%network_name% error - page not found', + '/name-domains': '%network_name% name domains - %network_name% explorer', + '/name-domains/[name]': '%network_name% %name% domain details', + '/validators': '%network_name% validators list', + '/validators/[id]': '%network_name% validator %id% details', + '/gas-tracker': 'Track %network_name% gas fees in %network_gwei%', + '/mud-worlds': '%network_name% MUD worlds list', + '/token-transfers': '%network_name% token transfers', + '/advanced-filter': '%network_name% advanced filter', + '/pools': '%network_name% DEX pools', + '/pools/[hash]': '%network_name% pool details', + '/interop-messages': '%network_name% interop messages', + + // service routes, added only to make typescript happy + '/login': '%network_name% login', + '/sprite': '%network_name% SVG sprite', + '/chakra': '%network_name% Chakra UI showcase', + '/api/metrics': '%network_name% node API prometheus metrics', + '/api/monitoring/invalid-api-schema': '%network_name% node API prometheus metrics', + '/api/log': '%network_name% node API request log', + '/api/media-type': '%network_name% node API media type', + '/api/proxy': '%network_name% node API proxy', + '/api/csrf': '%network_name% node API CSRF token', + '/api/healthz': '%network_name% node API health check', + '/api/config': '%network_name% node API app config', +}; + +const TEMPLATE_MAP_ENHANCED: Partial> = { + '/token/[hash]': '%network_name% %symbol_or_name% token details', + '/token/[hash]/instance/[id]': '%network_name% token instance for %symbol_or_name%', + '/apps/[id]': '%network_name% - %app_name%', + '/address/[hash]': '%network_name% address details for %domain_name%', + '/stats/[id]': '%title% chart on %network_name%', +}; + +export function make(pathname: Route['pathname'], isEnriched = false) { + const template = (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname]; + const postfix = config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : ''; + + return (template + postfix).trim(); +} diff --git a/explorer/frontend/lib/metadata/types.ts b/explorer/frontend/lib/metadata/types.ts new file mode 100644 index 000000000..8abe1df79 --- /dev/null +++ b/explorer/frontend/lib/metadata/types.ts @@ -0,0 +1,26 @@ +import type { LineChart } from '@blockscout/stats-types'; +import type { TokenInfo } from 'types/api/token'; + +import type { Route } from 'nextjs-routes'; + +/* eslint-disable @stylistic/indent */ +export type ApiData = +( + Pathname extends '/address/[hash]' ? { domain_name: string } : + Pathname extends '/token/[hash]' ? TokenInfo & { symbol_or_name: string } : + Pathname extends '/token/[hash]/instance/[id]' ? { symbol_or_name: string } : + Pathname extends '/apps/[id]' ? { app_name: string } : + Pathname extends '/stats/[id]' ? LineChart['info'] : + never +) | null; + +export interface Metadata { + title: string; + description: string; + opengraph: { + title: string; + description?: string; + imageUrl?: string; + }; + canonical: string | undefined; +} diff --git a/explorer/frontend/lib/metadata/update.ts b/explorer/frontend/lib/metadata/update.ts new file mode 100644 index 000000000..123e3ca10 --- /dev/null +++ b/explorer/frontend/lib/metadata/update.ts @@ -0,0 +1,13 @@ +import type { ApiData } from './types'; +import type { RouteParams } from 'nextjs/types'; + +import type { Route } from 'nextjs-routes'; + +import generate from './generate'; + +export default function update(route: RouteParams, apiData: ApiData) { + const { title, description } = generate(route, apiData); + + window.document.title = title; + window.document.querySelector('meta[name="description"]')?.setAttribute('content', description); +} diff --git a/explorer/frontend/lib/mixpanel/getPageType.ts b/explorer/frontend/lib/mixpanel/getPageType.ts new file mode 100644 index 000000000..4303c2d11 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/getPageType.ts @@ -0,0 +1,79 @@ +import type { Route } from 'nextjs-routes'; + +export const PAGE_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Transactions', + '/internal-txs': 'Internal transactions', + '/txs/kettle/[hash]': 'Kettle transactions', + '/tx/[hash]': 'Transaction details', + '/blocks': 'Blocks', + '/block/[height_or_hash]': 'Block details', + '/block/countdown': 'Block countdown search', + '/block/countdown/[height]': 'Block countdown', + '/accounts': 'Top accounts', + '/accounts/label/[slug]': 'Addresses search by label', + '/address/[hash]': 'Address details', + '/verified-contracts': 'Verified contracts', + '/contract-verification': 'Contract verification', + '/address/[hash]/contract-verification': 'Contract verification for address', + '/tokens': 'Tokens', + '/token/[hash]': 'Token details', + '/token/[hash]/instance/[id]': 'Token Instance', + '/apps': 'DApps', + '/apps/[id]': 'DApp', + '/stats': 'Stats', + '/stats/[id]': 'Stats chart', + '/api-docs': 'REST API', + '/graphiql': 'GraphQL', + '/search-results': 'Search results', + '/auth/profile': 'Profile', + '/account/merits': 'Merits', + '/account/watchlist': 'Watchlist', + '/account/api-key': 'API keys', + '/account/custom-abi': 'Custom ABI', + '/account/tag-address': 'Private tags', + '/account/verified-addresses': 'Verified addresses', + '/public-tags/submit': 'Submit public tag', + '/withdrawals': 'Withdrawals', + '/txn-withdrawals': 'Txn withdrawals', + '/visualize/sol2uml': 'Solidity UML diagram', + '/csv-export': 'Export data to CSV file', + '/deposits': 'Deposits (L1 > L2)', + '/output-roots': 'Output roots', + '/dispute-games': 'Dispute games', + '/batches': 'Txn batches', + '/batches/[number]': 'L2 txn batch details', + '/batches/celestia/[height]/[commitment]': 'L2 txn batch details', + '/blobs/[hash]': 'Blob details', + '/ops': 'User operations', + '/op/[hash]': 'User operation details', + '/404': '404', + '/name-domains': 'Domains search and resolve', + '/name-domains/[name]': 'Domain details', + '/validators': 'Validators list', + '/validators/[id]': 'Validator details', + '/gas-tracker': 'Gas tracker', + '/mud-worlds': 'MUD worlds', + '/token-transfers': 'Token transfers', + '/advanced-filter': 'Advanced filter', + '/pools': 'DEX pools', + '/pools/[hash]': 'Pool details', + '/interop-messages': 'Interop messages', + + // service routes, added only to make typescript happy + '/login': 'Login', + '/sprite': 'Sprite', + '/chakra': 'Chakra UI showcase', + '/api/metrics': 'Node API: Prometheus metrics', + '/api/monitoring/invalid-api-schema': 'Node API: Prometheus metrics', + '/api/log': 'Node API: Request log', + '/api/media-type': 'Node API: Media type', + '/api/proxy': 'Node API: Proxy', + '/api/csrf': 'Node API: CSRF token', + '/api/healthz': 'Node API: Health check', + '/api/config': 'Node API: App config', +}; + +export default function getPageType(pathname: Route['pathname']) { + return PAGE_TYPE_DICT[pathname] || 'Unknown page'; +} diff --git a/explorer/frontend/lib/mixpanel/getTabName.ts b/explorer/frontend/lib/mixpanel/getTabName.ts new file mode 100644 index 000000000..99f742a88 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/getTabName.ts @@ -0,0 +1,5 @@ +import { capitalize } from 'es-toolkit'; + +export default function getTabName(tab: string) { + return tab !== '' ? capitalize(tab.replaceAll('_', ' ')) : 'Default'; +} diff --git a/explorer/frontend/lib/mixpanel/index.ts b/explorer/frontend/lib/mixpanel/index.ts new file mode 100644 index 000000000..54928e8b6 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/index.ts @@ -0,0 +1,16 @@ +import getPageType from './getPageType'; +import logEvent from './logEvent'; +import reset from './reset'; +import useInit from './useInit'; +import useLogPageView from './useLogPageView'; +import * as userProfile from './userProfile'; +export * from './utils'; + +export { + useInit, + useLogPageView, + logEvent, + getPageType, + userProfile, + reset, +}; diff --git a/explorer/frontend/lib/mixpanel/logEvent.ts b/explorer/frontend/lib/mixpanel/logEvent.ts new file mode 100644 index 000000000..8a001351e --- /dev/null +++ b/explorer/frontend/lib/mixpanel/logEvent.ts @@ -0,0 +1,19 @@ +import mixpanel from 'mixpanel-browser'; + +import config from 'configs/app'; + +import type { EventTypes, EventPayload } from './utils'; + +type TrackFnArgs = Parameters; + +export default function logEvent( + type: EventType, + properties?: EventPayload, + optionsOrCallback?: TrackFnArgs[2], + callback?: TrackFnArgs[3], +) { + if (!config.features.mixpanel.isEnabled) { + return; + } + mixpanel.track(type, properties, optionsOrCallback, callback); +} diff --git a/explorer/frontend/lib/mixpanel/reset.ts b/explorer/frontend/lib/mixpanel/reset.ts new file mode 100644 index 000000000..489646d72 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/reset.ts @@ -0,0 +1,10 @@ +import mixpanel from 'mixpanel-browser'; + +import config from 'configs/app'; + +export default function reset() { + if (!config.features.mixpanel.isEnabled) { + return; + } + mixpanel.reset(); +} diff --git a/explorer/frontend/lib/mixpanel/useInit.tsx b/explorer/frontend/lib/mixpanel/useInit.tsx new file mode 100644 index 000000000..fd4c4bcba --- /dev/null +++ b/explorer/frontend/lib/mixpanel/useInit.tsx @@ -0,0 +1,62 @@ +import { capitalize } from 'es-toolkit'; +import type { Config } from 'mixpanel-browser'; +import mixpanel from 'mixpanel-browser'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { deviceType } from 'react-device-detect'; + +import config from 'configs/app'; +import * as cookies from 'lib/cookies'; +import dayjs from 'lib/date/dayjs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import * as userProfile from './userProfile'; + +export default function useMixpanelInit() { + const [ isInited, setIsInited ] = React.useState(false); + const router = useRouter(); + const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug)); + + React.useEffect(() => { + const feature = config.features.mixpanel; + if (!feature.isEnabled) { + return; + } + + const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG); + + const mixpanelConfig: Partial = { + debug: Boolean(debugFlagQuery.current || debugFlagCookie), + }; + const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); + + const uuid = cookies.get(cookies.NAMES.UUID); + + mixpanel.init(feature.projectToken, mixpanelConfig); + mixpanel.register({ + 'Chain id': config.chain.id, + Environment: config.app.isDev ? 'Dev' : 'Prod', + Authorized: isAuth, + 'Viewport width': window.innerWidth, + 'Viewport height': window.innerHeight, + Language: window.navigator.language, + 'Device type': capitalize(deviceType), + 'User id': uuid, + }); + mixpanel.identify(uuid); + userProfile.set({ + 'Device Type': capitalize(deviceType), + ...(isAuth ? { 'With Account': true } : {}), + }); + userProfile.setOnce({ + 'First Time Join': dayjs().toISOString(), + }); + + setIsInited(true); + if (debugFlagQuery.current && !debugFlagCookie) { + cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true'); + } + }, [ ]); + + return isInited; +} diff --git a/explorer/frontend/lib/mixpanel/useLogPageView.tsx b/explorer/frontend/lib/mixpanel/useLogPageView.tsx new file mode 100644 index 000000000..b7a09a6d6 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/useLogPageView.tsx @@ -0,0 +1,52 @@ +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; +import * as cookies from 'lib/cookies'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { COLOR_THEMES } from 'lib/settings/colorTheme'; +import { useColorMode } from 'toolkit/chakra/color-mode'; +import type { ColorMode } from 'toolkit/chakra/color-mode'; + +import getPageType from './getPageType'; +import getTabName from './getTabName'; +import logEvent from './logEvent'; +import { EventTypes } from './utils'; + +function getColorTheme(hex: string | undefined, colorMode: ColorMode) { + const colorTheme = COLOR_THEMES.find((theme) => theme.hex === hex) || + COLOR_THEMES.filter((theme) => theme.colorMode === colorMode).slice(-1)[0]; + + return colorTheme.id; +} + +export default function useLogPageView(isInited: boolean) { + const router = useRouter(); + const pathname = usePathname(); + + const tab = getQueryParamString(router.query.tab); + const page = getQueryParamString(router.query.page); + const { colorMode } = useColorMode(); + + React.useEffect(() => { + if (!config.features.mixpanel.isEnabled || !isInited) { + return; + } + + const cookieColorModeHex = cookies.get(cookies.NAMES.COLOR_MODE_HEX); + + logEvent(EventTypes.PAGE_VIEW, { + 'Page type': getPageType(router.pathname), + Tab: getTabName(tab), + Page: page || undefined, + 'Color mode': colorMode, + 'Color theme': getColorTheme(cookieColorModeHex, colorMode), + }); + // these are only deps that should trigger the effect + // in some scenarios page type is not changing (e.g navigation from one address page to another), + // but we still want to log page view + // so we use pathname from 'next/navigation' instead of router.pathname from 'next/router' as deps + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isInited, page, pathname, tab, colorMode ]); +} diff --git a/explorer/frontend/lib/mixpanel/userProfile.ts b/explorer/frontend/lib/mixpanel/userProfile.ts new file mode 100644 index 000000000..774d12472 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/userProfile.ts @@ -0,0 +1,24 @@ +import mixpanel from 'mixpanel-browser'; + +import type { PickByType } from 'types/utils'; + +interface UserProfileProperties { + 'With Account': boolean; + 'With Connected Wallet': boolean; + 'Device Type': string; + 'First Time Join': string; +} + +type UserProfilePropertiesNumerable = PickByType; + +export function set(props: Partial) { + mixpanel.people.set(props); +} + +export function setOnce(props: Partial) { + mixpanel.people.set_once(props); +} + +export function increment(props: UserProfilePropertiesNumerable) { + mixpanel.people.increment(props); +} diff --git a/explorer/frontend/lib/mixpanel/utils.ts b/explorer/frontend/lib/mixpanel/utils.ts new file mode 100644 index 000000000..42fa625c2 --- /dev/null +++ b/explorer/frontend/lib/mixpanel/utils.ts @@ -0,0 +1,173 @@ +import type { WalletType } from 'types/client/wallets'; +import type { ColorThemeId } from 'types/settings'; + +export enum EventTypes { + PAGE_VIEW = 'Page view', + SEARCH_QUERY = 'Search query', + LOCAL_SEARCH = 'Local search', + ADD_TO_WALLET = 'Add to wallet', + ACCOUNT_ACCESS = 'Account access', + LOGIN = 'Login', + ACCOUNT_LINK_INFO = 'Account link info', + PRIVATE_TAG = 'Private tag', + VERIFY_ADDRESS = 'Verify address', + VERIFY_TOKEN = 'Verify token', + WALLET_CONNECT = 'Wallet connect', + WALLET_ACTION = 'Wallet action', + CONTRACT_INTERACTION = 'Contract interaction', + CONTRACT_VERIFICATION = 'Contract verification', + QR_CODE = 'QR code', + PAGE_WIDGET = 'Page widget', + TX_INTERPRETATION_INTERACTION = 'Transaction interpretation interaction', + EXPERIMENT_STARTED = 'Experiment started', + FILTERS = 'Filters', + BUTTON_CLICK = 'Button click', + PROMO_BANNER = 'Promo banner', + APP_FEEDBACK = 'App feedback', +} + +/* eslint-disable @stylistic/indent */ +export type EventPayload = +Type extends EventTypes.PAGE_VIEW ? +{ + 'Page type': string; + Tab: string; + Page?: string; + 'Color mode': 'light' | 'dark'; + 'Color theme': ColorThemeId | undefined; +} : +Type extends EventTypes.SEARCH_QUERY ? { + 'Search query': string; + 'Source page type': string; + 'Result URL': string; +} : +Type extends EventTypes.LOCAL_SEARCH ? { + 'Search query': string; + Source: 'Marketplace'; +} : +Type extends EventTypes.ADD_TO_WALLET ? ( + { + Wallet: WalletType; + Target: 'network'; + } | { + Wallet: WalletType; + Target: 'token'; + Token: string; + } +) : +Type extends EventTypes.ACCOUNT_ACCESS ? { + Action: 'Dropdown open' | 'Logged out'; +} : +Type extends EventTypes.LOGIN ? ( + { + Action: 'Started'; + Source: string; + } | { + Action: 'Wallet' | 'Email'; + Source: 'Options selector'; + } | { + Action: 'OTP sent'; + Source: 'Email'; + } | { + Action: 'Success'; + Source: 'Email' | 'Wallet'; + } +) : +Type extends EventTypes.ACCOUNT_LINK_INFO ? { + Source: 'Profile' | 'Login modal' | 'Profile dropdown' | 'Merits'; + Status: 'Started' | 'OTP sent' | 'Finished'; + Type: 'Email' | 'Wallet'; +} : +Type extends EventTypes.PRIVATE_TAG ? { + Action: 'Form opened' | 'Submit'; + 'Page type': string; + 'Tag type': 'Address' | 'Tx'; +} : +Type extends EventTypes.VERIFY_ADDRESS ? ( + { + Action: 'Form opened' | 'Address entered'; + 'Page type': string; + } | { + Action: 'Sign ownership'; + 'Page type': string; + 'Sign method': 'wallet' | 'manual'; + } +) : +Type extends EventTypes.VERIFY_TOKEN ? { + Action: 'Form opened' | 'Submit'; +} : +Type extends EventTypes.WALLET_CONNECT ? { + Source: 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button' | 'Merits'; + Status: 'Started' | 'Connected'; +} : +Type extends EventTypes.WALLET_ACTION ? ( + { + Action: 'Open' | 'Address click'; + } | { + Action: 'Send Transaction' | 'Sign Message' | 'Sign Typed Data'; + Address: string | undefined; + AppId: string; + } +) : +Type extends EventTypes.CONTRACT_INTERACTION ? { + 'Method type': 'Read' | 'Write'; + 'Method name': string; +} : +Type extends EventTypes.CONTRACT_VERIFICATION ? { + Method: string; + Status: 'Method selected' | 'Finished'; +} : +Type extends EventTypes.QR_CODE ? { + 'Page type': string; +} : +Type extends EventTypes.PAGE_WIDGET ? ( + { + Type: 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; + } | { + Type: 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts'; + Info: string; + Source: 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup' | 'Banner'; + } | { + Type: 'Security score'; + Source: 'Analyzed contracts popup'; + } | { + Type: 'Action button'; + Info: string; + Source: 'Txn' | 'NFT collection' | 'NFT item'; + } | { + Type: 'Address tag'; + Info: string; + URL: string; + } | { + Type: 'Share chart'; + Info: string; + } +) : +Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { + Type: 'Address click' | 'Token click' | 'Domain click'; +} : +Type extends EventTypes.EXPERIMENT_STARTED ? { + 'Experiment name': string; + 'Variant name': string; + Source: 'growthbook'; +} : +Type extends EventTypes.FILTERS ? { + Source: 'Marketplace'; + 'Filter name': string; +} : +Type extends EventTypes.BUTTON_CLICK ? { + Content: string; + Source: string; +} : +Type extends EventTypes.PROMO_BANNER ? { + Source: 'Marketplace'; + Link: string; +} : +Type extends EventTypes.APP_FEEDBACK ? { + Action: 'Rating'; + Source: 'Discovery' | 'App modal' | 'App page'; + AppId: string; + Score: number; +} : +undefined; +/* eslint-enable @stylistic/indent */ diff --git a/explorer/frontend/lib/monitoring/metrics.ts b/explorer/frontend/lib/monitoring/metrics.ts new file mode 100644 index 000000000..05bdad660 --- /dev/null +++ b/explorer/frontend/lib/monitoring/metrics.ts @@ -0,0 +1,39 @@ +import * as promClient from 'prom-client'; + +const metrics = (() => { + // eslint-disable-next-line no-restricted-properties + if (process.env.PROMETHEUS_METRICS_ENABLED !== 'true') { + return; + } + + promClient.register.clear(); + + const invalidApiSchema = new promClient.Counter({ + name: 'invalid_api_schema', + help: 'Number of invalid external API schema events', + labelNames: [ 'resource', 'url' ] as const, + }); + + const socialPreviewBotRequests = new promClient.Counter({ + name: 'social_preview_bot_requests_total', + help: 'Number of incoming requests from social preview bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const searchEngineBotRequests = new promClient.Counter({ + name: 'search_engine_bot_requests_total', + help: 'Number of incoming requests from search engine bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const apiRequestDuration = new promClient.Histogram({ + name: 'api_request_duration_seconds', + help: 'Duration of requests to API in seconds', + labelNames: [ 'route', 'code' ], + buckets: [ 0.2, 0.5, 1, 3, 10 ], + }); + + return { invalidApiSchema, socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration }; +})(); + +export default metrics; diff --git a/explorer/frontend/lib/networks/getNetworkTitle.ts b/explorer/frontend/lib/networks/getNetworkTitle.ts new file mode 100644 index 000000000..5bcb602e3 --- /dev/null +++ b/explorer/frontend/lib/networks/getNetworkTitle.ts @@ -0,0 +1,6 @@ +import config from 'configs/app'; + +// TODO delete when page descriptions is refactored +export default function getNetworkTitle() { + return config.chain.name + (config.chain.shortName ? ` (${ config.chain.shortName })` : '') + ' Explorer'; +} diff --git a/explorer/frontend/lib/networks/getNetworkValidationActionText.ts b/explorer/frontend/lib/networks/getNetworkValidationActionText.ts new file mode 100644 index 000000000..c6a36f1a3 --- /dev/null +++ b/explorer/frontend/lib/networks/getNetworkValidationActionText.ts @@ -0,0 +1,21 @@ +import config from 'configs/app'; + +export default function getNetworkValidationActionText() { + switch (config.chain.verificationType) { + case 'validation': { + return 'validated'; + } + case 'mining': { + return 'mined'; + } + case 'posting': { + return 'posted'; + } + case 'sequencing': { + return 'sequenced'; + } + default: { + return 'miner'; + } + } +} diff --git a/explorer/frontend/lib/networks/getNetworkValidatorTitle.ts b/explorer/frontend/lib/networks/getNetworkValidatorTitle.ts new file mode 100644 index 000000000..a0c7977cd --- /dev/null +++ b/explorer/frontend/lib/networks/getNetworkValidatorTitle.ts @@ -0,0 +1,21 @@ +import config from 'configs/app'; + +export default function getNetworkValidatorTitle() { + switch (config.chain.verificationType) { + case 'validation': { + return 'validator'; + } + case 'mining': { + return 'miner'; + } + case 'posting': { + return 'poster'; + } + case 'sequencing': { + return 'sequencer'; + } + default: { + return 'miner'; + } + } +} diff --git a/explorer/frontend/lib/networks/networkExplorers.ts b/explorer/frontend/lib/networks/networkExplorers.ts new file mode 100644 index 000000000..b5a4648d4 --- /dev/null +++ b/explorer/frontend/lib/networks/networkExplorers.ts @@ -0,0 +1,38 @@ +import { mapValues } from 'es-toolkit'; + +import type { NetworkExplorer } from 'types/networks'; + +import config from 'configs/app'; + +// for easy .env update +// const NETWORK_EXPLORERS = JSON.stringify([ +// { +// title: 'Anyblock', +// baseUrl: 'https://explorer.anyblock.tools', +// paths: { +// tx: '/ethereum/ethereum/goerli/transaction', +// address: '/ethereum/ethereum/goerli/address' +// }, +// }, +// { +// title: 'Etherscan', +// baseUrl: 'https://goerli.etherscan.io/', +// paths: { +// tx: '/tx', +// address: '/address', +// }, +// }, +// ]).replaceAll('"', '\''); + +const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str; +const addLeadingSlash = (str: string) => str[0] === '/' ? str : '/' + str; + +const networkExplorers: Array = (() => { + return config.UI.explorers.items.map((explorer) => ({ + ...explorer, + baseUrl: stripTrailingSlash(explorer.baseUrl), + paths: mapValues(explorer.paths, (value) => value ? stripTrailingSlash(addLeadingSlash(value)) : value), + })); +})(); + +export default networkExplorers; diff --git a/explorer/frontend/lib/pools/getPoolLinks.ts b/explorer/frontend/lib/pools/getPoolLinks.ts new file mode 100644 index 000000000..19d738f01 --- /dev/null +++ b/explorer/frontend/lib/pools/getPoolLinks.ts @@ -0,0 +1,21 @@ +import type { Pool } from 'types/api/pools'; + +type PoolLink = { + url: string; + image: string; + title: string; +}; + +export default function getPoolLinks(pool?: Pool): Array { + if (!pool) { + return []; + } + + return [ + { + url: pool.coin_gecko_terminal_url, + image: '/static/gecko_terminal.png', + title: 'GeckoTerminal', + }, + ].filter(link => Boolean(link.url)); +} diff --git a/explorer/frontend/lib/pools/getPoolTitle.ts b/explorer/frontend/lib/pools/getPoolTitle.ts new file mode 100644 index 000000000..1e4db61ac --- /dev/null +++ b/explorer/frontend/lib/pools/getPoolTitle.ts @@ -0,0 +1,5 @@ +import type { Pool } from 'types/api/pools'; + +export const getPoolTitle = (pool: Pool) => { + return `${ pool.base_token_symbol } / ${ pool.quote_token_symbol } ${ pool.fee ? `(${ pool.fee }%)` : '' }`; +}; diff --git a/explorer/frontend/lib/recentSearchKeywords.ts b/explorer/frontend/lib/recentSearchKeywords.ts new file mode 100644 index 000000000..c6ae761bd --- /dev/null +++ b/explorer/frontend/lib/recentSearchKeywords.ts @@ -0,0 +1,56 @@ +import { uniq } from 'es-toolkit'; + +import { isBrowser } from 'toolkit/utils/isBrowser'; + +const RECENT_KEYWORDS_LS_KEY = 'recent_search_keywords'; +const MAX_KEYWORDS_NUMBER = 10; + +const parseKeywordsArray = (keywordsStr: string) => { + if (!keywordsStr) { + return []; + } + + try { + const parsedResult = JSON.parse(keywordsStr); + if (Array.isArray(parsedResult)) { + return parsedResult; + } + return []; + } catch (error) { + return []; + } +}; + +export function saveToRecentKeywords(value: string) { + if (!value) { + return; + } + + const keywordsArr = getRecentSearchKeywords(); + const result = uniq([ value, ...keywordsArr ]).slice(0, MAX_KEYWORDS_NUMBER - 1); + window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result)); +} + +export function getRecentSearchKeywords(input?: string) { + if (!isBrowser()) { + return []; + } + const keywordsStr = window.localStorage.getItem(RECENT_KEYWORDS_LS_KEY) || ''; + const keywordsArr = parseKeywordsArray(keywordsStr) as Array; + if (!input) { + return keywordsArr; + } + + return keywordsArr.filter(kw => kw.includes(input)); +} + +export function removeRecentSearchKeyword(value: string) { + + const keywordsArr = getRecentSearchKeywords(); + const result = keywordsArr.filter(kw => kw !== value); + window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result)); +} + +export function clearRecentSearchKeywords() { + window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, ''); +} diff --git a/explorer/frontend/lib/rollbar/index.tsx b/explorer/frontend/lib/rollbar/index.tsx new file mode 100644 index 000000000..6019cc829 --- /dev/null +++ b/explorer/frontend/lib/rollbar/index.tsx @@ -0,0 +1,72 @@ +import { Provider as DefaultProvider, useRollbar as useRollbarDefault } from '@rollbar/react'; +import type React from 'react'; +import type { Configuration } from 'rollbar'; + +import config from 'configs/app'; +import { ABSENT_PARAM_ERROR_MESSAGE } from 'lib/errors/throwOnAbsentParamError'; +import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError'; + +import { isBot, isHeadlessBrowser, isNextJsChunkError, getRequestInfo, getExceptionClass } from './utils'; + +const feature = config.features.rollbar; + +const FallbackProvider = ({ children }: { children: React.ReactNode }) => children; + +const useRollbarFallback = (): undefined => {}; + +export const Provider = feature.isEnabled ? DefaultProvider : FallbackProvider; +export const useRollbar = feature.isEnabled ? useRollbarDefault : useRollbarFallback; + +export const clientConfig: Configuration | undefined = feature.isEnabled ? { + accessToken: feature.clientToken, + environment: feature.environment, + payload: { + code_version: feature.codeVersion, + app_instance: feature.instance, + }, + checkIgnore(isUncaught, args, item) { + if (isBot(window.navigator.userAgent)) { + return true; + } + + if (isHeadlessBrowser(window.navigator.userAgent)) { + return true; + } + + if (isNextJsChunkError(getRequestInfo(item)?.url)) { + return true; + } + + const exceptionClass = getExceptionClass(item); + const IGNORED_EXCEPTION_CLASSES = [ + // these are React errors - "NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node." + // they could be caused by browser extensions + // one of the examples - https://github.com/facebook/react/issues/11538 + // we can ignore them for now + 'NotFoundError', + ]; + + if (exceptionClass && IGNORED_EXCEPTION_CLASSES.includes(exceptionClass)) { + return true; + } + + return false; + }, + hostSafeList: [ config.app.host ].filter(Boolean), + ignoredMessages: [ + // these are errors that we throw on when make a call to the API + RESOURCE_LOAD_ERROR_MESSAGE, + ABSENT_PARAM_ERROR_MESSAGE, + + // Filter out network-related errors that are usually not actionable + 'Network Error', + 'Failed to fetch', + + // Filter out CORS errors from third-party extensions + 'cross-origin', + + // Filter out client-side navigation cancellations + 'cancelled navigation', + ], + maxItems: 10, // Max items per page load +} : undefined; diff --git a/explorer/frontend/lib/rollbar/utils.ts b/explorer/frontend/lib/rollbar/utils.ts new file mode 100644 index 000000000..44ceca715 --- /dev/null +++ b/explorer/frontend/lib/rollbar/utils.ts @@ -0,0 +1,65 @@ +import { get } from 'es-toolkit/compat'; +import type { Dictionary } from 'rollbar'; + +export function isBot(userAgent: string | undefined) { + if (!userAgent) return false; + + const botPatterns = [ + 'Googlebot', // Google + 'Baiduspider', // Baidu + 'bingbot', // Bing + 'YandexBot', // Yandex + 'DuckDuckBot', // DuckDuckGo + 'Slurp', // Yahoo + 'Applebot', // Apple + 'facebookexternalhit', // Facebook + 'Twitterbot', // Twitter + 'rogerbot', // Moz + 'Alexa', // Alexa + 'AhrefsBot', // Ahrefs + 'SemrushBot', // Semrush + 'spider', // Generic spiders + 'crawler', // Generic crawlers + ]; + + return botPatterns.some(pattern => + userAgent.toLowerCase().includes(pattern.toLowerCase()), + ); +} + +export function isHeadlessBrowser(userAgent: string | undefined) { + if (!userAgent) return false; + + if ( + userAgent.includes('headless') || + userAgent.includes('phantomjs') || + userAgent.includes('selenium') || + userAgent.includes('puppeteer') + ) { + return true; + } +} + +export function isNextJsChunkError(url: unknown) { + if (typeof url !== 'string') return false; + return url.includes('/_next/'); +} + +export function getRequestInfo(item: Dictionary): { url: string } | undefined { + if ( + !item.request || + item.request === null || + typeof item.request !== 'object' || + !('url' in item.request) || + typeof item.request.url !== 'string' + ) { + return undefined; + } + return { url: item.request.url }; +} + +export function getExceptionClass(item: Dictionary) { + const exceptionClass = get(item, 'body.trace.exception.class'); + + return typeof exceptionClass === 'string' ? exceptionClass : undefined; +} diff --git a/explorer/frontend/lib/rollups/arbitrum.ts b/explorer/frontend/lib/rollups/arbitrum.ts new file mode 100644 index 000000000..37ac9b856 --- /dev/null +++ b/explorer/frontend/lib/rollups/arbitrum.ts @@ -0,0 +1,43 @@ +import { ARBITRUM_L2_TX_BATCH_STATUSES, type ArbitrumBatchStatus, type ArbitrumL2TxData } from 'types/api/arbitrumL2'; + +import config from 'configs/app'; + +const rollupFeature = config.features.rollup; + +type Args = { + status: ArbitrumBatchStatus; + commitment_transaction: ArbitrumL2TxData; + confirmation_transaction: ArbitrumL2TxData; +}; + +const parentChainName = rollupFeature.isEnabled ? rollupFeature.parentChain.name : undefined; + +export const VERIFICATION_STEPS_MAP: Record = { + 'Processed on rollup': 'Processed on rollup', + 'Sent to base': parentChainName ? `Sent to ${ parentChainName }` : 'Sent to parent chain', + 'Confirmed on base': parentChainName ? + `Confirmed on ${ parentChainName }` : + 'Confirmed on parent chain', +}; + +export const verificationSteps = (() => { + return ARBITRUM_L2_TX_BATCH_STATUSES.map((status) => VERIFICATION_STEPS_MAP[status]); +})(); + +export function getVerificationStepStatus({ + 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/explorer/frontend/lib/router/getQueryParamString.ts b/explorer/frontend/lib/router/getQueryParamString.ts new file mode 100644 index 000000000..a34f2a45c --- /dev/null +++ b/explorer/frontend/lib/router/getQueryParamString.ts @@ -0,0 +1,7 @@ +export default function getQueryParamString(param: string | Array | undefined): string { + if (Array.isArray(param)) { + return param.join(','); + } + + return param || ''; +} diff --git a/explorer/frontend/lib/router/removeQueryParam.ts b/explorer/frontend/lib/router/removeQueryParam.ts new file mode 100644 index 000000000..d0a37975c --- /dev/null +++ b/explorer/frontend/lib/router/removeQueryParam.ts @@ -0,0 +1,12 @@ +import type { NextRouter } from 'next/router'; + +export default function removeQueryParam(router: NextRouter, param: string) { + const { pathname, query, asPath } = router; + const newQuery = { ...query }; + delete newQuery[param]; + + const hashIndex = asPath.indexOf('#'); + const hash = hashIndex !== -1 ? asPath.substring(hashIndex) : ''; + + router.replace({ pathname, query: newQuery, hash }, undefined, { shallow: true }); +} diff --git a/explorer/frontend/lib/router/updateQueryParam.ts b/explorer/frontend/lib/router/updateQueryParam.ts new file mode 100644 index 000000000..1dfb1f2a6 --- /dev/null +++ b/explorer/frontend/lib/router/updateQueryParam.ts @@ -0,0 +1,12 @@ +import type { NextRouter } from 'next/router'; + +export default function updateQueryParam(router: NextRouter, param: string, newValue: string) { + const { pathname, query, asPath } = router; + const newQuery = { ...query }; + newQuery[param] = newValue; + + const hashIndex = asPath.indexOf('#'); + const hash = hashIndex !== -1 ? asPath.substring(hashIndex) : ''; + + router.replace({ pathname, query: newQuery, hash }, undefined, { shallow: true }); +} diff --git a/explorer/frontend/lib/saveAsCSV.ts b/explorer/frontend/lib/saveAsCSV.ts new file mode 100644 index 000000000..0a04b4dda --- /dev/null +++ b/explorer/frontend/lib/saveAsCSV.ts @@ -0,0 +1,14 @@ +import { unparse } from 'papaparse'; + +import downloadBlob from 'lib/downloadBlob'; + +export default function saveAsCSV(headerRows: Array, dataRows: Array>, filename: string) { + const csv = unparse([ + headerRows, + ...dataRows, + ]); + + const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' }); + + downloadBlob(blob, filename); +} diff --git a/explorer/frontend/lib/setLocale.ts b/explorer/frontend/lib/setLocale.ts new file mode 100644 index 000000000..e8a1db107 --- /dev/null +++ b/explorer/frontend/lib/setLocale.ts @@ -0,0 +1,6 @@ +const old = Number.prototype.toLocaleString; +Number.prototype.toLocaleString = function(locale, ...args) { + return old.call(this, 'en', ...args); +}; + +export {}; diff --git a/explorer/frontend/lib/settings/colorTheme.ts b/explorer/frontend/lib/settings/colorTheme.ts new file mode 100644 index 000000000..73245b037 --- /dev/null +++ b/explorer/frontend/lib/settings/colorTheme.ts @@ -0,0 +1,42 @@ +import type { ColorThemeId } from 'types/settings'; + +import type { ColorMode } from 'toolkit/chakra/color-mode'; + +export interface ColorTheme { + id: ColorThemeId; + label: string; + colorMode: ColorMode; + hex: string; + sampleBg: string; +} + +export const COLOR_THEMES: Array = [ + { + id: 'light', + label: 'Light', + colorMode: 'light', + hex: '#FFFFFF', + sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)', + }, + { + id: 'dim', + label: 'Dim', + colorMode: 'dark', + hex: '#232B37', + sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)', + }, + { + id: 'midnight', + label: 'Midnight', + colorMode: 'dark', + hex: '#1B2E48', + sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)', + }, + { + id: 'dark', + label: 'Dark', + colorMode: 'dark', + hex: '#101112', + sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)', + }, +]; diff --git a/explorer/frontend/lib/settings/identIcon.ts b/explorer/frontend/lib/settings/identIcon.ts new file mode 100644 index 000000000..1c26b6ef0 --- /dev/null +++ b/explorer/frontend/lib/settings/identIcon.ts @@ -0,0 +1,29 @@ +import type { IdenticonType } from 'types/views/address'; + +export const IDENTICONS: Array<{ label: string; id: IdenticonType; sampleBg: string }> = [ + { + label: 'GitHub', + id: 'github', + sampleBg: 'url("/static/identicon_logos/github.png") center / contain no-repeat', + }, + { + label: 'Metamask jazzicon', + id: 'jazzicon', + sampleBg: 'url("/static/identicon_logos/jazzicon.png") center / contain no-repeat', + }, + { + label: 'Ethereum blockies', + id: 'blockie', + sampleBg: 'url("/static/identicon_logos/blockies.png") center / contain no-repeat', + }, + { + label: 'Gradient avatar', + id: 'gradient_avatar', + sampleBg: 'url("/static/identicon_logos/gradient_avatar.png") center / contain no-repeat', + }, + { + label: 'Nouns', + id: 'nouns', + sampleBg: 'url("/static/identicon_logos/nouns.svg") center / contain no-repeat', + }, +]; diff --git a/explorer/frontend/lib/shortenString.ts b/explorer/frontend/lib/shortenString.ts new file mode 100644 index 000000000..4125ec06f --- /dev/null +++ b/explorer/frontend/lib/shortenString.ts @@ -0,0 +1,11 @@ +export default function shortenString(string: string | null, charNumber: number | undefined = 8) { + if (!string) { + return ''; + } + + if (string.length <= charNumber) { + return string; + } + + return string.slice(0, charNumber - 4) + '...' + string.slice(-4); +} diff --git a/explorer/frontend/lib/socket/context.tsx b/explorer/frontend/lib/socket/context.tsx new file mode 100644 index 000000000..55c362031 --- /dev/null +++ b/explorer/frontend/lib/socket/context.tsx @@ -0,0 +1,45 @@ +// https://hexdocs.pm/phoenix/js/ +import type { SocketConnectOption } from 'phoenix'; +import { Socket } from 'phoenix'; +import React, { useEffect, useState } from 'react'; + +export const SocketContext = React.createContext(null); + +interface SocketProviderProps { + children: React.ReactNode; + url?: string; + options?: Partial; +} + +export function SocketProvider({ children, options, url }: SocketProviderProps) { + const [ socket, setSocket ] = useState(null); + + useEffect(() => { + if (!url) { + return; + } + + const socketInstance = new Socket(url, options); + socketInstance.connect(); + setSocket(socketInstance); + + return () => { + socketInstance.disconnect(); + setSocket(null); + }; + }, [ options, url ]); + + return ( + + { children } + + ); +} + +export function useSocket() { + const context = React.useContext(SocketContext); + if (context === undefined) { + throw new Error('useSocket must be used within a SocketProvider'); + } + return context; +} diff --git a/explorer/frontend/lib/socket/types.ts b/explorer/frontend/lib/socket/types.ts new file mode 100644 index 000000000..5674dd144 --- /dev/null +++ b/explorer/frontend/lib/socket/types.ts @@ -0,0 +1,82 @@ +import type { Channel } from 'phoenix'; + +import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address'; +import type { NewArbitrumBatchSocketResponse } from 'types/api/arbitrumL2'; +import type { NewBlockSocketResponse } from 'types/api/block'; +import type { SmartContractVerificationResponse } from 'types/api/contract'; +import type { RawTracesResponse } from 'types/api/rawTrace'; +import type { TokenInstanceMetadataSocketMessage } from 'types/api/token'; +import type { TokenTransfer } from 'types/api/tokenTransfer'; +import type { Transaction } from 'types/api/transaction'; +import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2'; + +export type SocketMessageParams = SocketMessage.NewBlock | +SocketMessage.BlocksIndexStatus | +SocketMessage.InternalTxsIndexStatus | +SocketMessage.TxStatusUpdate | +SocketMessage.TxRawTrace | +SocketMessage.NewTx | +SocketMessage.NewPendingTx | +SocketMessage.NewOptimisticDeposits | +SocketMessage.NewArbitrumDeposits | +SocketMessage.AddressBalance | +SocketMessage.AddressCurrentCoinBalance | +SocketMessage.AddressTokenBalance | +SocketMessage.AddressTokenBalancesErc20 | +SocketMessage.AddressTokenBalancesErc721 | +SocketMessage.AddressTokenBalancesErc1155 | +SocketMessage.AddressTokenBalancesErc404 | +SocketMessage.AddressCoinBalance | +SocketMessage.AddressTxs | +SocketMessage.AddressTxsPending | +SocketMessage.AddressTokenTransfer | +SocketMessage.AddressChangedBytecode | +SocketMessage.AddressFetchedBytecode | +SocketMessage.SmartContractWasVerified | +SocketMessage.TokenTransfers | +SocketMessage.TokenTotalSupply | +SocketMessage.TokenInstanceMetadataFetched | +SocketMessage.ContractVerification | +SocketMessage.NewZkEvmL2Batch | +SocketMessage.NewArbitrumL2Batch | +SocketMessage.Unknown; + +interface SocketMessageParamsGeneric { + channel: Channel | undefined; + event: Event; + handler: (payload: Payload) => void; +} + +export namespace SocketMessage { + export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; + export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', { finished: boolean; ratio: string }>; + export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'index_status', { finished: boolean; ratio: string }>; + export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; + export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>; + export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; + export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; + export type NewOptimisticDeposits = SocketMessageParamsGeneric<'new_optimism_deposits', { deposits: number }>; + export type NewArbitrumDeposits = SocketMessageParamsGeneric<'new_messages_to_rollup_amount', { new_messages_to_rollup_amount: number }>; + export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>; + export type AddressCurrentCoinBalance = + SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; + export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; + export type AddressTokenBalancesErc20 = SocketMessageParamsGeneric<'updated_token_balances_erc_20', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc721 = SocketMessageParamsGeneric<'updated_token_balances_erc_721', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc1155 = SocketMessageParamsGeneric<'updated_token_balances_erc_1155', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc404 = SocketMessageParamsGeneric<'updated_token_balances_erc_404', AddressTokensBalancesSocketMessage>; + export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>; + export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array }>; + export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array }>; + export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array }>; + export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record>; + export type AddressFetchedBytecode = SocketMessageParamsGeneric<'fetched_bytecode', { fetched_bytecode: string }>; + export type SmartContractWasVerified = SocketMessageParamsGeneric<'smart_contract_was_verified', Record>; + export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', { token_transfer: number }>; + export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', { total_supply: number }>; + export type TokenInstanceMetadataFetched = SocketMessageParamsGeneric<'fetched_token_instance_metadata', TokenInstanceMetadataSocketMessage>; + export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>; + export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>; + export type NewArbitrumL2Batch = SocketMessageParamsGeneric<'new_arbitrum_batch', NewArbitrumBatchSocketResponse>; + export type Unknown = SocketMessageParamsGeneric; +} diff --git a/explorer/frontend/lib/socket/useSocketChannel.tsx b/explorer/frontend/lib/socket/useSocketChannel.tsx new file mode 100644 index 000000000..17f9ec3b0 --- /dev/null +++ b/explorer/frontend/lib/socket/useSocketChannel.tsx @@ -0,0 +1,85 @@ +import type { Channel } from 'phoenix'; +import { useEffect, useRef, useState } from 'react'; + +import { useSocket } from './context'; + +const CHANNEL_REGISTRY: Record = {}; + +interface Params { + topic: string | undefined; + params?: object; + isDisabled: boolean; + onJoin?: (channel: Channel, message: unknown) => void; + onSocketClose?: () => void; + onSocketError?: () => void; +} + +export default function useSocketChannel({ topic, params, isDisabled, onJoin, onSocketClose, onSocketError }: Params) { + const socket = useSocket(); + const [ channel, setChannel ] = useState(); + const onCloseRef = useRef(); + const onErrorRef = useRef(); + + const onJoinRef = useRef(onJoin); + onJoinRef.current = onJoin; + + useEffect(() => { + const cleanUpRefs = () => { + const refs = [ onCloseRef.current, onErrorRef.current ].filter(Boolean); + refs.length > 0 && socket?.off(refs); + }; + + if (!isDisabled) { + onCloseRef.current = onSocketClose && socket?.onClose(onSocketClose); + onErrorRef.current = onSocketError && socket?.onError(onSocketError); + } else { + cleanUpRefs(); + } + + return cleanUpRefs; + }, [ onSocketClose, onSocketError, socket, isDisabled ]); + + useEffect(() => { + if (isDisabled && channel) { + channel.leave(); + setChannel(undefined); + } + }, [ channel, isDisabled ]); + + useEffect(() => { + if (socket === null || isDisabled || !topic) { + return; + } + + let ch: Channel; + if (CHANNEL_REGISTRY[topic]) { + ch = CHANNEL_REGISTRY[topic].channel; + CHANNEL_REGISTRY[topic].subscribers++; + onJoinRef.current?.(ch, ''); + } else { + ch = socket.channel(topic); + CHANNEL_REGISTRY[topic] = { channel: ch, subscribers: 1 }; + ch.join() + .receive('ok', (message) => onJoinRef.current?.(ch, message)) + .receive('error', () => { + onSocketError?.(); + }); + } + + setChannel(ch); + + return () => { + if (CHANNEL_REGISTRY[topic]) { + CHANNEL_REGISTRY[topic].subscribers > 0 && CHANNEL_REGISTRY[topic].subscribers--; + if (CHANNEL_REGISTRY[topic].subscribers === 0) { + ch.leave(); + delete CHANNEL_REGISTRY[topic]; + } + } + + setChannel(undefined); + }; + }, [ socket, topic, params, isDisabled, onSocketError ]); + + return channel; +} diff --git a/explorer/frontend/lib/socket/useSocketMessage.tsx b/explorer/frontend/lib/socket/useSocketMessage.tsx new file mode 100644 index 000000000..fc3e2a96b --- /dev/null +++ b/explorer/frontend/lib/socket/useSocketMessage.tsx @@ -0,0 +1,22 @@ +import { useEffect, useRef } from 'react'; + +import type { SocketMessageParams } from 'lib/socket/types'; + +export default function useSocketMessage({ channel, event, handler }: SocketMessageParams) { + const handlerRef = useRef(handler); + handlerRef.current = handler; + + useEffect(() => { + if (channel === undefined || event === undefined) { + return; + } + + const ref = channel.on(event, (message) => { + handlerRef.current?.(message); + }); + + return () => { + channel.off(event, ref); + }; + }, [ channel, event ]); +} diff --git a/explorer/frontend/lib/solidityScan/schema.ts b/explorer/frontend/lib/solidityScan/schema.ts new file mode 100644 index 000000000..04ade657b --- /dev/null +++ b/explorer/frontend/lib/solidityScan/schema.ts @@ -0,0 +1,25 @@ +import * as v from 'valibot'; + +export const SolidityScanIssueSeverityDistributionSchema = v.object({ + critical: v.number(), + gas: v.number(), + high: v.number(), + informational: v.number(), + low: v.number(), + medium: v.number(), +}); + +export const SolidityScanSchema = v.object({ + scan_report: v.object({ + contractname: v.string(), + scan_status: v.string(), + scan_summary: v.object({ + score_v2: v.string(), + issue_severity_distribution: SolidityScanIssueSeverityDistributionSchema, + }), + scanner_reference_url: v.string(), + }), +}); + +export type SolidityScanReport = v.InferOutput; +export type SolidityScanReportSeverityDistribution = v.InferOutput; diff --git a/explorer/frontend/lib/solidityScan/useFetchReport.ts b/explorer/frontend/lib/solidityScan/useFetchReport.ts new file mode 100644 index 000000000..4df19bc85 --- /dev/null +++ b/explorer/frontend/lib/solidityScan/useFetchReport.ts @@ -0,0 +1,51 @@ +import React from 'react'; +import * as v from 'valibot'; + +import buildUrl from 'lib/api/buildUrl'; +import useApiQuery from 'lib/api/useApiQuery'; +import { SOLIDITY_SCAN_REPORT } from 'stubs/contract'; + +import { SolidityScanSchema } from './schema'; + +interface Params { + hash: string; +} + +const RESOURCE_NAME = 'general:contract_solidity_scan_report'; +const ERROR_NAME = 'Invalid response schema'; + +export default function useFetchReport({ hash }: Params) { + const query = useApiQuery(RESOURCE_NAME, { + pathParams: { hash }, + queryOptions: { + select: (response) => { + const parsedResponse = v.safeParse(SolidityScanSchema, response); + + if (!parsedResponse.success) { + throw Error(ERROR_NAME); + } + + return parsedResponse.output; + }, + enabled: Boolean(hash), + placeholderData: SOLIDITY_SCAN_REPORT, + retry: 0, + }, + }); + + const errorMessage = query.error && 'message' in query.error ? query.error.message : undefined; + + React.useEffect(() => { + if (errorMessage === ERROR_NAME) { + fetch('/node-api/monitoring/invalid-api-schema', { + method: 'POST', + body: JSON.stringify({ + resource: RESOURCE_NAME, + url: buildUrl(RESOURCE_NAME, { hash }, undefined, true), + }), + }); + } + }, [ errorMessage, hash ]); + + return query; +} diff --git a/explorer/frontend/lib/token/metadata/attributesParser.ts b/explorer/frontend/lib/token/metadata/attributesParser.ts new file mode 100644 index 000000000..2167464d9 --- /dev/null +++ b/explorer/frontend/lib/token/metadata/attributesParser.ts @@ -0,0 +1,80 @@ +import { upperFirst } from 'es-toolkit'; + +import type { Metadata, MetadataAttributes } from 'types/client/token'; + +import dayjs from 'lib/date/dayjs'; + +function formatValue(value: string | number, display: string | undefined, trait: string | undefined): Pick { + // https://docs.opensea.io/docs/metadata-standards#attributes + switch (display) { + case 'boost_number': { + return { + value: `+${ value } boost`, + }; + } + case 'boost_percentage': { + return { + value: `${ value }% boost`, + }; + } + case 'date': { + return { + value: dayjs(Number(value) * 1000).format('YYYY-MM-DD'), + }; + } + default: { + try { + if (trait?.toLowerCase().includes('url') || value.toString().startsWith('http')) { + const url = new URL(String(value)); + return { + value: url.toString(), + value_type: 'URL', + }; + } + throw new Error(); + } catch (error) { + return { + value: String(value), + }; + } + } + } +} + +export default function attributesParser(attributes: Array): Metadata['attributes'] { + return attributes + .map((item) => { + if (typeof item !== 'object' || !item) { + return; + } + + const value = (() => { + if (!('value' in item)) { + return; + } + switch (typeof item.value) { + case 'string': + case 'number': + return item.value; + case 'boolean': + return String(item.value); + case 'object': + return JSON.stringify(item.value); + } + })(); + + const trait = 'trait_type' in item && typeof item.trait_type === 'string' ? item.trait_type : undefined; + const display = 'display_type' in item && typeof item.display_type === 'string' ? item.display_type : undefined; + + if (value === undefined) { + return; + } + + return { + ...formatValue(value, display, trait), + trait_type: upperFirst(trait || 'property'), + }; + }) + .filter((item) => item?.value) + .filter(Boolean); +} diff --git a/explorer/frontend/lib/token/metadata/urlParser.ts b/explorer/frontend/lib/token/metadata/urlParser.ts new file mode 100644 index 000000000..586dd1e35 --- /dev/null +++ b/explorer/frontend/lib/token/metadata/urlParser.ts @@ -0,0 +1,17 @@ +import * as regexp from 'toolkit/utils/regexp'; + +export default function urlParser(maybeUrl: string): URL | undefined { + try { + return constructUrl(maybeUrl); + } catch (error) {} +} + +function constructUrl(maybeUrl: string) { + if (regexp.IPFS_PREFIX.test(maybeUrl)) { + return new URL(maybeUrl.replace(regexp.IPFS_PREFIX, 'https://ipfs.io/ipfs/')); + } + + if (regexp.URL_PREFIX.test(maybeUrl)) { + return new URL(maybeUrl); + } +} diff --git a/explorer/frontend/lib/token/parseMetadata.ts b/explorer/frontend/lib/token/parseMetadata.ts new file mode 100644 index 000000000..8ecdb3dea --- /dev/null +++ b/explorer/frontend/lib/token/parseMetadata.ts @@ -0,0 +1,30 @@ +import type { TokenInstance } from 'types/api/token'; +import type { Metadata } from 'types/client/token'; + +import attributesParser from './metadata/attributesParser'; + +export default function parseMetadata(raw: TokenInstance['metadata'] | undefined): Metadata | undefined { + if (!raw) { + return; + } + + const parsed: Metadata = {}; + + if ('name' in raw && typeof raw.name === 'string') { + parsed.name = raw.name; + } + + if ('description' in raw && typeof raw.description === 'string') { + parsed.description = raw.description; + } + + if ('attributes' in raw && Array.isArray(raw.attributes)) { + parsed.attributes = attributesParser(raw.attributes); + } + + if (Object.keys(parsed).length === 0) { + return; + } + + return parsed; +} diff --git a/explorer/frontend/lib/token/tokenTypes.ts b/explorer/frontend/lib/token/tokenTypes.ts new file mode 100644 index 000000000..a382e6134 --- /dev/null +++ b/explorer/frontend/lib/token/tokenTypes.ts @@ -0,0 +1,23 @@ +import type { NFTTokenType, TokenType } from 'types/api/token'; + +import config from 'configs/app'; + +const tokenStandardName = config.chain.tokenStandard; + +export const NFT_TOKEN_TYPES: Record = { + 'ERC-721': `${ tokenStandardName }-721`, + 'ERC-1155': `${ tokenStandardName }-1155`, + 'ERC-404': `${ tokenStandardName }-404`, +}; + +export const TOKEN_TYPES: Record = { + 'ERC-20': `${ tokenStandardName }-20`, + ...NFT_TOKEN_TYPES, +}; + +export const NFT_TOKEN_TYPE_IDS: Array = [ 'ERC-721', 'ERC-1155', 'ERC-404' ]; +export const TOKEN_TYPE_IDS: Array = [ 'ERC-20', ...NFT_TOKEN_TYPE_IDS ]; + +export function getTokenTypeName(typeId: TokenType) { + return TOKEN_TYPES[typeId]; +} diff --git a/explorer/frontend/lib/tx/getConfirmationDuration.ts b/explorer/frontend/lib/tx/getConfirmationDuration.ts new file mode 100644 index 000000000..36d9a9afa --- /dev/null +++ b/explorer/frontend/lib/tx/getConfirmationDuration.ts @@ -0,0 +1,17 @@ +export default function getConfirmationString(durations: Array) { + if (durations.length === 0) { + return ''; + } + + const [ lower, upper ] = durations.map((time) => time / 1_000); + + if (!upper) { + return `Confirmed within ${ lower.toLocaleString() } secs`; + } + + if (lower === 0) { + return `Confirmed within <= ${ upper.toLocaleString() } secs`; + } + + return `Confirmed within ${ lower.toLocaleString() } - ${ upper.toLocaleString() } secs`; +} diff --git a/explorer/frontend/lib/units.ts b/explorer/frontend/lib/units.ts new file mode 100644 index 000000000..546e4308e --- /dev/null +++ b/explorer/frontend/lib/units.ts @@ -0,0 +1,11 @@ +import type { Unit } from 'types/unit'; + +import config from 'configs/app'; + +const weiName = config.chain.currency.weiName || 'wei'; + +export const currencyUnits: Record = { + wei: weiName, + gwei: `G${ weiName }`, + ether: config.chain.currency.symbol || 'ETH', +}; diff --git a/explorer/frontend/lib/web3/chains.ts b/explorer/frontend/lib/web3/chains.ts new file mode 100644 index 000000000..0342d620c --- /dev/null +++ b/explorer/frontend/lib/web3/chains.ts @@ -0,0 +1,57 @@ +import { type Chain } from 'viem'; + +import config from 'configs/app'; + +export const currentChain: Chain = { + id: Number(config.chain.id), + name: config.chain.name ?? '', + nativeCurrency: { + decimals: config.chain.currency.decimals, + name: config.chain.currency.name ?? '', + symbol: config.chain.currency.symbol ?? '', + }, + rpcUrls: { + 'default': { + http: config.chain.rpcUrls, + }, + }, + blockExplorers: { + 'default': { + name: 'Blockscout', + url: config.app.baseUrl, + }, + }, + testnet: config.chain.isTestnet, +}; + +export const parentChain: Chain | undefined = (() => { + const rollupFeature = config.features.rollup; + + const parentChain = rollupFeature.isEnabled && rollupFeature.parentChain; + + if (!parentChain) { + return; + } + + if (!parentChain.id || !parentChain.name || !parentChain.rpcUrls || !parentChain.baseUrl || !parentChain.currency) { + return; + } + + return { + id: parentChain.id, + name: parentChain.name, + nativeCurrency: parentChain.currency, + rpcUrls: { + 'default': { + http: parentChain.rpcUrls, + }, + }, + blockExplorers: { + 'default': { + name: 'Blockscout', + url: parentChain.baseUrl, + }, + }, + testnet: parentChain.isTestnet, + }; +})(); diff --git a/explorer/frontend/lib/web3/client.ts b/explorer/frontend/lib/web3/client.ts new file mode 100644 index 000000000..262066d12 --- /dev/null +++ b/explorer/frontend/lib/web3/client.ts @@ -0,0 +1,19 @@ +import { createPublicClient, http } from 'viem'; + +import { currentChain } from './chains'; + +export const publicClient = (() => { + if (currentChain.rpcUrls.default.http.filter(Boolean).length === 0) { + return; + } + + try { + return createPublicClient({ + chain: currentChain, + transport: http(), + batch: { + multicall: true, + }, + }); + } catch (error) {} +})(); diff --git a/explorer/frontend/lib/web3/useAccount.ts b/explorer/frontend/lib/web3/useAccount.ts new file mode 100644 index 000000000..f3dfcd48c --- /dev/null +++ b/explorer/frontend/lib/web3/useAccount.ts @@ -0,0 +1,23 @@ +import type { UseAccountReturnType } from 'wagmi'; +import { useAccount } from 'wagmi'; + +import config from 'configs/app'; + +function useAccountFallback(): UseAccountReturnType { + return { + address: undefined, + addresses: undefined, + chain: undefined, + chainId: undefined, + connector: undefined, + isConnected: false, + isConnecting: false, + isDisconnected: true, + isReconnecting: false, + status: 'disconnected', + }; +} + +const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback; + +export default hook; diff --git a/explorer/frontend/lib/web3/useAccountWithDomain.ts b/explorer/frontend/lib/web3/useAccountWithDomain.ts new file mode 100644 index 000000000..69a5e83df --- /dev/null +++ b/explorer/frontend/lib/web3/useAccountWithDomain.ts @@ -0,0 +1,31 @@ +import React from 'react'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; + +import useAccount from './useAccount'; + +export default function useAccountWithDomain(isEnabled: boolean) { + const { address, isConnecting } = useAccount(); + + const isQueryEnabled = config.features.nameService.isEnabled && Boolean(address) && Boolean(isEnabled); + + const domainQuery = useApiQuery('bens:address_domain', { + pathParams: { + chainId: config.chain.id, + address, + }, + queryOptions: { + enabled: isQueryEnabled, + refetchOnMount: false, + }, + }); + + return React.useMemo(() => { + return { + address: isEnabled ? address : undefined, + domain: domainQuery.data?.domain?.name, + isLoading: (isQueryEnabled && domainQuery.isLoading) || isConnecting, + }; + }, [ address, domainQuery.data?.domain?.name, domainQuery.isLoading, isEnabled, isQueryEnabled, isConnecting ]); +} diff --git a/explorer/frontend/lib/web3/useAddChain.tsx b/explorer/frontend/lib/web3/useAddChain.tsx new file mode 100644 index 000000000..b504a7b64 --- /dev/null +++ b/explorer/frontend/lib/web3/useAddChain.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type { AddEthereumChainParameter } from 'viem'; + +import config from 'configs/app'; +import { SECOND } from 'toolkit/utils/consts'; + +import useRewardsActivity from '../hooks/useRewardsActivity'; +import useProvider from './useProvider'; +import { getHexadecimalChainId } from './utils'; + +function getParams(): AddEthereumChainParameter { + if (!config.chain.id) { + throw new Error('Missing required chain config'); + } + + return { + chainId: getHexadecimalChainId(Number(config.chain.id)), + chainName: config.chain.name ?? '', + nativeCurrency: { + name: config.chain.currency.name ?? '', + symbol: config.chain.currency.symbol ?? '', + decimals: config.chain.currency.decimals ?? 18, + }, + rpcUrls: config.chain.rpcUrls, + blockExplorerUrls: [ config.app.baseUrl ], + }; +} + +export default function useAddChain() { + const { wallet, provider } = useProvider(); + const { trackUsage } = useRewardsActivity(); + + return React.useCallback(async() => { + if (!wallet || !provider) { + throw new Error('Wallet or provider not found'); + } + + const start = Date.now(); + + await provider.request({ + method: 'wallet_addEthereumChain', + params: [ getParams() ], + }); + + // if network is already added, the promise resolves immediately + if (Date.now() - start > SECOND) { + await trackUsage('add_network'); + } + }, [ wallet, provider, trackUsage ]); +} diff --git a/explorer/frontend/lib/web3/useProvider.tsx b/explorer/frontend/lib/web3/useProvider.tsx new file mode 100644 index 000000000..43cc7aa8f --- /dev/null +++ b/explorer/frontend/lib/web3/useProvider.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import type { WalletType } from 'types/client/wallets'; +import type { WalletProvider } from 'types/web3'; + +import config from 'configs/app'; + +const feature = config.features.web3Wallet; + +export default function useProvider() { + const [ provider, setProvider ] = React.useState(); + const [ wallet, setWallet ] = React.useState(); + + const initializeProvider = React.useMemo(() => async() => { + if (!feature.isEnabled) { + return; + } + + if (!('ethereum' in window && window.ethereum)) { + if (feature.wallets.includes('metamask') && window.navigator.userAgent.includes('Firefox')) { + const { WindowPostMessageStream } = (await import('@metamask/post-message-stream')); + const { initializeProvider } = (await import('@metamask/providers')); + + // workaround for MetaMask in Firefox + // Firefox blocks MetaMask injection script because of our CSP for 'script-src' + // so we have to inject it manually while the issue is not fixed + // https://github.com/MetaMask/metamask-extension/issues/3133#issuecomment-1025641185 + const metamaskStream = new WindowPostMessageStream({ + name: 'metamask-inpage', + target: 'metamask-contentscript', + }); + + // this will initialize the provider and set it as window.ethereum + initializeProvider({ + connectionStream: metamaskStream as never, + shouldShimWeb3: true, + }); + } else { + return; + } + } + + // have to check again in case provider was not set as window.ethereum in the previous step for MM in FF + // and also it makes typescript happy + if (!('ethereum' in window && window.ethereum)) { + return; + } + + // if user has multiple wallets installed, they all are injected in the window.ethereum.providers array + // if user has only one wallet, the provider is injected in the window.ethereum directly + const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ]; + + for (const wallet of feature.wallets) { + const provider = providers.find((provider) => { + return ( + // some wallets (e.g TokenPocket, Liquality, etc) try to look like MetaMask but they are not (not even close) + // so we have to check in addition the presence of the provider._events property + // found this hack in wagmi repo + // https://github.com/wagmi-dev/wagmi/blob/399b9eab8cfd698b49bfaa8456598dad9b597e0e/packages/connectors/src/types.ts#L65 + // for now it's the only way to distinguish them + (wallet === 'metamask' && provider.isMetaMask && Boolean(provider._events)) || + (wallet === 'coinbase' && provider.isCoinbaseWallet) || + (wallet === 'token_pocket' && provider.isTokenPocket) + ); + }); + + if (provider) { + setProvider(provider); + setWallet(wallet); + break; + } + } + }, []); + + React.useEffect(() => { + initializeProvider(); + }, [ initializeProvider ]); + + return { provider, wallet }; +} diff --git a/explorer/frontend/lib/web3/useSwitchChain.tsx b/explorer/frontend/lib/web3/useSwitchChain.tsx new file mode 100644 index 000000000..14c1cf53c --- /dev/null +++ b/explorer/frontend/lib/web3/useSwitchChain.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import config from 'configs/app'; + +import useProvider from './useProvider'; +import { getHexadecimalChainId } from './utils'; + +function getParams(): { chainId: string } { + if (!config.chain.id) { + throw new Error('Missing required chain config'); + } + + return { chainId: getHexadecimalChainId(Number(config.chain.id)) }; +} + +export default function useSwitchChain() { + const { wallet, provider } = useProvider(); + + return React.useCallback(() => { + if (!wallet || !provider) { + throw new Error('Wallet or provider not found'); + } + + return provider.request({ + method: 'wallet_switchEthereumChain', + params: [ getParams() ], + }); + }, [ wallet, provider ]); +} diff --git a/explorer/frontend/lib/web3/useSwitchOrAddChain.tsx b/explorer/frontend/lib/web3/useSwitchOrAddChain.tsx new file mode 100644 index 000000000..7b2ee9f66 --- /dev/null +++ b/explorer/frontend/lib/web3/useSwitchOrAddChain.tsx @@ -0,0 +1,35 @@ +import { get } from 'es-toolkit/compat'; +import React from 'react'; + +import getErrorObj from 'lib/errors/getErrorObj'; + +import useAddChain from './useAddChain'; +import useProvider from './useProvider'; +import useSwitchChain from './useSwitchChain'; + +export default function useSwitchOrAddChain() { + const { wallet, provider } = useProvider(); + const addChain = useAddChain(); + const switchChain = useSwitchChain(); + + return React.useCallback(async() => { + if (!wallet || !provider) { + return; + } + + try { + return switchChain(); + } catch (error) { + const errorObj = getErrorObj(error); + const code = get(errorObj, 'code'); + const originalErrorCode = get(errorObj, 'data.originalError.code'); + + // This error code indicates that the chain has not been added to Wallet. + if (code === 4902 || originalErrorCode === 4902) { + return addChain(); + } + + throw error; + } + }, [ addChain, provider, wallet, switchChain ]); +} diff --git a/explorer/frontend/lib/web3/useWallet.ts b/explorer/frontend/lib/web3/useWallet.ts new file mode 100644 index 000000000..abc6267e2 --- /dev/null +++ b/explorer/frontend/lib/web3/useWallet.ts @@ -0,0 +1,63 @@ +import { useAppKit, useAppKitState } from '@reown/appkit/react'; +import React from 'react'; +import { useDisconnect, useAccountEffect } from 'wagmi'; + +import * as mixpanel from 'lib/mixpanel/index'; +import useAccount from 'lib/web3/useAccount'; + +interface Params { + source: mixpanel.EventPayload['Source']; + onConnect?: () => void; +} + +export default function useWeb3Wallet({ source, onConnect }: Params) { + const { open: openModal } = useAppKit(); + const { open: isOpen } = useAppKitState(); + const { disconnect } = useDisconnect(); + const [ isOpening, setIsOpening ] = React.useState(false); + const [ isClientLoaded, setIsClientLoaded ] = React.useState(false); + const isConnectionStarted = React.useRef(false); + + React.useEffect(() => { + setIsClientLoaded(true); + }, []); + + const handleConnect = React.useCallback(async() => { + setIsOpening(true); + await openModal(); + setIsOpening(false); + mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' }); + isConnectionStarted.current = true; + }, [ openModal, source ]); + + const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => { + if (!isReconnected && isConnectionStarted.current) { + mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' }); + mixpanel.userProfile.setOnce({ + 'With Connected Wallet': true, + }); + onConnect?.(); + } + isConnectionStarted.current = false; + }, [ source, onConnect ]); + + const handleDisconnect = React.useCallback(() => { + disconnect(); + }, [ disconnect ]); + + useAccountEffect({ onConnect: handleAccountConnected }); + + const account = useAccount(); + const address = account.address; + const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined; + + return React.useMemo(() => ({ + connect: handleConnect, + disconnect: handleDisconnect, + isOpen: isOpening || isOpen, + isConnected, + isReconnecting: account.isReconnecting, + address, + openModal, + }), [ handleConnect, handleDisconnect, isOpening, isOpen, isConnected, account.isReconnecting, address, openModal ]); +} diff --git a/explorer/frontend/lib/web3/utils.ts b/explorer/frontend/lib/web3/utils.ts new file mode 100644 index 000000000..f77e9e04a --- /dev/null +++ b/explorer/frontend/lib/web3/utils.ts @@ -0,0 +1,3 @@ +export function getHexadecimalChainId(chainId: number) { + return '0x' + Number(chainId).toString(16); +} diff --git a/explorer/frontend/lib/web3/wagmiConfig.ts b/explorer/frontend/lib/web3/wagmiConfig.ts new file mode 100644 index 000000000..a4d8b27ca --- /dev/null +++ b/explorer/frontend/lib/web3/wagmiConfig.ts @@ -0,0 +1,50 @@ +import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; +import type { AppKitNetwork } from '@reown/appkit/networks'; +import type { Chain } from 'viem'; +import { fallback, http } from 'viem'; +import { createConfig } from 'wagmi'; + +import config from 'configs/app'; +import { currentChain, parentChain } from 'lib/web3/chains'; + +const feature = config.features.blockchainInteraction; + +const chains = [ currentChain, parentChain ].filter(Boolean); + +const wagmi = (() => { + + if (!feature.isEnabled) { + const wagmiConfig = createConfig({ + chains: chains as [Chain, ...Array], + transports: { + [currentChain.id]: fallback( + config.chain.rpcUrls + .map((url) => http(url)) + .concat(http(`${ config.apis.general.endpoint }/api/eth-rpc`)), + ), + ...(parentChain ? { [parentChain.id]: http(parentChain.rpcUrls.default.http[0]) } : {}), + }, + ssr: true, + batch: { multicall: { wait: 100 } }, + }); + + return { config: wagmiConfig, adapter: null }; + } + + const wagmiAdapter = new WagmiAdapter({ + networks: chains as Array, + multiInjectedProviderDiscovery: true, + transports: { + [currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))), + ...(parentChain ? { [parentChain.id]: http() } : {}), + }, + projectId: feature.walletConnect.projectId, + ssr: true, + batch: { multicall: { wait: 100 } }, + syncConnectedChain: false, + }); + + return { config: wagmiAdapter.wagmiConfig, adapter: wagmiAdapter }; +})(); + +export default wagmi; diff --git a/explorer/frontend/lib/web3/wallets.ts b/explorer/frontend/lib/web3/wallets.ts new file mode 100644 index 000000000..6bcb257e6 --- /dev/null +++ b/explorer/frontend/lib/web3/wallets.ts @@ -0,0 +1,16 @@ +import type { WalletType, WalletInfo } from 'types/client/wallets'; + +export const WALLETS_INFO: Record, WalletInfo> = { + metamask: { + name: 'MetaMask', + icon: 'wallets/metamask', + }, + coinbase: { + name: 'Coinbase Wallet', + icon: 'wallets/coinbase', + }, + token_pocket: { + name: 'TokenPocket', + icon: 'wallets/token-pocket', + }, +}; diff --git a/explorer/frontend/lib/xStarScore/useFetchXStarScore.ts b/explorer/frontend/lib/xStarScore/useFetchXStarScore.ts new file mode 100644 index 000000000..9fe773baa --- /dev/null +++ b/explorer/frontend/lib/xStarScore/useFetchXStarScore.ts @@ -0,0 +1,51 @@ +import React from 'react'; +import * as v from 'valibot'; + +import config from 'configs/app'; +import buildUrl from 'lib/api/buildUrl'; +import useApiQuery from 'lib/api/useApiQuery'; + +interface Params { + hash: string; +} + +const RESOURCE_NAME = 'general:address_xstar_score'; +const ERROR_NAME = 'Invalid response schema'; + +export default function useFetchXStarScore({ hash }: Params) { + const query = useApiQuery(RESOURCE_NAME, { + pathParams: { hash }, + queryOptions: { + select: (response) => { + const parsedResponse = v.safeParse(v.object({ data: v.object({ level: v.nullable(v.string()) }) }), response); + + if (!parsedResponse.success) { + throw Error(ERROR_NAME); + } + + return parsedResponse.output; + }, + enabled: Boolean(hash) && config.features.xStarScore.isEnabled, + placeholderData: { + data: { level: 'Base' }, + }, + retry: 0, + }, + }); + + const errorMessage = query.error && 'message' in query.error ? query.error.message : undefined; + + React.useEffect(() => { + if (errorMessage === ERROR_NAME) { + fetch('/node-api/monitoring/invalid-api-schema', { + method: 'POST', + body: JSON.stringify({ + resource: RESOURCE_NAME, + url: buildUrl(RESOURCE_NAME, { hash }, undefined, true), + }), + }); + } + }, [ errorMessage, hash ]); + + return query; +} diff --git a/explorer/frontend/middleware.ts b/explorer/frontend/middleware.ts new file mode 100644 index 000000000..d9145fe1b --- /dev/null +++ b/explorer/frontend/middleware.ts @@ -0,0 +1,7 @@ +export function middleware() { + return; +} + +export const config = { + matcher: [], +}; diff --git a/explorer/frontend/mocks/account/verifiedAddresses.ts b/explorer/frontend/mocks/account/verifiedAddresses.ts new file mode 100644 index 000000000..ce9141119 --- /dev/null +++ b/explorer/frontend/mocks/account/verifiedAddresses.ts @@ -0,0 +1,164 @@ +import type { TokenInfoApplication, TokenInfoApplications, VerifiedAddress, VerifiedAddressResponse } from 'types/api/account'; +import type { AddressValidationResponseSuccess } from 'ui/addressVerification/types'; + +export const SIGNATURE = '0x96491e0cd1b99c14951552361b7f6ff64f41651b5d1c12501914342c8a6847e21e08726c3505e11ba2af9a40ac0b05c8d113e7fd1f74594224b9c7276ebb3a661b'; + +export const VERIFIED_ADDRESS: Record = { + NEW_ITEM: { + userId: '1', + chainId: '99', + contractAddress: '0xF822070D07067D1519490dBf49448a7E30EE9ea5', + verifiedDate: '2022-09-01', + metadata: { + tokenName: 'Test Token', + tokenSymbol: 'TT', + }, + }, + ITEM_1: { + userId: '1', + chainId: '99', + contractAddress: '0xd0e3010d1ecdbd17aae178b2bf36eb413d8a7441', + verifiedDate: '2022-08-01', + metadata: { + tokenName: 'My Token', + tokenSymbol: 'MYT', + }, + }, + ITEM_2: { + userId: '1', + chainId: '99', + contractAddress: '0xa8FCe579a11E551635b9c9CB915BEcd873C51254', + verifiedDate: '2022-09-23', + metadata: { + tokenName: 'Cat Token', + tokenSymbol: 'CATT', + }, + }, +}; + +export const ADDRESS_CHECK_RESPONSE = { + SUCCESS: { + status: 'SUCCESS', + result: { + // eslint-disable-next-line max-len + signingMessage: '[eth-goerli.blockscout.com] [2023-04-18 18:47:40] I, hereby verify that I am the owner/creator of the address [0xf822070d07067d1519490dbf49448a7e30ee9ea5]', + contractCreator: '0xd0e3010d1ecdbd17aae178b2bf36eb413d8a7441', + contractOwner: '0xa8FCe579a11E551635b9c9CB915BEcd873C51254', + }, + }, + SOURCE_CODE_NOT_VERIFIED_ERROR: { + status: 'SOURCE_CODE_NOT_VERIFIED_ERROR', + }, +}; + +export const ADDRESS_VERIFY_RESPONSE: Record = { + SUCCESS: { + status: 'SUCCESS', + result: { + verifiedAddress: VERIFIED_ADDRESS.NEW_ITEM, + }, + }, + INVALID_SIGNER_ERROR: { + status: 'INVALID_SIGNER_ERROR', + invalidSigner: { + signer: '0xF822070D07067D1519490dBf49448a7E30EE9ea5', + }, + }, +}; + +export const VERIFIED_ADDRESS_RESPONSE: Record = { + DEFAULT: { + verifiedAddresses: [ + VERIFIED_ADDRESS.ITEM_1, + VERIFIED_ADDRESS.ITEM_2, + ], + }, +}; + +export const TOKEN_INFO_APPLICATION_BASE = { + id: '1', + tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress, + status: 'APPROVED', + updatedAt: '2022-11-08 12:47:10.149148Z', + requesterName: 'Tom', + requesterEmail: 'tom@example.com', + projectName: 'My project', + projectWebsite: 'http://example.com', + projectEmail: 'token@example.com', + iconUrl: 'https://placekitten.com/100', + projectDescription: 'description', + projectSector: 'DeFi', + comment: '', + docs: 'https://example.com/docs', + github: 'https://github.com', + telegram: 'https://telegram.com', + linkedin: 'https://linkedin.com', + discord: 'https://discord.com', + slack: 'https://slack.com', + twitter: 'https://twitter.com', + openSea: 'https://opensea.com', + facebook: 'https://facebook.com', + medium: 'https://medium.com', + reddit: 'https://reddit.com', + support: 'support@example.com', + coinMarketCapTicker: 'https://coinmarketcap.com', + coinGeckoTicker: 'https://coingecko.com', + defiLlamaTicker: 'https://defillama.com', +}; + +export const TOKEN_INFO_APPLICATION: Record = { + APPROVED: { + ...TOKEN_INFO_APPLICATION_BASE, + tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress, + id: '1', + status: 'APPROVED', + updatedAt: '2022-11-08 12:47:10.149148Z', + }, + IN_PROCESS: { + ...TOKEN_INFO_APPLICATION_BASE, + tokenAddress: VERIFIED_ADDRESS.ITEM_2.contractAddress, + id: '2', + status: 'IN_PROCESS', + updatedAt: '2022-11-10 08:11:10.149148Z', + }, + UPDATED_ITEM: { + ...TOKEN_INFO_APPLICATION_BASE, + tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress, + id: '1', + status: 'IN_PROCESS', + updatedAt: '2022-11-11 05:11:10.149148Z', + }, +}; + +export const TOKEN_INFO_APPLICATIONS_RESPONSE: Record = { + DEFAULT: { + submissions: [ + TOKEN_INFO_APPLICATION.APPROVED, + TOKEN_INFO_APPLICATION.IN_PROCESS, + ], + }, + FOR_UPDATE: { + submissions: [ + { + ...TOKEN_INFO_APPLICATION.APPROVED, + status: 'UPDATE_REQUIRED', + }, + TOKEN_INFO_APPLICATION.IN_PROCESS, + ], + }, +}; + +export const TOKEN_INFO_FORM_CONFIG = { + projectSectors: [ + 'Infra & Dev tooling', + 'DeFi', + 'Data', + 'Bridge', + 'NFT', + 'Payments', + 'Faucet', + 'DAO', + 'Games', + 'Wallet', + ], +}; diff --git a/explorer/frontend/mocks/ad/textAd.ts b/explorer/frontend/mocks/ad/textAd.ts new file mode 100644 index 000000000..0e5bb6fea --- /dev/null +++ b/explorer/frontend/mocks/ad/textAd.ts @@ -0,0 +1,9 @@ +export const duck = { + ad: { + name: 'Hello utia!', + description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!', + thumbnail: 'http://localhost:3100/utia.jpg', + url: 'https://test.url', + cta_button: 'Click me!', + }, +}; diff --git a/explorer/frontend/mocks/address/address.ts b/explorer/frontend/mocks/address/address.ts new file mode 100644 index 000000000..fbc81bf17 --- /dev/null +++ b/explorer/frontend/mocks/address/address.ts @@ -0,0 +1,170 @@ +import type { Address } from 'types/api/address'; +import type { AddressParam } from 'types/api/addressParams'; + +import { publicTag, privateTag, watchlistName } from 'mocks/address/tag'; +import { tokenInfo } from 'mocks/tokens/tokenInfo'; + +export const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; + +export const withName: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: null, +}; + +export const withEns: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', +}; + +export const withNameTag: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', + metadata: { + reputation: null, + tags: [ + { tagType: 'name', name: 'Mrs. Duckie', slug: 'mrs-duckie', ordinal: 0, meta: null }, + ], + }, +}; + +export const withoutName: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: null, +}; + +export const delegated: AddressParam = { + ...withoutName, + is_verified: true, + proxy_type: 'eip7702', +}; + +export const token: Address = { + hash: hash, + implementations: null, + is_contract: true, + is_verified: false, + name: null, + private_tags: [], + watchlist_names: [], + watchlist_address_id: null, + public_tags: [], + token: tokenInfo, + block_number_balance_updated_at: 8201413, + coin_balance: '1', + creation_transaction_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98', + creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72', + exchange_rate: '0.04311', + has_logs: false, + has_token_transfers: true, + has_tokens: true, + has_validated_blocks: false, + ens_domain_name: null, +}; + +export const eoa: Address = { + block_number_balance_updated_at: 30811263, + coin_balance: '2782650189688719421432220500', + creation_transaction_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', + creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', + exchange_rate: '0.04311', + has_logs: true, + has_token_transfers: false, + has_tokens: true, + has_validated_blocks: false, + hash: hash, + implementations: [], + is_contract: false, + is_verified: false, + name: null, + private_tags: [ publicTag ], + public_tags: [ privateTag ], + token: null, + watchlist_names: [ watchlistName ], + watchlist_address_id: 42, + ens_domain_name: null, +}; + +export const contract: Address = { + block_number_balance_updated_at: 30811263, + coin_balance: '27826501896887194214322205', + creation_transaction_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', + creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', + exchange_rate: '0.04311', + has_logs: true, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: false, + hash: hash, + implementations: [ + { address_hash: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' }, + ], + is_contract: true, + is_verified: true, + name: 'EternalStorageProxy', + private_tags: [ publicTag ], + public_tags: [ privateTag ], + token: null, + watchlist_names: [ watchlistName ], + watchlist_address_id: 42, + ens_domain_name: null, +}; + +export const validator: Address = { + block_number_balance_updated_at: 30811932, + coin_balance: '22910462800601256910890', + creation_transaction_hash: null, + creator_address_hash: null, + exchange_rate: '0.00432018', + has_logs: false, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: true, + hash: hash, + implementations: [], + is_contract: false, + is_verified: false, + name: 'Kiryl Ihnatsyeu', + private_tags: [], + public_tags: [], + token: null, + watchlist_names: [], + watchlist_address_id: null, + ens_domain_name: null, +}; + +export const filecoin = { + ...validator, + filecoin: { + actor_type: 'evm' as const, + id: 'f02977693', + robust: 'f410fuiwj6a3yxajbohrl5vu6ns6o2e2jriul52lvzci', + }, +}; diff --git a/explorer/frontend/mocks/address/coinBalanceHistory.ts b/explorer/frontend/mocks/address/coinBalanceHistory.ts new file mode 100644 index 000000000..cc78d7560 --- /dev/null +++ b/explorer/frontend/mocks/address/coinBalanceHistory.ts @@ -0,0 +1,70 @@ +import type { AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryChart } from 'types/api/address'; + +export const base: AddressCoinBalanceHistoryItem = { + block_number: 30367643, + block_timestamp: '2022-12-11T17:55:20Z', + delta: '-5568096000000000', + transaction_hash: null, + value: '107014805905725000000', +}; + +export const baseResponse: AddressCoinBalanceHistoryResponse = { + items: [ + { + block_number: 30367643, + block_timestamp: '2022-10-11T17:55:20Z', + delta: '-2105682233848856', + transaction_hash: null, + value: '10102109526582662088', + }, + { + block_number: 30367234, + block_timestamp: '2022-10-01T17:55:20Z', + delta: '1933020674364000', + transaction_hash: null, + value: '10143933697708939226', + }, + { + block_number: 30363402, + block_timestamp: '2022-09-03T17:55:20Z', + delta: '-1448410607186694', + transaction_hash: null, + value: '10142485287101752532', + }, + ], + next_page_params: null, +}; + +export const chartResponse: AddressCoinBalanceHistoryChart = { + items: [ + { + date: '2022-11-02', + value: '128238612887883515', + }, + { + date: '2022-11-03', + value: '199807583157570922', + }, + { + date: '2022-11-04', + value: '114487912907005778', + }, + { + date: '2022-11-05', + value: '219533112907005778', + }, + { + date: '2022-11-06', + value: '116487912907005778', + }, + { + date: '2022-11-07', + value: '199807583157570922', + }, + { + date: '2022-11-08', + value: '216488112907005778', + }, + ], + days: 10, +}; diff --git a/explorer/frontend/mocks/address/counters.ts b/explorer/frontend/mocks/address/counters.ts new file mode 100644 index 000000000..9a676e324 --- /dev/null +++ b/explorer/frontend/mocks/address/counters.ts @@ -0,0 +1,22 @@ +import type { AddressCounters } from 'types/api/address'; + +export const forContract: AddressCounters = { + gas_usage_count: '319340525', + token_transfers_count: '0', + transactions_count: '5462', + validations_count: '0', +}; + +export const forToken: AddressCounters = { + gas_usage_count: '247479698', + token_transfers_count: '1', + transactions_count: '8474', + validations_count: '0', +}; + +export const forValidator: AddressCounters = { + gas_usage_count: '91675762951', + token_transfers_count: '0', + transactions_count: '820802', + validations_count: '1726416', +}; diff --git a/explorer/frontend/mocks/address/epochRewards.ts b/explorer/frontend/mocks/address/epochRewards.ts new file mode 100644 index 000000000..5b22a66cd --- /dev/null +++ b/explorer/frontend/mocks/address/epochRewards.ts @@ -0,0 +1,50 @@ +import type { AddressEpochRewardsResponse } from 'types/api/address'; + +import { tokenInfo } from 'mocks/tokens/tokenInfo'; + +import { withEns, withName, withoutName } from './address'; + +export const epochRewards: AddressEpochRewardsResponse = { + items: [ + { + type: 'delegated_payment', + amount: '136609473658452408568', + account: withName, + associated_account: withName, + block_hash: '0x', + block_number: 26369280, + block_timestamp: '2022-05-15T13:16:24Z', + epoch_number: 1526, + token: tokenInfo, + }, + { + type: 'group', + amount: '117205842355246195095', + account: withoutName, + associated_account: withoutName, + block_hash: '0x', + block_number: 26352000, + block_timestamp: '2022-05-15T13:16:24Z', + epoch_number: 1525, + token: tokenInfo, + }, + { + type: 'validator', + amount: '125659647325556554060', + account: withEns, + associated_account: withEns, + block_hash: '0x', + block_number: 26300160, + block_timestamp: '2022-05-15T13:16:24Z', + epoch_number: 1524, + token: tokenInfo, + }, + ], + next_page_params: { + amount: '71952055594478242556', + associated_account_address_hash: '0x30d060f129817c4de5fbc1366d53e19f43c8c64f', + block_number: 25954560, + items_count: 50, + type: 'delegated_payment', + }, +}; diff --git a/explorer/frontend/mocks/address/implementations.ts b/explorer/frontend/mocks/address/implementations.ts new file mode 100644 index 000000000..e99c99013 --- /dev/null +++ b/explorer/frontend/mocks/address/implementations.ts @@ -0,0 +1,11 @@ +export const multiple = [ + { address_hash: '0xA84d24bD8ACE4d349C5f8c5DeeDd8bc071Ce5e2b', name: null }, + { address_hash: '0xc9e91eDeA9DC16604022e4E5b437Df9c64EdB05A', name: 'Diamond' }, + { address_hash: '0x2041832c62C0F89426b48B5868146C0b1fcd23E7', name: null }, + { address_hash: '0x5f7DC6ECcF05594429671F83cc0e42EE18bC0974', name: 'VariablePriceFacet' }, + { address_hash: '0x7abC92E242e88e4B0d6c5Beb4Df80e94D2c8A78c', name: null }, + { address_hash: '0x84178a0c58A860eCCFB7E3aeA64a09543062A356', name: 'MultiSaleFacet' }, + { address_hash: '0x33aD95537e63e9f09d96dE201e10715Ed40D9400', name: 'SVGTemplatesFacet' }, + { address_hash: '0xfd86Aa7f902185a8Df9859c25E4BF52D3DaDd9FA', name: 'ERC721AReceiverFacet' }, + { address_hash: '0x6945a35df18e59Ce09fec4B6cD3C4F9cFE6369de', name: null }, +]; diff --git a/explorer/frontend/mocks/address/tabCounters.ts b/explorer/frontend/mocks/address/tabCounters.ts new file mode 100644 index 000000000..85c3cd1e9 --- /dev/null +++ b/explorer/frontend/mocks/address/tabCounters.ts @@ -0,0 +1,11 @@ +import type { AddressTabsCounters } from 'types/api/address'; + +export const base: AddressTabsCounters = { + internal_transactions_count: 13, + logs_count: 51, + token_balances_count: 3, + token_transfers_count: 3, + transactions_count: 51, + validations_count: 42, + withdrawals_count: 11, +}; diff --git a/explorer/frontend/mocks/address/tag.ts b/explorer/frontend/mocks/address/tag.ts new file mode 100644 index 000000000..bb1b06667 --- /dev/null +++ b/explorer/frontend/mocks/address/tag.ts @@ -0,0 +1,18 @@ +import type { AddressTag, WatchlistName } from 'types/api/addressParams'; + +export const privateTag: AddressTag = { + label: 'my-private-tag', + display_name: 'my private tag', + address_hash: '0x', +}; + +export const publicTag: AddressTag = { + label: 'some-public-tag', + display_name: 'some public tag', + address_hash: '0x', +}; + +export const watchlistName: WatchlistName = { + label: 'watchlist-name', + display_name: 'watchlist name', +}; diff --git a/explorer/frontend/mocks/address/tokens.ts b/explorer/frontend/mocks/address/tokens.ts new file mode 100644 index 000000000..7505eebc5 --- /dev/null +++ b/explorer/frontend/mocks/address/tokens.ts @@ -0,0 +1,214 @@ +import type { AddressCollectionsResponse, AddressNFTsResponse, AddressTokenBalance, AddressTokensResponse } from 'types/api/address'; + +import * as tokens from 'mocks/tokens/tokenInfo'; +import * as tokenInstance from 'mocks/tokens/tokenInstance'; + +export const erc20a: AddressTokenBalance = { + token: tokens.tokenInfoERC20a, + token_id: null, + value: '1169321234567891234567891', + token_instance: null, +}; + +export const erc20b: AddressTokenBalance = { + token: tokens.tokenInfoERC20b, + token_id: null, + value: '872500000000', + token_instance: null, +}; + +export const erc20c: AddressTokenBalance = { + token: tokens.tokenInfoERC20c, + token_id: null, + value: '9852000000000000000000', + token_instance: null, +}; + +export const erc20d: AddressTokenBalance = { + token: tokens.tokenInfoERC20d, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + +export const erc20LongSymbol: AddressTokenBalance = { + token: tokens.tokenInfoERC20LongSymbol, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + +export const erc20BigAmount: AddressTokenBalance = { + token: { + ...tokens.tokenInfoERC20LongSymbol, + exchange_rate: '4200000000', + name: 'DuckDuckGoose Stable Coin', + }, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + +export const erc721a: AddressTokenBalance = { + token: tokens.tokenInfoERC721a, + token_id: null, + value: '51', + token_instance: null, +}; + +export const erc721b: AddressTokenBalance = { + token: tokens.tokenInfoERC721b, + token_id: null, + value: '1', + token_instance: null, +}; + +export const erc721c: AddressTokenBalance = { + token: tokens.tokenInfoERC721c, + token_id: null, + value: '5', + token_instance: null, +}; + +export const erc721LongSymbol: AddressTokenBalance = { + token: tokens.tokenInfoERC721LongSymbol, + token_id: null, + value: '5', + token_instance: null, +}; + +export const erc1155a: AddressTokenBalance = { + token: tokens.tokenInfoERC1155a, + token_id: '42', + token_instance: tokenInstance.base, + value: '24', +}; + +export const erc1155b: AddressTokenBalance = { + token: tokens.tokenInfoERC1155b, + token_id: '100010000000001', + token_instance: tokenInstance.base, + value: '11', +}; + +export const erc1155withoutName: AddressTokenBalance = { + token: tokens.tokenInfoERC1155WithoutName, + token_id: '64532245', + token_instance: tokenInstance.base, + value: '42', +}; + +export const erc1155LongId: AddressTokenBalance = { + token: tokens.tokenInfoERC1155b, + token_id: '483200961027732618117991942553110860267520', + token_instance: tokenInstance.base, + value: '42', +}; + +export const erc404a: AddressTokenBalance = { + token: tokens.tokenInfoERC404, + token_id: '42', + token_instance: tokenInstance.base, + value: '240000000000000', +}; + +export const erc404b: AddressTokenBalance = { + token: tokens.tokenInfoERC404, + token_instance: null, + value: '11', + token_id: null, +}; + +export const erc20List: AddressTokensResponse = { + items: [ + erc20a, + erc20b, + erc20c, + ], + next_page_params: null, +}; + +export const erc721List: AddressTokensResponse = { + items: [ + erc721a, + erc721b, + erc721c, + ], + next_page_params: null, +}; + +export const erc1155List: AddressTokensResponse = { + items: [ + erc1155withoutName, + erc1155a, + erc1155b, + ], + next_page_params: null, +}; + +export const erc404List: AddressTokensResponse = { + items: [ + erc404a, + erc404b, + ], + next_page_params: null, +}; + +export const nfts: AddressNFTsResponse = { + items: [ + { + ...tokenInstance.base, + token: tokens.tokenInfoERC1155a, + token_type: 'ERC-1155', + value: '11', + }, + { + ...tokenInstance.unique, + token: tokens.tokenInfoERC721a, + token_type: 'ERC-721', + value: '1', + }, + { + ...tokenInstance.unique, + token: tokens.tokenInfoERC404, + token_type: 'ERC-404', + value: '11000', + }, + ], + next_page_params: null, +}; + +const nftInstance = { + ...tokenInstance.base, + token_type: 'ERC-1155', + value: '11', +}; + +const nftInstanceWithoutImage = { + ...nftInstance, + image_url: null, +}; + +export const collections: AddressCollectionsResponse = { + items: [ + { + token: tokens.tokenInfoERC1155a, + amount: '100', + token_instances: Array(5).fill(nftInstanceWithoutImage), + }, + { + token: tokens.tokenInfoERC20LongSymbol, + amount: '100', + token_instances: Array(5).fill(nftInstanceWithoutImage), + }, + { + token: tokens.tokenInfoERC1155WithoutName, + amount: '1', + token_instances: [ nftInstanceWithoutImage ], + }, + ], + next_page_params: { + token_contract_address_hash: '123', + token_type: 'ERC-1155', + }, +}; diff --git a/explorer/frontend/mocks/advancedFilter/advancedFilter.ts b/explorer/frontend/mocks/advancedFilter/advancedFilter.ts new file mode 100644 index 000000000..f1f7a35e6 --- /dev/null +++ b/explorer/frontend/mocks/advancedFilter/advancedFilter.ts @@ -0,0 +1,124 @@ +import type { AdvancedFilterResponse } from 'types/api/advancedFilter'; + +export const baseResponse: AdvancedFilterResponse = { + items: [ + { + timestamp: '2024-12-06T12:38:59.000000Z', + total: null, + type: 'coin_transfer', + value: '0', + hash: '0x35e5793d3da98d8e8e3944e40fa15028806502b53a2319501c6acdb8c83ed4bc', + from: { + ens_domain_name: null, + hash: '0xC1b634853Cb333D3aD8663715b08f41A3Aec47cc', + implementations: [], + is_contract: false, + is_verified: false, + metadata: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + token: null, + to: { + ens_domain_name: null, + hash: '0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6', + implementations: [ + { + address_hash: '0x31DA64D19Cd31A19CD09F4070366Fe2144792cf7', + name: 'SequencerInbox', + }, + ], + is_contract: true, + is_verified: true, + metadata: null, + name: 'TransparentUpgradeableProxy', + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + method: 'addSequencerL2BatchFromBlobs', + fee: '2657475294553624', + }, + { + timestamp: '2024-12-06T12:38:59.000000Z', + total: null, + type: 'coin_transfer', + value: '1328910000000000', + hash: '0x0d7a6c1e91540f767bc4d48bbcf2aa3fa22c93d0d8a60fb34bd7f0ecec5565b0', + from: { + ens_domain_name: null, + hash: '0x9BDc51980d3b81a0fBd031d0F0E39e9E1aFCB294', + implementations: [], + is_contract: false, + is_verified: false, + metadata: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + token: null, + to: { + ens_domain_name: null, + hash: '0xFe4cda7cc3603bdB9447cAd4A6550290AFeF6b38', + implementations: [], + is_contract: false, + is_verified: false, + metadata: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + method: null, + fee: '279416150328000', + }, + { + timestamp: '2024-12-06T12:38:59.000000Z', + total: null, + type: 'coin_transfer', + value: '0', + hash: '0x925bb2b7bf0b7a37ba4012bd718015cae29fa44e7846a7563c01f11ef99461e2', + from: { + ens_domain_name: null, + hash: '0x807Db16fd01766EE8A7040B6d32F4169c0A0Bf47', + implementations: [], + is_contract: false, + is_verified: false, + metadata: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + token: null, + to: { + ens_domain_name: null, + hash: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', + implementations: [], + is_contract: true, + is_verified: true, + metadata: null, + name: 'WstETH', + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + method: 'approve', + fee: '620080096879104', + }, + ], + next_page_params: { + block_number: 5867485, + internal_transaction_index: null, + token_transfer_index: null, + transaction_index: 208, + items_count: 50, + }, + search_params: { + tokens: {}, + methods: {}, + }, +}; diff --git a/explorer/frontend/mocks/apps/app.html b/explorer/frontend/mocks/apps/app.html new file mode 100644 index 000000000..c7c675b97 --- /dev/null +++ b/explorer/frontend/mocks/apps/app.html @@ -0,0 +1,32 @@ + + + + + Mock HTML Content + + + +

Full view app

+ + diff --git a/explorer/frontend/mocks/apps/apps.ts b/explorer/frontend/mocks/apps/apps.ts new file mode 100644 index 000000000..2f748c625 --- /dev/null +++ b/explorer/frontend/mocks/apps/apps.ts @@ -0,0 +1,29 @@ +/* eslint-disable max-len */ +export const apps = [ + { + author: 'Hop', + id: 'hop-exchange', + title: 'Hop', + logo: 'https://www.gitbook.com/cdn-cgi/image/width=288,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FfhJJGsR3RSfFmRoxfvqk%252FHop.png%3Falt%3Dmedia%26token%3D8107e45c-546c-4771-bbfe-e86bb0fe8c1a', + categories: [ 'Bridge' ], + shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + site: 'https://help.hop.exchange/hc/en-us/articles/4405172445197-What-is-Hop-Protocol-', + description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + external: true, + url: 'https://goerli.hop.exchange/send?token=ETH&sourceNetwork=ethereum', + github: [ 'https://github.com/hop-protocol/hop', 'https://github.com/hop-protocol/hop-ui' ], + discord: 'https://discord.gg/hopprotocol', + twitter: 'https://twitter.com/HopProtocol', + }, + { + author: 'Blockscout', + id: 'token-approval-tracker', + title: 'Token Approval Tracker', + logo: 'https://approval-tracker.apps.blockscout.com/icon-192.png', + 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://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', + 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://approval-tracker.apps.blockscout.com/', + }, +]; diff --git a/explorer/frontend/mocks/apps/ratings.ts b/explorer/frontend/mocks/apps/ratings.ts new file mode 100644 index 000000000..3a0322850 --- /dev/null +++ b/explorer/frontend/mocks/apps/ratings.ts @@ -0,0 +1,13 @@ +import { apps } from './apps'; + +export const ratings = { + records: [ + { + fields: { + appId: apps[0].id, + rating: 4.3, + count: 15, + }, + }, + ], +}; diff --git a/explorer/frontend/mocks/apps/securityReports.ts b/explorer/frontend/mocks/apps/securityReports.ts new file mode 100644 index 000000000..33457ddf2 --- /dev/null +++ b/explorer/frontend/mocks/apps/securityReports.ts @@ -0,0 +1,60 @@ +import { apps } from './apps'; + +export const securityReports = [ + { + appName: apps[0].id, + doc: 'http://docs.li.fi/smart-contracts/deployments#mainnet', + chainsData: { + '1': { + overallInfo: { + verifiedNumber: 1, + totalContractsNumber: 1, + solidityScanContractsNumber: 1, + securityScore: 87.5, + issueSeverityDistribution: { + critical: 4, + gas: 1, + high: 0, + informational: 4, + low: 2, + medium: 0, + }, + }, + contractsData: [ + { + address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + isVerified: true, + solidityScanReport: { + connection_id: '', + contract_address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + contract_chain: 'optimism', + contract_platform: 'blockscout', + contract_url: 'http://optimism.blockscout.com/address/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + contractname: 'LiFiDiamond', + is_quick_scan: true, + node_reference_id: null, + request_type: 'threat_scan', + scanner_reference_url: 'http://solidityscan.com/quickscan/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE/blockscout/eth?ref=blockscout', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 4, + low: 2, + medium: 0, + }, + lines_analyzed_count: 72, + scan_time_taken: 1, + score: '4.38', + score_v2: '87.50', + threat_score: '100.00', + }, + }, + }, + ], + }, + }, + }, +]; diff --git a/explorer/frontend/mocks/arbitrum/deposits.ts b/explorer/frontend/mocks/arbitrum/deposits.ts new file mode 100644 index 000000000..88f8d4901 --- /dev/null +++ b/explorer/frontend/mocks/arbitrum/deposits.ts @@ -0,0 +1,46 @@ +import type { ArbitrumL2MessagesResponse, ArbitrumLatestDepositsResponse } from 'types/api/arbitrumL2'; + +export const baseResponse: ArbitrumL2MessagesResponse = { + items: [ + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181920, + origination_address_hash: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123456, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'initiated', + }, + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181921, + origination_address_hash: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123400, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'relayed', + }, + ], + next_page_params: { + items_count: 50, + id: 123, + direction: 'to-rollup', + }, +}; + +export const latestDepositsResponse: ArbitrumLatestDepositsResponse = { + items: [ + { + completion_transaction_hash: '0x3ccdf87449d3de6a9dcd3eddb7bc9ecdf1770d4631f03cdf12a098911618d138', + origination_transaction_block_number: 123400, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + }, + { + completion_transaction_hash: '0xd16d918b2f95a5cdf66824f6291b6d5eb80b6f4acab3f9fb82ee0ec4109646a0', + origination_timestamp: null, + origination_transaction_block_number: null, + origination_transaction_hash: null, + }, + ], +}; diff --git a/explorer/frontend/mocks/arbitrum/txnBatch.ts b/explorer/frontend/mocks/arbitrum/txnBatch.ts new file mode 100644 index 000000000..46a849fca --- /dev/null +++ b/explorer/frontend/mocks/arbitrum/txnBatch.ts @@ -0,0 +1,53 @@ +/* eslint-disable max-len */ +import type { ArbitrumL2TxnBatch } from 'types/api/arbitrumL2'; + +import { finalized } from './txnBatches'; + +export const batchData: ArbitrumL2TxnBatch = { + ...finalized, + after_acc_hash: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb', + before_acc_hash: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc', + start_block_number: 1245209, + end_block_number: 1245490, + data_availability: { + batch_data_container: 'in_blob4844', + }, +}; + +export const batchDataAnytrust: ArbitrumL2TxnBatch = { + ...finalized, + after_acc_hash: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb', + before_acc_hash: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc', + start_block_number: 1245209, + end_block_number: 1245490, + data_availability: { + batch_data_container: 'in_anytrust', + bls_signature: '0x142577943e30b1ad1b4e40a1c08e00c24a68d6c366f953e361048b7127e327b5bdb8f168ba986beae40cfaf79ea2788004d750555684751e361d6f6445e5c521b45ac93a76da24add241a4a5410ca3a09fa82cf0aafd78801cbd0ad99d5be6b3', + data_hash: '0x4ffada101d8185bcba227f2cff9e0ea0a4deeb08f328601a898131429a436ebe', + timeout: '2024-08-22T12:39:22Z', + signers: [ + { + key: '0x0c6694955b524d718ca445831c5375393773401f33725a79661379dddabd5fff28619dc070befd9ed73d699e5c236c1a163be58ba81002b6130709bc064af5d7ba947130b72056bf17263800f1a3ab2269c6a510ef8e7412fd56d1ef1b916a1306e3b1d9c82c099371bd9861582acaada3a16e9dfee5d0ebce61096598a82f112d0a935e8cab5c48d82e3104b0c7ba79157dad1a019a3e7f6ad077b8e6308b116fec0f58239622463c3631fa01e2b4272409215b8009422c16715dbede590906', + proof: '0x06dcb5e56764bb72e6a45e6deb301ca85d8c4315c1da2efa29927f2ac8fb25571ce31d2d603735fe03196f6d56bcbf9a1999a89a74d5369822c4445d676c15ed52e5008daa775dc9a839c99ff963a19946ac740579874dac4f639907ae1bc69f', + trusted: false, + }, + { + key: '0x0ee5aaeabd57313285207eb89366b411286cf3f1c5e30eb7e355f55385308b91d5807284323ee89a9743c70676f4949504ced3ed41612cbfda06ad55200c1c77d3fb3700059befd64c44bc4a57cb567ec1481ee564cf6cd6cf1f2f4a2dee6db00c547c38400ab118dedae8afd5bab93b703f76a0991baa5d43fbb125194c06b5461f8c738a3c4278a3d98e5456aec0720883c0d28919537a36e2ffd5f731e742b6653557d154c164e068ef983b367ef626faaed46f4eadecbb12b7e55f23175d', + trusted: true, + }, + ], + }, +}; + +export const batchDataCelestia: ArbitrumL2TxnBatch = { + ...finalized, + after_acc_hash: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb', + before_acc_hash: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc', + start_block_number: 1245209, + end_block_number: 1245490, + data_availability: { + batch_data_container: 'in_celestia', + height: 4520041, + transaction_commitment: '0x3ebe5a43f47fbf69db003e543bb27e4875929ede2fa9a25d09f0bd082d5d20f0', + }, +}; diff --git a/explorer/frontend/mocks/arbitrum/txnBatches.ts b/explorer/frontend/mocks/arbitrum/txnBatches.ts new file mode 100644 index 000000000..54ab913fa --- /dev/null +++ b/explorer/frontend/mocks/arbitrum/txnBatches.ts @@ -0,0 +1,39 @@ +import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatchesResponse } from 'types/api/arbitrumL2'; + +export const finalized: ArbitrumL2TxnBatchesItem = { + number: 12345, + blocks_count: 12345, + transactions_count: 10000, + commitment_transaction: { + block_number: 12345, + timestamp: '2022-04-17T08:51:58.000000Z', + hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + status: 'finalized', + }, + batch_data_container: 'in_blob4844', +}; + +export const unfinalized: ArbitrumL2TxnBatchesItem = { + number: 12344, + blocks_count: 10000, + transactions_count: 103020, + commitment_transaction: { + block_number: 12340, + timestamp: '2022-04-17T08:51:58.000000Z', + hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + status: 'unfinalized', + }, + batch_data_container: null, + +}; + +export const baseResponse: ArbitrumL2TxnBatchesResponse = { + items: [ + finalized, + unfinalized, + ], + next_page_params: { + items_count: 50, + number: 123, + }, +}; diff --git a/explorer/frontend/mocks/arbitrum/txnWithdrawals.ts b/explorer/frontend/mocks/arbitrum/txnWithdrawals.ts new file mode 100644 index 000000000..49e7859c7 --- /dev/null +++ b/explorer/frontend/mocks/arbitrum/txnWithdrawals.ts @@ -0,0 +1,36 @@ +import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2'; + +export const unclaimed: ArbitrumL2TxnWithdrawalsItem = { + arb_block_number: 115114348, + caller_address_hash: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d', + callvalue: '21000000000000000000', + completion_transaction_hash: null, + data: '0x', + destination_address_hash: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d', + eth_block_number: 7503173, + id: 59874, + l2_timestamp: 1737020350, + status: 'confirmed', + token: null, +}; + +export const claimed: ArbitrumL2TxnWithdrawalsItem = { + arb_block_number: 115114348, + caller_address_hash: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d', + callvalue: '21000000000000000000', + completion_transaction_hash: '0x215382498438cb6532a5e5fb07d664bbf912187866591470d47c3cfbce2dc4a8', + data: '0x', + destination_address_hash: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d', + eth_block_number: 7503173, + id: 59875, + l2_timestamp: 1737020350, + status: 'relayed', + token: { + address_hash: '0x0000000000000000000000000000000000000000', + symbol: 'USDC', + name: 'USDC Token', + decimals: 6, + amount: '10000000000', + destination_address_hash: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d', + }, +}; diff --git a/explorer/frontend/mocks/arbitrum/withdrawals.ts b/explorer/frontend/mocks/arbitrum/withdrawals.ts new file mode 100644 index 000000000..610a1772a --- /dev/null +++ b/explorer/frontend/mocks/arbitrum/withdrawals.ts @@ -0,0 +1,29 @@ +import type { ArbitrumL2MessagesResponse } from 'types/api/arbitrumL2'; + +export const baseResponse: ArbitrumL2MessagesResponse = { + items: [ + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181920, + origination_address_hash: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123456, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'sent', + }, + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181921, + origination_address_hash: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123400, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'confirmed', + }, + ], + next_page_params: { + items_count: 50, + id: 123, + direction: 'from-rollup', + }, +}; diff --git a/explorer/frontend/mocks/blobs/blobs.ts b/explorer/frontend/mocks/blobs/blobs.ts new file mode 100644 index 000000000..24d25b465 --- /dev/null +++ b/explorer/frontend/mocks/blobs/blobs.ts @@ -0,0 +1,36 @@ +import type { Blob, TxBlobs } from 'types/api/blobs'; + +export const base1: Blob = { + blob_data: '0x004242004242004242004242004242004242', + hash: '0x016316f61a259aa607096440fc3eeb90356e079be01975d2fb18347bd50df33c', + kzg_commitment: '0xa95caabd009e189b9f205e0328ff847ad886e4f8e719bd7219875fbb9688fb3fbe7704bb1dfa7e2993a3dea8d0cf767d', + kzg_proof: '0x89cf91c4c8be6f2a390d4262425f79dffb74c174fb15a210182184543bf7394e5a7970a774ee8e0dabc315424c22df0f', + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x970d8c45c713a50a1fa351b00ca29a8890cac474c59cc8eee4eddec91a1729f0' }, + ], +}; + +export const base2: Blob = { + blob_data: '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403', + hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd1', + kzg_commitment: '0x89b0d8ac715ee134135471994a161ef068a784f51982fcd7161aa8e3e818eb83017ccfbfc30c89b796a2743d77554e2f', + kzg_proof: '0x8255a6c6a236483814b8e68992e70f3523f546866a9fed6b8e0ecfef314c65634113b8aa02d6c5c6e91b46e140f17a07', + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' }, + ], +}; + +export const withoutData: Blob = { + blob_data: null, + hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd3', + kzg_commitment: null, + kzg_proof: null, + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' }, + ], +}; + +export const txBlobs: TxBlobs = { + items: [ base1, base2, withoutData ], + next_page_params: null, +}; diff --git a/explorer/frontend/mocks/blocks/block.ts b/explorer/frontend/mocks/blocks/block.ts new file mode 100644 index 000000000..135c9497c --- /dev/null +++ b/explorer/frontend/mocks/blocks/block.ts @@ -0,0 +1,336 @@ +/* eslint-disable max-len */ +import type { RpcBlock } from 'viem'; + +import type { Block, BlocksResponse, ZilliqaBlockData } from 'types/api/block'; + +import { ZERO_ADDRESS } from 'toolkit/utils/consts'; + +import * as addressMock from '../address/address'; +import * as tokenMock from '../tokens/tokenInfo'; + +export const base: Block = { + base_fee_per_gas: '10000000000', + burnt_fees: '5449200000000000', + burnt_fees_percentage: 20.292245650793845, + difficulty: '340282366920938463463374607431768211454', + extra_data: 'TODO', + gas_limit: '12500000', + gas_target_percentage: -91.28128, + gas_used: '544920', + gas_used_percentage: 4.35936, + hash: '0xccc75136de485434d578b73df66537c06b34c3c9b12d085daf95890c914fc2bc', + height: 30146364, + miner: { + hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41', + implementations: null, + is_contract: false, + is_verified: null, + name: 'Alex Emelyanov', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + nonce: '0x0000000000000000', + parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f', + priority_fee: '23211757500000000', + rewards: [ + { + reward: '500000000000000000', + type: 'POA Mania Reward', + }, + { + reward: '1026853607510000000', + type: 'Validator Reward', + }, + { + reward: '500000000000000000', + type: 'Emission Reward', + }, + ], + size: 2448, + state_root: 'TODO', + timestamp: '2022-11-11T11:59:35Z', + total_difficulty: '10258276095980170141167591583995189665817672619', + transactions_count: 5, + transaction_fees: '26853607500000000', + type: 'block', + uncles_hashes: [], +}; + +export const genesis: Block = { + base_fee_per_gas: null, + burnt_fees: null, + burnt_fees_percentage: null, + difficulty: '131072', + extra_data: 'TODO', + gas_limit: '6700000', + gas_target_percentage: -100, + gas_used: '0', + gas_used_percentage: 0, + hash: '0x39f02c003dde5b073b3f6e1700fc0b84b4877f6839bb23edadd3d2d82a488634', + height: 0, + miner: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + nonce: '0x0000000000000000', + parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000', + priority_fee: null, + rewards: [], + size: 533, + state_root: 'TODO', + timestamp: '2017-12-16T00:13:24.000000Z', + total_difficulty: '131072', + transactions_count: 0, + transaction_fees: '0', + type: 'block', + uncles_hashes: [], +}; + +export const base2: Block = { + ...base, + height: base.height - 1, + size: 592, + miner: { + hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165', + implementations: null, + is_contract: false, + is_verified: null, + name: 'Kiryl Ihnatsyeu', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + timestamp: '2022-11-11T11:46:05Z', + transactions_count: 253, + gas_target_percentage: 23.6433, + gas_used: '6333342', + gas_used_percentage: 87.859504, + burnt_fees: '232438000000000000', + burnt_fees_percentage: 65.3333333333334, + rewards: [ + { + reward: '500000000000000000', + type: 'Chore Reward', + }, + { + reward: '1017432850000000000', + type: 'Miner Reward', + }, + { + reward: '500000000000000000', + type: 'Emission Reward', + }, + ], +}; + +export const rootstock: Block = { + ...base, + bitcoin_merged_mining_coinbase_transaction: '0x0000000000000080a1219cea298d65d545b56abafe7c5421edfaf084cf9e374bb23ea985ebd86b206088ac0000000000000000266a24aa21a9edb2ac3022ad2a5327449f029b6aa3d2e55605061b5d8171b30abf5b330d1959c900000000000000002a6a52534b424c4f434b3a481d071e57c6c47cb8eb716295a7079b15859962abf35e32f107b21f003f0bb900000000', + bitcoin_merged_mining_header: '0x000000204a7e42cadf8b5b0a094755c5a13298e596d61f361c6d31171a00000000000000970e51977cd6f82bab9ed62e678c8d8ca664af9d5c3b5cea39d5d4337c7abedae334c9649fc63e1982a84aaa', + bitcoin_merged_mining_merkle_proof: '0x09f386e5e6feb20706a1b5d0817eae96f0ebb0d713eeefe6d5625afc6fd87fcdfe8cc9118bb49e32db87f8e928dcb13dd327b526ced76fb9de0115a5dca8d2a9657c929360ad07418fc7e1a3120da27e0002470d0c98c9b8b5b2835e64e379421d2469204533307bf0c5a087d93fd1dfb3aaea3ee83099928860f6cca891cf59d73c4e3c6053ea4b385dce39067e87c28805ddd89c4ff10500401bec7c248f749ad6f0933e6ad270e447d01711aca1cc26d7989ee59e1431fd2fd5d058edca6d', + hash_for_merged_mining: '0x481d071e57c6c47cb8eb716295a7079b15859962abf35e32f107b21f003f0bb9', + minimum_gas_price: '59240000', +}; + +export const celo: Block = { + ...base, + celo: { + base_fee: { + token: tokenMock.tokenInfoERC20a, + amount: '445690000000000', + breakdown: [ + { + address: addressMock.withName, + amount: '356552000000000.0000000000000', + percentage: 80, + }, + { + address: { + ...addressMock.withoutName, + hash: ZERO_ADDRESS, + }, + amount: '89138000000000.0000000000000', + percentage: 20, + }, + ], + recipient: addressMock.contract, + }, + epoch_number: 1486, + is_epoch_block: true, + }, +}; + +export const zilliqaWithAggregateQuorumCertificate: Block = { + ...base, + zilliqa: { + view: 1137735, + aggregate_quorum_certificate: { + signature: '0x82d29e8f06adc890f6574c3d0ae0c811de1db695b05ed2755ef384fe21bc44f6505b99e201f6000a65f38ff6a13e286306d0e380ef1b43a273eb9947b3f11f852e14b93c258c32b516f89696fcb1190b147364b789572ebdf85d79c4cf3cbbbb', + view: 1137735, + signers: [ 1, 2, 3, 8 ], + nested_quorum_certificates: [ + { + signature: '0xaeb3567577f9db68565c6f97c158b17522620a9684c6f6beaa78920951ad4cae0f287b630bdd034c4a4f89ada42e3dbe012985e976a6f64057d735a4531a26b4e46c182414eabbe625e5b15e6645be5b6522bdec113df408874f6d1e0d894dca', + view: 1137732, + proposed_by_validator_index: 1, + signers: [ 3, 8 ], + }, + { + signature: '0xaeb3567577f9db68565c6f97c158b17522620a9684c6f6beaa78920951ad4cae0f287b630bdd034c4a4f89ada42e3dbe012985e976a6f64057d735a4531a26b4e46c182414eabbe625e5b15e6645be5b6522bdec113df408874f6d1e0d894dca', + view: 1137732, + proposed_by_validator_index: 2, + signers: [ 0, 2 ], + }, + ], + }, + quorum_certificate: { + signature: '0xaeb3567577f9db68565c6f97c158b17522620a9684c6f6beaa78920951ad4cae0f287b630bdd034c4a4f89ada42e3dbe012985e976a6f64057d735a4531a26b4e46c182414eabbe625e5b15e6645be5b6522bdec113df408874f6d1e0d894dca', + view: 1137732, + signers: [ 0, 2, 3, 8 ], + }, + }, +}; + +export const zilliqaWithoutAggregateQuorumCertificate: Block = { + ...base, + zilliqa: { + ...zilliqaWithAggregateQuorumCertificate.zilliqa, + aggregate_quorum_certificate: null, + } as ZilliqaBlockData, +}; + +export const withBlobTxs: Block = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '393216', + burnt_blob_fees: '8461393325064192', + excess_blob_gas: '79429632', + blob_transaction_count: 1, +}; + +export const withWithdrawals: Block = { + ...base, + withdrawals_count: 2, +}; + +export const baseListResponse: BlocksResponse = { + items: [ + base, + base2, + ], + next_page_params: null, +}; + +export const rpcBlockBase: RpcBlock = { + difficulty: '0x37fcc04bef8', + extraData: '0x476574682f76312e302e312d38326566323666362f6c696e75782f676f312e34', + gasLimit: '0x2fefd8', + gasUsed: '0x0', + hash: '0xfbafb4b7b6f6789338d15ff046f40dc608a42b1a33b093e109c6d7a36cd76f61', + logsBloom: '0x0', + miner: '0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca', + mixHash: '0x038956b9df89d0c1f980fd656d045e912beafa515cff7d7fd3c5f34ffdcb9e4b', + nonce: '0xd8d3392f340bbb22', + number: '0x1869f', + parentHash: '0x576fd45e598c9f86835f50fe2c6e6d11df2d4c4b01f19e4241b7e793d852f9e4', + receiptsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x225', + stateRoot: '0x32356228651d64cc5e6e7be87a556ecdbf40e876251dc867ba9e4bb82a0124a3', + timestamp: '0x55d19741', + totalDifficulty: '0x259e89748daae17', + transactions: [ + '0x0e70849f10e22fe2e53fe6755f86a572aa6bb2fc472f0b87d9e561efa1fc2e1f', + '0xae5624c77f06d0164301380afa7780ebe49debe77eb3d5167004d69bd188a09f', + ], + transactionsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + uncles: [], + baseFeePerGas: null, + blobGasUsed: `0x0`, + excessBlobGas: `0x0`, + sealFields: [], + withdrawals: [ + { address: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amount: '0x12128cd', index: '0x3216bbb', validatorIndex: '0x4dca3' }, + { address: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amount: '0x12027dd', index: '0x3216bbc', validatorIndex: '0x4dca4' }, + ], +}; + +export const rpcBlockWithTxsInfo: RpcBlock = { + ...rpcBlockBase, + transactions: [ + { + accessList: [ + { + address: '0x7af661a6463993e05a171f45d774cf37e761c83f', + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000007', + '0x000000000000000000000000000000000000000000000000000000000000000c', + '0x0000000000000000000000000000000000000000000000000000000000000008', + '0x0000000000000000000000000000000000000000000000000000000000000006', + '0x0000000000000000000000000000000000000000000000000000000000000009', + '0x000000000000000000000000000000000000000000000000000000000000000a', + ], + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + storageKeys: [ + '0x0d726f311404f8052d44e7004a6ffb747709a6d3666a62ce3f5aad13374680ab', + '0x1a824a6850dcbd9223afea4418727593881e2911ed2e734272a263153159fe26', + '0xfae3a383c82daf853bbd8bbcd21280410599b135c274c01354ea7d3a5e09f43c', + ], + }, + ], + blockHash: '0xeb37ebc94e31773e5c5703073fd3911b2ab596f099d00d18b55ae3ac8203c1d5', + blockNumber: '0x136058d', + chainId: '0x1', + from: '0x111527f1386c6725a2f5986230f3060bdcac041f', + gas: '0xf4240', + gasPrice: '0x1780b2ff9', + hash: '0x0e70849f10e22fe2e53fe6755f86a572aa6bb2fc472f0b87d9e561efa1fc2e1f', + input: '0x258d7af661a6463993e05a171f45d774cf37e761c83f402ab3277301b3574863a151d042dc870fb1b3f0c72cbbdd53a85898f62415fe124406f6608d8802269d1283cdb2a5a329649e5cb4cdcee91ab6', + // maxFeePerGas: '0x3ac1bf7ee', + // maxPriorityFeePerGas: '0x0', + nonce: '0x127b2', + r: '0x3c47223f880a3fb7b1eca368d9d7320d2278f0b679109d9ed0af4080ee386f23', + s: '0x587a441f9472b312ff302d7132547aa250ea06c6203c76831d56a46ec188e664', + to: '0x000000d40b595b94918a28b27d1e2c66f43a51d3', + transactionIndex: '0x0', + type: '0x1', + v: '0x1', + value: '0x31', + yParity: '0x1', + }, + { + accessList: [], + blockHash: '0xeb37ebc94e31773e5c5703073fd3911b2ab596f099d00d18b55ae3ac8203c1d5', + blockNumber: '0x136058d', + chainId: '0x1', + from: '0xe25d2cb47b606bb6fd9272125457a7230e26f956', + gas: '0x47bb0', + gasPrice: '0x1ba875cb6', + hash: '0xae5624c77f06d0164301380afa7780ebe49debe77eb3d5167004d69bd188a09f', + input: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006696237b00000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000b1a2bc2ec500000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000000006d1aaedfab0f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d422990000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d42299000000000000000000000000000000fee13a103a10d593b9ae06b3e05f2e7e1c00000000000000000000000000000000000000000000000000000000000000190000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d42299000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000006cd4db3c8c8d', + // maxFeePerGas: '0x23493c9cd', + // maxPriorityFeePerGas: '0x427c2cbd', + nonce: '0x32b', + r: '0x6566181b3cfd01702b24a2124ea7698b8cc815c7f37d1ea55800f176ca7a94cf', + s: '0x34f8dd837f37746ccd18f4fa71e05de98a2212f1c931f740598e491518616bb3', + to: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + transactionIndex: '0x1', + type: '0x1', + v: '0x1', + value: '0xb1a2bc2ec50000', + yParity: '0x1', + }, + ], +}; diff --git a/explorer/frontend/mocks/blocks/epoch.ts b/explorer/frontend/mocks/blocks/epoch.ts new file mode 100644 index 000000000..165660c7c --- /dev/null +++ b/explorer/frontend/mocks/blocks/epoch.ts @@ -0,0 +1,57 @@ +import { padStart } from 'es-toolkit/compat'; + +import type { BlockEpoch, BlockEpochElectionRewardDetails, BlockEpochElectionRewardDetailsResponse } from 'types/api/block'; + +import * as addressMock from '../address/address'; +import * as tokenMock from '../tokens/tokenInfo'; +import * as tokenTransferMock from '../tokens/tokenTransfer'; + +export const blockEpoch1: BlockEpoch = { + number: 1486, + distribution: { + carbon_offsetting_transfer: tokenTransferMock.erc20, + community_transfer: tokenTransferMock.erc20, + reserve_bolster_transfer: null, + }, + aggregated_election_rewards: { + delegated_payment: { + count: 0, + total: '71210001063118670575', + token: tokenMock.tokenInfoERC20d, + }, + group: { + count: 10, + total: '157705500305820107521', + token: tokenMock.tokenInfoERC20b, + }, + validator: { + count: 10, + total: '1348139501689262297152', + token: tokenMock.tokenInfoERC20c, + }, + voter: { + count: 38, + total: '2244419545166303388', + token: tokenMock.tokenInfoERC20a, + }, + }, +}; + +function getRewardDetailsItem(index: number): BlockEpochElectionRewardDetails { + return { + amount: `${ 100 - index }210001063118670575`, + account: { + ...addressMock.withoutName, + hash: `0x30D060F129817c4DE5fBc1366d53e19f43c8c6${ padStart(String(index), 2, '0') }`, + }, + associated_account: { + ...addressMock.withoutName, + hash: `0x456f41406B32c45D59E539e4BBA3D7898c3584${ padStart(String(index), 2, '0') }`, + }, + }; +} + +export const electionRewardDetails1: BlockEpochElectionRewardDetailsResponse = { + items: Array(15).fill('').map((item, index) => getRewardDetailsItem(index)), + next_page_params: null, +}; diff --git a/explorer/frontend/mocks/config/footerLinks.ts b/explorer/frontend/mocks/config/footerLinks.ts new file mode 100644 index 000000000..5ec3bdf9b --- /dev/null +++ b/explorer/frontend/mocks/config/footerLinks.ts @@ -0,0 +1,93 @@ +import type { CustomLinksGroup } from 'types/footerLinks'; + +export const FOOTER_LINKS: Array = [ + { + title: 'Company', + links: [ + { + text: 'Advertise', + url: 'https://coinzilla.com/', + }, + { + text: 'Staking', + url: '', + }, + { + text: 'Contact us', + url: '', + }, + { + text: 'Brand assets', + url: '', + }, + { + text: 'Term of service', + url: '', + }, + ], + }, + { + title: 'Community', + links: [ + { + text: 'API docs', + url: '', + }, + { + text: 'Knowledge base', + url: '', + }, + { + text: 'Network status', + url: '', + }, + { + text: 'Learn Alphabet', + url: '', + }, + ], + }, + { + title: 'Product', + links: [ + { + text: 'Stake Alphabet', + url: '', + }, + { + text: 'Build token', + url: '', + }, + { + text: 'Build DAPPS', + url: '', + }, + { + text: 'NFT marketplace', + url: '', + }, + { + text: 'Become validator', + url: '', + }, + + ], + }, + { + title: 'Partners', + links: [ + { + text: 'MetaDock', + url: 'https://blocksec.com/metadock', + }, + { + text: 'Sourcify', + url: 'https://sourcify.dev/', + }, + { + text: 'DRPC', + url: 'https://drpc.org?ref=559183', + }, + ], + }, +]; diff --git a/explorer/frontend/mocks/config/network.ts b/explorer/frontend/mocks/config/network.ts new file mode 100644 index 000000000..f5dbcf115 --- /dev/null +++ b/explorer/frontend/mocks/config/network.ts @@ -0,0 +1,15 @@ +import type { FeaturedNetwork } from 'types/networks'; + +export const FEATURED_NETWORKS: Array = [ + { title: 'Gnosis Chain', url: 'https://blockscout.com/xdai/mainnet', group: 'Mainnets', isActive: true }, + { title: 'Arbitrum on xDai', url: 'https://blockscout.com/xdai/aox', group: 'Mainnets' }, + { title: 'Ethereum', url: 'https://blockscout.com/eth/mainnet', group: 'Mainnets' }, + { title: 'Ethereum Classic', url: 'https://blockscout.com/etx/mainnet', group: 'Mainnets', icon: 'https://localhost:3000/my-logo.png' }, + { title: 'POA', url: 'https://blockscout.com/poa/core', group: 'Mainnets' }, + { title: 'RSK', url: 'https://blockscout.com/rsk/mainnet', group: 'Mainnets' }, + { title: 'Gnosis Chain Testnet', url: 'https://blockscout.com/xdai/testnet', group: 'Testnets' }, + { title: 'POA Sokol', url: 'https://blockscout.com/poa/sokol', group: 'Testnets' }, + { title: 'ARTIS Σ1', url: 'https://blockscout.com/artis/sigma1', group: 'Other' }, + { title: 'LUKSO L14', url: 'https://blockscout.com/lukso/l14', group: 'Other' }, + { title: 'Astar', url: 'https://blockscout.com/astar', group: 'Other' }, +]; diff --git a/explorer/frontend/mocks/contract/audits.ts b/explorer/frontend/mocks/contract/audits.ts new file mode 100644 index 000000000..a2a664422 --- /dev/null +++ b/explorer/frontend/mocks/contract/audits.ts @@ -0,0 +1,16 @@ +import type { SmartContractSecurityAudits } from 'types/api/contract'; + +export const contractAudits: SmartContractSecurityAudits = { + items: [ + { + audit_company_name: 'OpenZeppelin', + audit_publish_date: '2023-03-01', + audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit', + }, + { + audit_company_name: 'OpenZeppelin', + audit_publish_date: '2023-03-01', + audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit', + }, + ], +}; diff --git a/explorer/frontend/mocks/contract/info.ts b/explorer/frontend/mocks/contract/info.ts new file mode 100644 index 000000000..b9a06aa24 --- /dev/null +++ b/explorer/frontend/mocks/contract/info.ts @@ -0,0 +1,149 @@ +/* eslint-disable max-len */ +import type { SmartContract } from 'types/api/contract'; + +export const verified: SmartContract = { + abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ], + can_be_visualized_via_sol2uml: true, + compiler_version: 'v0.5.16+commit.9c3226ce', + constructor_args: 'constructor_args', + creation_bytecode: 'creation_bytecode', + deployed_bytecode: 'deployed_bytecode', + compiler_settings: { + evmVersion: 'london', + remappings: [ + '@openzeppelin/=node_modules/@openzeppelin/', + ], + }, + evm_version: 'default', + is_verified: true, + is_blueprint: false, + name: 'WPOA', + optimization_enabled: true, + optimization_runs: 1500, + source_code: 'source_code', + verified_at: '2021-08-03T10:40:41.679421Z', + decoded_constructor_args: [ + [ '0xc59615da2da226613b1c78f0c6676cac497910bc', { internalType: 'address', name: '_token', type: 'address' } ], + [ [ 1800, 3600, 7200 ], { internalType: 'uint256[]', name: '_durations', type: 'uint256[]' } ], + [ '900000000', { internalType: 'uint256', name: '_totalSupply', type: 'uint256' } ], + ], + external_libraries: [ + { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' }, + { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, + ], + language: 'solidity', + license_type: 'gnu_gpl_v3', + is_self_destructed: false, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + file_path: '', + additional_sources: [], + verified_twin_address_hash: null, +}; + +export const certified: SmartContract = { + ...verified, + certified: true, +}; + +export const withMultiplePaths: SmartContract = { + ...verified, + file_path: './simple_storage.sol', + additional_sources: [ + { + file_path: '/contracts/protocol/libraries/logic/GenericLogic.sol', + source_code: '// SPDX-License-Identifier: GPL-3.0 \n pragma solidity >=0.7.0 <0.9.0; \n contract Storage {\n //2112313123; \nuint256 number; \n function store(uint256 num) public {\nnumber = num;\n}\n function retrieve() public view returns (uint256)\n {\nreturn number;\n}\n}', + }, + ], +}; + +export const verifiedViaSourcify: SmartContract = { + ...verified, + is_verified_via_sourcify: true, + is_fully_verified: false, + is_partially_verified: true, + sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/', +}; + +export const verifiedViaEthBytecodeDb: SmartContract = { + ...verified, + is_verified_via_eth_bytecode_db: true, +}; + +export const withTwinAddress: SmartContract = { + ...verified, + is_verified: false, + verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', +}; + +export const withProxyAddress: SmartContract = { + ...verified, + is_verified: false, + verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', +}; + +export const selfDestructed: SmartContract = { + ...verified, + is_self_destructed: true, +}; + +export const withChangedByteCode: SmartContract = { + ...verified, + is_changed_bytecode: true, + is_blueprint: true, +}; + +export const zkSync: SmartContract = { + ...verified, + zk_compiler_version: 'v1.2.5', + optimization_enabled: true, + optimization_runs: 's', +}; + +export const stylusRust: SmartContract = { + ...verified, + language: 'stylus_rust', + github_repository_metadata: { + commit: 'af5029f822815e32def0015bf8e591e769c62f34', + path_prefix: 'examples/erc20', + repository_url: 'https://github.com/blockscout/cargo-stylus-test-examples', + }, + compiler_version: 'v0.5.6', + package_name: 'erc20', + evm_version: null, +}; + +export const nonVerified: SmartContract = { + is_verified: false, + is_blueprint: false, + creation_bytecode: 'creation_bytecode', + deployed_bytecode: 'deployed_bytecode', + is_self_destructed: false, + abi: null, + compiler_version: null, + evm_version: null, + optimization_enabled: null, + optimization_runs: null, + name: null, + verified_at: null, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + source_code: null, + constructor_args: null, + decoded_constructor_args: null, + can_be_visualized_via_sol2uml: null, + file_path: '', + additional_sources: [], + external_libraries: null, + verified_twin_address_hash: null, + language: null, + license_type: null, +}; diff --git a/explorer/frontend/mocks/contract/methods.ts b/explorer/frontend/mocks/contract/methods.ts new file mode 100644 index 000000000..5fa714921 --- /dev/null +++ b/explorer/frontend/mocks/contract/methods.ts @@ -0,0 +1,134 @@ +import type { SmartContractMethodRead, SmartContractMethodWrite } from 'ui/address/contract/methods/types'; + +export const read: Array = [ + { + constant: true, + inputs: [ + { internalType: 'address', name: 'wallet', type: 'address' }, + ], + method_id: '70a08231', + name: 'FLASHLOAN_PREMIUM_TOTAL', + outputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + method_id: '06fdde03', + name: 'name', + outputs: [ + { internalType: 'string', name: '', type: 'string' }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, +]; + +export const write: Array = [ + { + payable: true, + stateMutability: 'payable', + type: 'fallback', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'guy', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'setReserveInterestRateStrategyAddress', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x01', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'src', type: 'address' }, + { internalType: 'address', name: 'dst', type: 'address' }, + ], + name: 'transferFrom', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + ], + payable: true, + stateMutability: 'payable', + type: 'function', + method_id: '0x02', + }, + { + stateMutability: 'payable', + type: 'receive', + }, + { + constant: false, + inputs: [], + name: 'pause', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x03', + }, + { + constant: false, + inputs: [ + { name: '_from', type: 'address' }, + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' }, + { name: '_data', type: 'bytes' }, + ], + name: 'safeTransferFrom', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x04', + }, + { + constant: false, + inputs: [ + { name: '_tokenId', type: 'uint256' }, + { name: '_hash', type: 'bytes32' }, + { name: '_keepRequestToken', type: 'bool' }, + { name: '_newOwner', type: 'address' }, + { name: '_signature', type: 'bytes' }, + ], + name: 'requestToken', + outputs: [ + { name: 'reward', type: 'uint256' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x05', + }, + { + constant: false, + inputs: [ + { name: '_tokenId', type: 'uint256' }, + { name: '_imprint', type: 'bytes32' }, + { name: '_uri', type: 'string' }, + { name: '_initialKey', type: 'address' }, + { name: '_tokenRecoveryTimestamp', type: 'uint256' }, + { name: '_initialKeyIsRequestKey', type: 'bool' }, + ], + name: 'hydrateToken', + outputs: [ + { name: '', type: 'uint256' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + is_invalid: true, + }, +]; diff --git a/explorer/frontend/mocks/contract/solidityscanReport.ts b/explorer/frontend/mocks/contract/solidityscanReport.ts new file mode 100644 index 000000000..e3d83d590 --- /dev/null +++ b/explorer/frontend/mocks/contract/solidityscanReport.ts @@ -0,0 +1,58 @@ +import type { SolidityScanReport } from 'lib/solidityScan/schema'; + +export const solidityscanReportAverage: SolidityScanReport = { + scan_report: { + contractname: 'foo', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 0, + low: 2, + medium: 0, + }, + score_v2: '72.22', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; + +export const solidityscanReportGreat: SolidityScanReport = { + scan_report: { + contractname: 'foo', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 0, + high: 0, + informational: 0, + low: 0, + medium: 0, + }, + score_v2: '100', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; + +export const solidityscanReportLow: SolidityScanReport = { + scan_report: { + contractname: 'foo', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 2, + gas: 1, + high: 3, + informational: 0, + low: 2, + medium: 10, + }, + score_v2: '22.22', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; diff --git a/explorer/frontend/mocks/contracts/counters.ts b/explorer/frontend/mocks/contracts/counters.ts new file mode 100644 index 000000000..fb3326227 --- /dev/null +++ b/explorer/frontend/mocks/contracts/counters.ts @@ -0,0 +1,8 @@ +import type { VerifiedContractsCounters } from 'types/api/contracts'; + +export const verifiedContractsCountersMock: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '0', +}; diff --git a/explorer/frontend/mocks/contracts/index.ts b/explorer/frontend/mocks/contracts/index.ts new file mode 100644 index 000000000..32441a598 --- /dev/null +++ b/explorer/frontend/mocks/contracts/index.ts @@ -0,0 +1,85 @@ +import type { VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts'; + +export const contract1: VerifiedContract = { + address: { + hash: '0xef490030ac0d53B70E304b6Bc5bF657dc6780bEB', + implementations: null, + is_contract: true, + is_verified: null, + name: 'MockERC20', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + coin_balance: '2346534676900000008', + compiler_version: 'v0.8.17+commit.8df45f5f', + has_constructor_args: false, + language: 'solidity', + market_cap: null, + optimization_enabled: false, + transactions_count: 7334224, + verified_at: '2022-09-16T18:49:29.605179Z', + license_type: 'mit', +}; + +export const contract2: VerifiedContract = { + address: { + hash: '0xB2218bdEbe8e90f80D04286772B0968ead666942', + implementations: null, + is_contract: true, + is_verified: null, + name: 'EternalStorageProxyWithSomeExternalLibrariesAndEvenMore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + certified: true, + coin_balance: '9078234570352343999', + compiler_version: 'v0.3.1+commit.0463ea4c', + has_constructor_args: true, + language: 'vyper', + market_cap: null, + optimization_enabled: true, + transactions_count: 440, + verified_at: '2021-09-07T20:01:56.076979Z', + license_type: 'bsd_3_clause', +}; + +export const contract3: VerifiedContract = { + address: { + ens_domain_name: null, + hash: '0xf145e3A26c6706F64d95Dc8d9d45022D8b3D676B', + implementations: [], + is_contract: true, + is_verified: true, + metadata: null, + name: 'StylusTestToken', + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + certified: false, + coin_balance: '0', + compiler_version: 'v0.5.6', + has_constructor_args: false, + language: 'stylus_rust', + license_type: 'none', + market_cap: null, + optimization_enabled: false, + transactions_count: 0, + verified_at: '2024-12-03T14:05:42.796224Z', +}; + +export const baseResponse: VerifiedContractsResponse = { + items: [ + contract1, + contract2, + contract3, + ], + next_page_params: { + items_count: '50', + smart_contract_id: '172', + }, +}; diff --git a/explorer/frontend/mocks/ens/domain.ts b/explorer/frontend/mocks/ens/domain.ts new file mode 100644 index 000000000..6cf446b83 --- /dev/null +++ b/explorer/frontend/mocks/ens/domain.ts @@ -0,0 +1,136 @@ +import * as bens from '@blockscout/bens-types'; + +const domainTokenA: bens.Token = { + id: '97352314626701792030827861137068748433918254309635329404916858191911576754327', + contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85', + type: bens.TokenType.NATIVE_DOMAIN_TOKEN, +}; +const domainTokenB = { + id: '423546333', + contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea86', + type: bens.TokenType.WRAPPED_DOMAIN_TOKEN, +}; + +export const protocolA: bens.ProtocolInfo = { + id: 'ens', + short_name: 'ENS', + title: 'Ethereum Name Service', + description: 'The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain.', + tld_list: [ + 'eth', + 'xyz', + ], + icon_url: 'https://i.imgur.com/GOfUwCb.jpeg', + docs_url: 'https://docs.ens.domains/', + deployment_blockscout_base_url: 'http://localhost:3200/', +}; + +export const protocolB: bens.ProtocolInfo = { + id: 'duck', + short_name: 'DUCK', + title: 'Duck Name Service', + description: '"Duck Name Service" is a cutting-edge blockchain naming service, providing seamless naming for crypto and decentralized applications. 🦆', + tld_list: [ + 'duck', + 'quack', + ], + icon_url: 'https://localhost:3000/duck.jpg', + docs_url: 'https://docs.duck.domains/', + deployment_blockscout_base_url: '', +}; + +export const ensDomainA: bens.DetailedDomain = { + id: '0xb140bf9645e54f02ed3c1bcc225566b515a98d1688f10494a5c3bc5b447936a7', + tokens: [ + domainTokenA, + domainTokenB, + ], + name: 'cat.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + registration_date: '2021-06-27T13:34:44.000Z', + expiry_date: '2025-03-01T14:20:24.000Z', + other_addresses: { + ETH: 'fe6ab8a0dafe7d41adf247c210451c264155c9b0', + GNO: 'DDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', + NEAR: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near', + }, + protocol: protocolA, + resolver_address: { + hash: '0xD578780f1dA7404d9CC0eEbC9D684c140CC4b638', + }, + resolved_with_wildcard: true, + stored_offchain: true, + wrapped_owner: { + hash: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401', + }, +}; + +export const ensDomainB: bens.DetailedDomain = { + id: '0x632ac7bec8e883416b371b36beaa822f4784208c99d063ee030020e2bd09b885', + tokens: [ domainTokenA ], + name: 'kitty.kitty.kitty.cat.eth', + resolved_address: undefined, + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: undefined, + registration_date: '2023-08-13T13:01:12.000Z', + expiry_date: undefined, + other_addresses: {}, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; + +export const ensDomainC: bens.DetailedDomain = { + id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ad', + tokens: [ domainTokenA ], + name: 'duck.duck.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: undefined, + registration_date: '2022-04-24T07:34:44.000Z', + expiry_date: '2022-11-01T13:10:36.000Z', + other_addresses: {}, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; + +export const ensDomainD: bens.DetailedDomain = { + id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ae', + tokens: [ domainTokenA ], + name: '🦆.duck.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + owner: undefined, + wrapped_owner: undefined, + registration_date: '2022-04-24T07:34:44.000Z', + expiry_date: '2027-09-23T13:10:36.000Z', + other_addresses: {}, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; diff --git a/explorer/frontend/mocks/ens/events.ts b/explorer/frontend/mocks/ens/events.ts new file mode 100644 index 000000000..d13fbcbe9 --- /dev/null +++ b/explorer/frontend/mocks/ens/events.ts @@ -0,0 +1,19 @@ +import type * as bens from '@blockscout/bens-types'; + +export const ensDomainEventA: bens.DomainEvent = { + transaction_hash: '0x86c66b9fad66e4f20d42a6eed4fe12a0f48a274786fd85e9d4aa6c60e84b5874', + timestamp: '2021-06-27T13:34:44.000000Z', + from_address: { + hash: '0xaa96a50a2f67111262fe24576bd85bb56ec65016', + }, + action: '0xf7a16963', +}; + +export const ensDomainEventB: bens.DomainEvent = { + transaction_hash: '0x150bf7d5cd42457dd9c799ddd9d4bf6c30c703e1954a88c6d4b668b23fe0fbf8', + timestamp: '2022-11-02T14:20:24.000000Z', + from_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + action: 'register', +}; diff --git a/explorer/frontend/mocks/interop/interop.ts b/explorer/frontend/mocks/interop/interop.ts new file mode 100644 index 000000000..ca91595ad --- /dev/null +++ b/explorer/frontend/mocks/interop/interop.ts @@ -0,0 +1,56 @@ +import type { ChainInfo, InteropMessage } from 'types/api/interop'; + +export const chain: ChainInfo = { + chain_id: 1, + chain_name: 'Ethereum', + chain_logo: 'https://example.com/logo.png', + instance_url: 'https://example.com', +}; + +export const interopMessageIn: InteropMessage = { + init_transaction_hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + nonce: 1, + payload: 'payload', + relay_transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + sender: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + status: 'Relayed', + target: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + timestamp: '2022-10-10T14:34:30.000000Z', + init_chain: chain, +}; + +export const interopMessageIn1: InteropMessage = { + init_transaction_hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + nonce: 1, + payload: 'payload', + relay_transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + sender: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + status: 'Sent', + target: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + timestamp: '2022-10-10T14:34:30.000000Z', + init_chain: null, +}; + +export const interopMessageOut: InteropMessage = { + init_transaction_hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + nonce: 1, + payload: 'payload', + relay_transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + sender: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + status: 'Relayed', + target: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + timestamp: '2022-10-10T14:34:30.000000Z', + relay_chain: chain, +}; + +export const interopMessageOut1: InteropMessage = { + init_transaction_hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + nonce: 1, + payload: 'payload', + relay_transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + sender: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + status: 'Failed', + target: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + timestamp: '2022-10-10T14:34:30.000000Z', + relay_chain: null, +}; diff --git a/explorer/frontend/mocks/metadata/address.ts b/explorer/frontend/mocks/metadata/address.ts new file mode 100644 index 000000000..186711cc0 --- /dev/null +++ b/explorer/frontend/mocks/metadata/address.ts @@ -0,0 +1,115 @@ +/* eslint-disable max-len */ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +export const nameTag: AddressMetadataTagApi = { + slug: 'quack-quack', + name: 'Quack quack', + tagType: 'name', + ordinal: 99, + meta: null, +}; + +export const customNameTag: AddressMetadataTagApi = { + slug: 'unicorn-uproar', + name: 'Unicorn Uproar', + tagType: 'name', + ordinal: 777, + meta: { + tagUrl: 'https://example.com', + bgColor: 'linear-gradient(45deg, deeppink, deepskyblue)', + textColor: '#FFFFFF', + }, +}; + +export const genericTag: AddressMetadataTagApi = { + slug: 'duck-owner', + name: 'duck owner 🦆', + tagType: 'generic', + ordinal: 55, + meta: { + bgColor: 'rgba(255,243,12,90%)', + }, +}; + +export const infoTagWithLink: AddressMetadataTagApi = { + slug: 'goosegang', + name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG', + tagType: 'classifier', + ordinal: 11, + meta: { + tagUrl: 'https://example.com', + }, +}; + +export const tagWithTooltip: AddressMetadataTagApi = { + slug: 'blockscout-heroes', + name: 'BlockscoutHeroes', + tagType: 'classifier', + ordinal: 42, + meta: { + tooltipDescription: 'The Blockscout team, EVM blockchain aficionados, illuminate Ethereum networks with unparalleled insight and prowess, leading the way in blockchain exploration! 🚀🔎', + tooltipIcon: 'https://localhost:3100/icon.svg', + tooltipTitle: 'Blockscout team member', + tooltipUrl: 'https://blockscout.com', + }, +}; + +export const protocolTag: AddressMetadataTagApi = { + slug: 'aerodrome', + name: 'Aerodrome', + tagType: 'protocol', + ordinal: 0, + meta: null, +}; + +export const protocolTagWithMeta: AddressMetadataTagApi = { + slug: 'uniswap', + name: 'Uniswap', + tagType: 'protocol', + ordinal: 0, + meta: { + appID: 'uniswap', + appMarketplaceURL: 'https://example.com', + appLogoURL: 'https://localhost:3100/icon.svg', + appActionButtonText: 'Swap', + textColor: '#FFFFFF', + bgColor: '#FF007A', + }, +}; + +export const warpcastTag: AddressMetadataTagApi = { + slug: 'warpcast-account', + name: 'Farcaster', + tagType: 'protocol', + ordinal: 0, + meta: { + bgColor: '#8465CB', + tagIcon: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2032%2029%22%3E%3Cpath%20d%3D%22M%205.507%200.072%20L%2026.097%200.072%20L%2026.097%204.167%20L%2031.952%204.167%20L%2030.725%208.263%20L%2029.686%208.263%20L%2029.686%2024.833%20C%2030.207%2024.833%2030.63%2025.249%2030.63%2025.763%20L%2030.63%2026.88%20L%2030.819%2026.88%20C%2031.341%2026.88%2031.764%2027.297%2031.764%2027.811%20L%2031.764%2028.928%20L%2021.185%2028.928%20L%2021.185%2027.811%20C%2021.185%2027.297%2021.608%2026.88%2022.13%2026.88%20L%2022.319%2026.88%20L%2022.319%2025.763%20C%2022.319%2025.316%2022.639%2024.943%2023.065%2024.853%20L%2023.045%2015.71%20C%2022.711%2012.057%2019.596%209.194%2015.802%209.194%20C%2012.008%209.194%208.893%2012.057%208.559%2015.71%20L%208.539%2024.845%20C%209.043%2024.919%209.663%2025.302%209.663%2025.763%20L%209.663%2026.88%20L%209.852%2026.88%20C%2010.373%2026.88%2010.796%2027.297%2010.796%2027.811%20L%2010.796%2028.928%20L%200.218%2028.928%20L%200.218%2027.811%20C%200.218%2027.297%200.641%2026.88%201.162%2026.88%20L%201.351%2026.88%20L%201.351%2025.763%20C%201.351%2025.249%201.774%2024.833%202.296%2024.833%20L%202.296%208.263%20L%201.257%208.263%20L%200.029%204.167%20L%205.507%204.167%20L%205.507%200.072%20Z%22%20fill%3D%22rgb(255%2C%20255%2C%20255)%22%3E%3C%2Fpath%3E%3Cpath%20d%3D%22M%2026.097%200.072%20L%2026.166%200.072%20L%2026.166%200.004%20L%2026.097%200.004%20Z%20M%205.507%200.072%20L%205.507%200.004%20L%205.438%200.004%20L%205.438%200.072%20Z%20M%2026.097%204.167%20L%2026.028%204.167%20L%2026.028%204.235%20L%2026.097%204.235%20Z%20M%2031.952%204.167%20L%2032.019%204.187%20L%2032.045%204.099%20L%2031.952%204.099%20L%2031.952%204.167%20Z%20M%2030.725%208.263%20L%2030.725%208.331%20L%2030.776%208.331%20L%2030.791%208.282%20Z%20M%2029.686%208.263%20L%2029.686%208.195%20L%2029.617%208.195%20L%2029.617%208.263%20Z%20M%2029.686%2024.833%20L%2029.617%2024.833%20L%2029.617%2024.901%20L%2029.686%2024.901%20Z%20M%2030.63%2026.88%20L%2030.561%2026.88%20L%2030.561%2026.948%20L%2030.63%2026.948%20Z%20M%2031.764%2028.928%20L%2031.764%2028.996%20L%2031.832%2028.996%20L%2031.832%2028.928%20Z%20M%2021.185%2028.928%20L%2021.116%2028.928%20L%2021.116%2028.996%20L%2021.185%2028.996%20Z%20M%2022.319%2026.88%20L%2022.319%2026.948%20L%2022.388%2026.948%20L%2022.388%2026.88%20Z%20M%2023.065%2024.853%20L%2023.08%2024.919%20L%2023.134%2024.908%20L%2023.134%2024.853%20Z%20M%2023.045%2015.71%20L%2023.114%2015.71%20L%2023.114%2015.707%20L%2023.113%2015.704%20Z%20M%208.559%2015.71%20L%208.49%2015.704%20L%208.49%2015.707%20L%208.49%2015.71%20Z%20M%208.539%2024.845%20L%208.47%2024.845%20L%208.469%2024.904%20L%208.528%2024.913%20Z%20M%209.663%2026.88%20L%209.594%2026.88%20L%209.594%2026.948%20L%209.663%2026.948%20Z%20M%2010.796%2028.928%20L%2010.796%2028.996%20L%2010.865%2028.996%20L%2010.865%2028.928%20Z%20M%200.218%2028.928%20L%200.149%2028.928%20L%200.149%2028.996%20L%200.218%2028.996%20Z%20M%201.351%2026.88%20L%201.351%2026.948%20L%201.42%2026.948%20L%201.42%2026.88%20Z%20M%202.296%2024.833%20L%202.296%2024.901%20L%202.365%2024.901%20L%202.365%2024.833%20Z%20M%202.296%208.263%20L%202.365%208.263%20L%202.365%208.195%20L%202.296%208.195%20Z%20M%201.257%208.263%20L%201.191%208.282%20L%201.205%208.331%20L%201.257%208.331%20Z%20M%200.029%204.167%20L%200.029%204.1%20L%20-0.063%204.1%20L%20-0.037%204.187%20L%200.029%204.167%20Z%20M%205.507%204.167%20L%205.507%204.235%20L%205.576%204.235%20L%205.576%204.167%20Z%20M%2026.097%200.004%20L%205.507%200.004%20L%205.507%200.139%20L%2026.097%200.139%20Z%20M%2026.166%204.167%20L%2026.166%200.072%20L%2026.028%200.072%20L%2026.028%204.167%20L%2026.166%204.167%20Z%20M%2031.952%204.099%20L%2026.097%204.099%20L%2026.097%204.235%20L%2031.952%204.235%20Z%20M%2030.791%208.282%20L%2032.019%204.187%20L%2031.886%204.148%20L%2030.658%208.244%20Z%20M%2029.686%208.331%20L%2030.725%208.331%20L%2030.725%208.195%20L%2029.686%208.195%20Z%20M%2029.755%2024.833%20L%2029.755%208.263%20L%2029.617%208.263%20L%2029.617%2024.833%20Z%20M%2030.699%2025.763%20C%2030.699%2025.212%2030.245%2024.765%2029.686%2024.765%20L%2029.686%2024.9%20C%2030.169%2024.9%2030.561%2025.287%2030.561%2025.763%20Z%20M%2030.699%2026.88%20L%2030.699%2025.763%20L%2030.561%2025.763%20L%2030.561%2026.88%20Z%20M%2030.819%2026.813%20L%2030.63%2026.813%20L%2030.63%2026.948%20L%2030.819%2026.948%20Z%20M%2031.832%2027.811%20C%2031.832%2027.26%2031.379%2026.813%2030.819%2026.813%20L%2030.819%2026.948%20C%2031.303%2026.948%2031.695%2027.335%2031.695%2027.811%20Z%20M%2031.832%2028.928%20L%2031.832%2027.811%20L%2031.695%2027.811%20L%2031.695%2028.928%20Z%20M%2026.097%2028.996%20L%2031.764%2028.996%20L%2031.764%2028.86%20L%2026.097%2028.86%20Z%20M%2023.074%2028.996%20L%2026.097%2028.996%20L%2026.097%2028.86%20L%2023.074%2028.86%20Z%20M%2021.185%2028.996%20L%2023.074%2028.996%20L%2023.074%2028.86%20L%2021.185%2028.86%20Z%20M%2021.116%2027.811%20L%2021.116%2028.928%20L%2021.254%2028.928%20L%2021.254%2027.811%20Z%20M%2022.13%2026.813%20C%2021.57%2026.813%2021.116%2027.26%2021.116%2027.811%20L%2021.254%2027.811%20C%2021.254%2027.335%2021.646%2026.948%2022.13%2026.948%20Z%20M%2022.319%2026.813%20L%2022.13%2026.813%20L%2022.13%2026.948%20L%2022.319%2026.948%20Z%20M%2022.25%2025.763%20L%2022.25%2026.88%20L%2022.388%2026.88%20L%2022.388%2025.763%20Z%20M%2023.051%2024.787%20C%2022.593%2024.883%2022.25%2025.284%2022.25%2025.763%20L%2022.388%2025.763%20C%2022.388%2025.349%2022.684%2025.003%2023.08%2024.919%20Z%20M%2022.976%2015.71%20L%2022.996%2024.853%20L%2023.134%2024.853%20L%2023.114%2015.71%20Z%20M%2015.802%209.262%20C%2019.559%209.262%2022.645%2012.098%2022.976%2015.716%20L%2023.113%2015.704%20C%2022.776%2012.016%2019.632%209.126%2015.802%209.126%20Z%20M%208.628%2015.716%20C%208.959%2012.098%2012.044%209.262%2015.802%209.262%20L%2015.802%209.126%20C%2011.972%209.126%208.828%2012.016%208.49%2015.704%20Z%20M%208.608%2024.845%20L%208.628%2015.71%20L%208.49%2015.71%20L%208.47%2024.845%20Z%20M%209.732%2025.763%20C%209.732%2025.502%209.557%2025.273%209.331%2025.105%20C%209.104%2024.935%208.812%2024.817%208.549%2024.778%20L%208.528%2024.912%20C%208.769%2024.948%209.039%2025.057%209.248%2025.213%20C%209.459%2025.37%209.594%2025.563%209.594%2025.763%20Z%20M%209.732%2026.88%20L%209.732%2025.763%20L%209.594%2025.763%20L%209.594%2026.88%20Z%20M%209.852%2026.813%20L%209.663%2026.813%20L%209.663%2026.948%20L%209.852%2026.948%20Z%20M%2010.865%2027.811%20C%2010.865%2027.26%2010.411%2026.813%209.852%2026.813%20L%209.852%2026.948%20C%2010.335%2026.948%2010.727%2027.335%2010.727%2027.811%20Z%20M%2010.865%2028.928%20L%2010.865%2027.811%20L%2010.727%2027.811%20L%2010.727%2028.928%20Z%20M%208.529%2028.996%20L%2010.796%2028.996%20L%2010.796%2028.86%20L%208.529%2028.86%20Z%20M%208.372%2028.996%20L%208.529%2028.996%20L%208.529%2028.86%20L%208.372%2028.86%20Z%20M%205.507%2028.996%20L%208.372%2028.996%20L%208.372%2028.86%20L%205.507%2028.86%20Z%20M%200.218%2028.996%20L%205.507%2028.996%20L%205.507%2028.86%20L%200.218%2028.86%20Z%20M%200.149%2027.811%20L%200.149%2028.928%20L%200.287%2028.928%20L%200.287%2027.811%20Z%20M%201.162%2026.813%20C%200.603%2026.813%200.149%2027.26%200.149%2027.811%20L%200.287%2027.811%20C%200.287%2027.335%200.679%2026.948%201.162%2026.948%20Z%20M%201.351%2026.813%20L%201.162%2026.813%20L%201.162%2026.948%20L%201.351%2026.948%20Z%20M%201.282%2025.763%20L%201.282%2026.88%20L%201.42%2026.88%20L%201.42%2025.763%20Z%20M%202.296%2024.765%20C%201.736%2024.765%201.282%2025.212%201.282%2025.763%20L%201.42%2025.763%20C%201.42%2025.287%201.812%2024.9%202.296%2024.9%20Z%20M%202.227%208.263%20L%202.227%2024.833%20L%202.365%2024.833%20L%202.365%208.263%20Z%20M%201.257%208.331%20L%202.296%208.331%20L%202.296%208.195%20L%201.257%208.195%20Z%20M%20-0.037%204.187%20L%201.191%208.282%20L%201.323%208.244%20L%200.095%204.148%20Z%20M%205.507%204.099%20L%200.029%204.099%20L%200.029%204.235%20L%205.507%204.235%20L%205.507%204.099%20Z%20M%205.438%200.072%20L%205.438%204.167%20L%205.576%204.167%20L%205.576%200.072%20Z%22%20fill%3D%22rgb(255%2C255%2C255)%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E', tagUrl: 'https://warpcast.com/mbj357', + textColor: '#FFFFFF', + tooltipDescription: 'This address is linked to a Farcaster account', + warpcastHandle: 'duckYduck', + }, +}; + +export const noteTag: AddressMetadataTagApi = { + slug: 'scam-tag', + name: 'Phish 🐟', + tagType: 'note', + ordinal: 100, + meta: { + alertBgColor: 'deeppink', + alertTextColor: 'white', + data: 'Warning! This is scam! See the report', + }, +}; + +export const noteTag2: AddressMetadataTagApi = { + slug: 'note0', + name: 'note_0', + tagType: 'note', + ordinal: 0, + meta: { + alertStatus: 'info', + data: 'The token MILF was launched on May 13, 2021. The maximum total supply of the token is 100 billion.', + }, +}; diff --git a/explorer/frontend/mocks/metadata/appActionButton.ts b/explorer/frontend/mocks/metadata/appActionButton.ts new file mode 100644 index 000000000..47938638c --- /dev/null +++ b/explorer/frontend/mocks/metadata/appActionButton.ts @@ -0,0 +1,38 @@ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +const appID = 'uniswap'; +const appMarketplaceURL = 'https://example.com'; +export const appLogoURL = 'https://localhost:3100/icon.svg'; +const appActionButtonText = 'Swap'; +const textColor = '#FFFFFF'; +const bgColor = '#FF007A'; + +export const buttonWithoutStyles: AddressMetadataTagApi['meta'] = { + appID, + appMarketplaceURL, + appLogoURL, + appActionButtonText, +}; + +export const linkWithoutStyles: AddressMetadataTagApi['meta'] = { + appMarketplaceURL, + appLogoURL, + appActionButtonText, +}; + +export const buttonWithStyles: AddressMetadataTagApi['meta'] = { + appID, + appMarketplaceURL, + appLogoURL, + appActionButtonText, + textColor, + bgColor, +}; + +export const linkWithStyles: AddressMetadataTagApi['meta'] = { + appMarketplaceURL, + appLogoURL, + appActionButtonText, + textColor, + bgColor, +}; diff --git a/explorer/frontend/mocks/metadata/publicTagTypes.ts b/explorer/frontend/mocks/metadata/publicTagTypes.ts new file mode 100644 index 000000000..80bf92acb --- /dev/null +++ b/explorer/frontend/mocks/metadata/publicTagTypes.ts @@ -0,0 +1,34 @@ +export const publicTagTypes = { + tagTypes: [ + { + id: '96f9db76-02fc-477d-a003-640a0c5e7e15', + type: 'name' as const, + description: 'Alias for the address', + }, + { + id: 'e75f396e-f52a-44c9-8790-a1dbae496b72', + type: 'generic' as const, + description: 'Group classification for the address', + }, + { + id: '11a2d4f3-412e-4eb7-b663-86c6f48cdec3', + type: 'information' as const, + description: 'Tags with custom data for the address, e.g. additional link to project, or classification details, or minor account details', + }, + { + id: 'd37443d4-748f-4314-a4a0-283b666e9f29', + type: 'classifier' as const, + description: 'E.g. "ERC20", "Contract", "CEX", "DEX", "NFT"', + }, + { + id: 'ea9d0f91-9b46-44ff-be70-128bac468f6f', + type: 'protocol' as const, + description: 'Special tag type for protocol-related contracts, e.g. for bridges', + }, + { + id: 'd2600acb-473c-445f-ac72-ed6fef53e06a', + type: 'note' as const, + description: 'Short general-purpose description for the address', + }, + ], +}; diff --git a/explorer/frontend/mocks/mud/mudTables.ts b/explorer/frontend/mocks/mud/mudTables.ts new file mode 100644 index 000000000..b5f0186c9 --- /dev/null +++ b/explorer/frontend/mocks/mud/mudTables.ts @@ -0,0 +1,94 @@ +/* eslint-disable max-len */ +import type { AddressMudRecord, AddressMudRecords, AddressMudRecordsItem, AddressMudTables } from 'types/api/address'; +import type { MudWorldSchema, MudWorldTable } from 'types/api/mudWorlds'; + +export const table1: MudWorldTable = { + table_full_name: 'tb.store.Tables', + table_id: '0x746273746f72650000000000000000005461626c657300000000000000000000', + table_name: 'Tables', + table_namespace: 'store', + table_type: 'onchain', +}; + +export const table2: MudWorldTable = { + table_full_name: 'ot.world.FunctionSignatur', + table_id: '0x6f74776f726c6400000000000000000046756e6374696f6e5369676e61747572', + table_name: 'FunctionSignatur', + table_namespace: 'world', + table_type: 'offchain', +}; + +export const schema1: MudWorldSchema = { + key_names: [ 'moduleAddress', 'argumentsHash' ], + key_types: [ 'address', 'bytes32' ], + value_names: [ 'fieldLayout', 'keySchema', 'valueSchema', 'abiEncodedKeyNames', 'abiEncodedFieldNames' ], + value_types: [ 'bytes32', 'bytes32', 'bytes32', 'bytes', 'bytes' ], +}; + +export const schema2: MudWorldSchema = { + key_names: [], + key_types: [], + value_names: [ 'value' ], + value_types: [ 'address' ], +}; + +export const mudTables: AddressMudTables = { + items: [ + { + table: table1, + schema: schema1, + }, + { + table: table2, + schema: schema2, + }, + ], + next_page_params: { + items_count: 50, + table_id: '1', + }, +}; + +const record: AddressMudRecordsItem = { + decoded: { + abiEncodedFieldNames: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000006706c617965720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000974696d657374616d700000000000000000000000000000000000000000000000', + abiEncodedKeyNames: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026964000000000000000000000000000000000000000000000000000000000000', + goldCosts: [ '100000', '150000', '200000', '250000', '400000', '550000', '700000' ], + prototypeIds: [ + '0x53776f7264736d616e0000000000000000000000000000000000000000000000', + '0x50696b656d616e00000000000000000000000000000000000000000000000000', + '0x50696b656d616e00000000000000000000000000000000000000000000000000', + '0x4172636865720000000000000000000000000000000000000000000000000000', + '0x4b6e696768740000000000000000000000000000000000000000000000000000', + ], + keySchema: '0x002001001f000000000000000000000000000000000000000000000000000000', + tableId: '0x6f74000000000000000000000000000044726177557064617465000000000000', + valueSchema: '0x00540300611f1f00000000000000000000000000000000000000000000000000', + }, + id: '0x007a651a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007', + is_deleted: false, + timestamp: '2024-05-09T15:14:32.000000Z', +}; + +export const mudRecords: AddressMudRecords = { + items: [ record, record ], + next_page_params: { + items_count: 50, + key0: '1', + key1: '2', + key_bytes: '3', + }, + schema: { + key_names: [ 'tableId' ], + key_types: [ 'bytes32' ], + value_names: [ 'prototypeIds', 'goldCosts', 'keySchema', 'valueSchema', 'abiEncodedKeyNames', 'abiEncodedFieldNames' ], + value_types: [ 'bytes32[]', 'int32[]', 'bytes32', 'bytes32', 'bytes32', 'bytes', 'bytes' ], + }, + table: table1, +}; + +export const mudRecord: AddressMudRecord = { + record, + schema: mudRecords.schema, + table: table1, +}; diff --git a/explorer/frontend/mocks/mud/mudWorlds.ts b/explorer/frontend/mocks/mud/mudWorlds.ts new file mode 100644 index 000000000..0cfcfd52c --- /dev/null +++ b/explorer/frontend/mocks/mud/mudWorlds.ts @@ -0,0 +1,27 @@ +import type { MudWorldsResponse } from 'types/api/mudWorlds'; + +import { withName, withoutName } from 'mocks/address/address'; + +export const mudWorlds: MudWorldsResponse = { + items: [ + { + address: withName, + coin_balance: '300000000000000000', + transactions_count: 3938, + }, + { + address: withoutName, + coin_balance: '0', + transactions_count: 0, + }, + { + address: withoutName, + coin_balance: '0', + transactions_count: 0, + }, + ], + next_page_params: { + items_count: 50, + world: '0x18f01f12ca21b6fc97b917c3e32f671f8a933caa', + }, +}; diff --git a/explorer/frontend/mocks/noves/transaction.ts b/explorer/frontend/mocks/noves/transaction.ts new file mode 100644 index 000000000..6feb72a56 --- /dev/null +++ b/explorer/frontend/mocks/noves/transaction.ts @@ -0,0 +1,103 @@ +import type { NovesResponseData } from 'types/api/noves'; + +import type { TokensData } from 'ui/tx/assetFlows/utils/getTokensData'; + +export const hash = '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53'; + +export const transaction: NovesResponseData = { + accountAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + chain: 'eth-goerli', + classificationData: { + description: 'Called function \'stake\' on contract 0xef326CdAdA59D3A740A76bB5f4F88Fb2.', + protocol: { + name: null, + }, + received: [], + sent: [ + { + action: 'sent', + actionFormatted: 'Sent', + amount: '3000', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + { + action: 'paidGas', + actionFormatted: 'Paid Gas', + amount: '0.000395521502109448', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: null, + name: 'Validators', + }, + token: { + address: 'ETH', + decimals: 18, + name: 'ETH', + symbol: 'ETH', + }, + }, + ], + source: { + type: null, + }, + type: 'unclassified', + typeFormatted: 'Unclassified', + }, + rawTransactionData: { + blockNumber: 10388918, + fromAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + gas: 275079, + gasPrice: 1500000008, + timestamp: 1705488588, + toAddress: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + transactionFee: { + amount: '395521502109448', + token: { + decimals: 18, + symbol: 'ETH', + }, + }, + transactionHash: '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53', + }, + txTypeVersion: 2, +}; + +export const tokenData: TokensData = { + nameList: [ 'PQR-Test', 'ETH' ], + symbolList: [ 'PQR' ], + idList: [], + byName: { + 'PQR-Test': { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + ETH: { name: 'ETH', symbol: null, address: '', id: undefined }, + }, + bySymbol: { + PQR: { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + 'null': { name: 'ETH', symbol: null, address: '', id: undefined }, + }, +}; diff --git a/explorer/frontend/mocks/optimism/deposits.ts b/explorer/frontend/mocks/optimism/deposits.ts new file mode 100644 index 000000000..e2f0ba1e9 --- /dev/null +++ b/explorer/frontend/mocks/optimism/deposits.ts @@ -0,0 +1,33 @@ +export const data = { + items: [ + { + l1_block_number: 8382841, + l1_block_timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + l1_transaction_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + l2_transaction_gas_limit: '2156928', + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + l1_block_timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xa280f18cc72f9ad904087eb262c236048e935ad184a85bbd042d544c172c10bf', + l1_transaction_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + l2_transaction_gas_limit: '1216064', + l2_transaction_hash: '0xaaaeb47a78b5c42d870f8d831a683a7cefe1b031a992170b28b43b82bd08318c', + }, + { + l1_block_number: 8382834, + l1_block_timestamp: '2022-06-27T01:11:48.000000Z', + l1_transaction_hash: '0xfca8cc5440bffa8b975873c02bba3ff3380dd75fbc3260d10179e282cf72d6d4', + l1_transaction_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + l2_transaction_gas_limit: '405824', + l2_transaction_hash: '0xa0604ebf2614ad708aeefa83f766fb25928dadb5ffb2f45028f5b4f1fa4d9358', + }, + ], + next_page_params: { + items_count: 50, + l1_block_number: 8382363, + transaction_hash: '0x2012f0ce966ce6573e7826e9235f227edf5a2f8382b8d646c979f85a77e15c05', + }, +}; diff --git a/explorer/frontend/mocks/optimism/disputeGames.ts b/explorer/frontend/mocks/optimism/disputeGames.ts new file mode 100644 index 000000000..4e513cdb7 --- /dev/null +++ b/explorer/frontend/mocks/optimism/disputeGames.ts @@ -0,0 +1,26 @@ +export const data = { + items: [ + { + contract_address_hash: '0x5cbe1b88b6357e6a8f0821bea72cc0b88c231f1c', + created_at: '2022-05-27T01:13:48.000000Z', + game_type: 0, + index: 6662, + l2_block_number: 12542890, + resolved_at: null, + status: 'In progress', + }, + { + contract_address_hash: '0x5cbe1b88b6357e6a8f0821bea72cc0b88c231f1c', + created_at: '2022-05-27T01:13:48.000000Z', + game_type: 0, + index: 6662, + l2_block_number: 12542890, + resolved_at: '2022-05-27T01:13:48.000000Z', + status: 'Defender wins', + }, + ], + next_page_params: { + items_count: 50, + index: 8382363, + }, +}; diff --git a/explorer/frontend/mocks/optimism/outputRoots.ts b/explorer/frontend/mocks/optimism/outputRoots.ts new file mode 100644 index 000000000..0be0426fe --- /dev/null +++ b/explorer/frontend/mocks/optimism/outputRoots.ts @@ -0,0 +1,32 @@ +export const outputRootsData = { + items: [ + { + l1_block_number: 8456113, + l1_timestamp: '2022-02-08T12:08:48.000000Z', + l1_transaction_hash: '0x19455a53758d5de89070164ff09c40d93f1b4447e721090f03aa150f6159265a', + l2_block_number: 5214988, + l2_output_index: 9926, + output_root: '0xa7de9bd3986ce5ca8de9f0ab6c7473f4cebe225fb13b57cc5c8472de84a8bab3', + }, + { + l1_block_number: 8456099, + l1_timestamp: '2022-02-08T12:05:24.000000Z', + l1_transaction_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b', + l2_block_number: 5214868, + l2_output_index: 9925, + output_root: '0x4ec2822d2f7b4f834d693d88f8a4cf15899882915980a21756d29cfd9f9f3898', + }, + { + l1_block_number: 8456078, + l1_timestamp: '2022-02-08T12:00:48.000000Z', + l1_transaction_hash: '0x4238988b0959e41a7b09cef67f58698e05e3bcc29b8d2f60e6c77dc68c91f16e', + l2_block_number: 5214748, + l2_output_index: 9924, + output_root: '0x78b2e13c20f4bbfb4a008127edaaf25aa476f933669edd4856305bf4ab64a92b', + }, + ], + next_page_params: { + index: 9877, + items_count: 50, + }, +}; diff --git a/explorer/frontend/mocks/optimism/txnBatches.ts b/explorer/frontend/mocks/optimism/txnBatches.ts new file mode 100644 index 000000000..d7704edc9 --- /dev/null +++ b/explorer/frontend/mocks/optimism/txnBatches.ts @@ -0,0 +1,104 @@ +import type { + OptimismL2TxnBatchTypeCallData, + OptimismL2TxnBatchTypeCelestia, + OptimismL2TxnBatchTypeEip4844, + OptimisticL2TxnBatchesResponse, +} from 'types/api/optimisticL2'; + +export const txnBatchesData: OptimisticL2TxnBatchesResponse = { + items: [ + { + batch_data_container: 'in_blob4844', + number: 260998, + l1_timestamp: '2022-11-10T11:29:11.000000Z', + l1_transaction_hashes: [ + '0x9553351f6bd1577f4e782738c087be08697fb11f3b91745138d71ba166d62c3b', + ], + l2_end_block_number: 124882074, + l2_start_block_number: 124881833, + transactions_count: 4011, + }, + { + batch_data_container: 'in_calldata', + number: 260997, + l1_timestamp: '2022-11-03T11:20:59.000000Z', + l1_transaction_hashes: [ + '0x80f5fba70d5685bc2b70df836942e892b24afa7bba289a2fac0ca8f4d554cc72', + ], + l2_end_block_number: 124881832, + l2_start_block_number: 124881613, + transactions_count: 4206, + }, + { + number: 260996, + l1_timestamp: '2024-09-03T11:14:23.000000Z', + l1_transaction_hashes: [ + '0x39f4c46cae57bae936acb9159e367794f41f021ed3788adb80ad93830edb5f22', + ], + l2_end_block_number: 124881612, + l2_start_block_number: 124881380, + transactions_count: 4490, + }, + ], + next_page_params: { + id: 5902834, + items_count: 50, + }, +}; + +export const txnBatchTypeCallData: OptimismL2TxnBatchTypeCallData = { + batch_data_container: 'in_calldata', + number: 309123, + l1_timestamp: '2022-08-10T10:30:24.000000Z', + l1_transaction_hashes: [ + '0x478c45f182631ae6f7249d40f31fdac36f41d88caa2e373fba35340a7345ca67', + ], + l2_end_block_number: 10146784, + l2_start_block_number: 10145379, + transactions_count: 1608, +}; + +export const txnBatchTypeCelestia: OptimismL2TxnBatchTypeCelestia = { + batch_data_container: 'in_celestia', + blobs: [ + { + commitment: '0x39c18c21c6b127d58809b8d3b5931472421f9b51532959442f53038f10b78f2a', + height: 2584868, + l1_timestamp: '2024-08-28T16:51:12.000000Z', + l1_transaction_hash: '0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24', + namespace: '0x00000000000000000000000000000000000000000008e5f679bf7116cb', + }, + ], + number: 309667, + l1_timestamp: '2022-08-28T16:51:12.000000Z', + l1_transaction_hashes: [ + '0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24', + ], + l2_end_block_number: 10935879, + l2_start_block_number: 10934514, + transactions_count: 1574, +}; + +export const txnBatchTypeEip4844: OptimismL2TxnBatchTypeEip4844 = { + batch_data_container: 'in_blob4844', + blobs: [ + { + hash: '0x012a4f0c6db6bce9d3d357b2bf847764320bcb0107ab318f3a532f637bc60dfe', + l1_timestamp: '2022-08-23T03:59:12.000000Z', + l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a', + }, + { + hash: '0x01d1097cce23229931afbc2fd1cf0d707da26df7b39cef1c542276ae718de4f6', + l1_timestamp: '2022-08-23T03:59:12.000000Z', + l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a', + }, + ], + number: 2538459, + l1_timestamp: '2022-08-23T03:59:12.000000Z', + l1_transaction_hashes: [ + '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a', + ], + l2_end_block_number: 16291502, + l2_start_block_number: 16291373, + transactions_count: 704, +}; diff --git a/explorer/frontend/mocks/optimism/withdrawals.ts b/explorer/frontend/mocks/optimism/withdrawals.ts new file mode 100644 index 000000000..b9d9588cd --- /dev/null +++ b/explorer/frontend/mocks/optimism/withdrawals.ts @@ -0,0 +1,50 @@ +import type { OptimisticL2WithdrawalsResponse } from 'types/api/optimisticL2'; + +export const data: OptimisticL2WithdrawalsResponse = { + items: [ + { + challenge_period_end: null, + from: { + hash: '0x67aab90c548b284be30b05c376001b4db90b87ba', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l1_transaction_hash: '0x1a235bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136dca684', + l2_timestamp: '2022-02-15T12:50:02.000000Z', + l2_transaction_hash: '0x918cd8c5c24c17e06cd02b0379510c4ad56324bf153578fb9caaaa2fe4e7dc35', + msg_nonce: 396, + msg_nonce_version: 1, + status: 'Ready to prove', + }, + { + challenge_period_end: null, + from: null, + l1_transaction_hash: null, + l2_timestamp: null, + l2_transaction_hash: '0x2f117bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136def593', + msg_nonce: 391, + msg_nonce_version: 1, + status: 'Ready to prove', + }, + { + challenge_period_end: '2022-11-11T12:50:02.000000Z', + from: null, + l1_transaction_hash: null, + l2_timestamp: null, + l2_transaction_hash: '0xe14b1f46838176702244a5343629bcecf728ca2d9881d47b4ce46e00c387d7e3', + msg_nonce: 390, + msg_nonce_version: 1, + status: 'Ready for relay', + }, + ], + next_page_params: { + items_count: 50, + nonce: '1766847064778384329583297500742918515827483896875618958121606201292620123', + }, +}; diff --git a/explorer/frontend/mocks/pools/pool.ts b/explorer/frontend/mocks/pools/pool.ts new file mode 100644 index 000000000..109649696 --- /dev/null +++ b/explorer/frontend/mocks/pools/pool.ts @@ -0,0 +1,26 @@ +import type { Pool } from 'types/api/pools'; + +export const base: Pool = { + contract_address: '0x06da0fd433c1a5d7a4faa01111c044910a184553', + chain_id: '1', + base_token_address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + base_token_symbol: 'USDT', + base_token_icon_url: 'https://localhost:3000/utia.jpg', + quote_token_address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + quote_token_symbol: 'WETH', + quote_token_icon_url: 'https://localhost:3000/secondary_utia.jpg', + base_token_fully_diluted_valuation_usd: '75486579078', + base_token_market_cap_usd: '139312819076.195', + quote_token_fully_diluted_valuation_usd: '486579078', + quote_token_market_cap_usd: '312819076.195', + liquidity: '2099941.2238', + dex: { id: 'sushiswap', name: 'SushiSwap' }, + fee: '0.03', + coin_gecko_terminal_url: 'https://www.geckoterminal.com/eth/pools/0x06da0fd433c1a5d7a4faa01111c044910a184553', +}; + +export const noIcons: Pool = { + ...base, + base_token_icon_url: null, + quote_token_icon_url: null, +}; diff --git a/explorer/frontend/mocks/rewards/activity.ts b/explorer/frontend/mocks/rewards/activity.ts new file mode 100644 index 000000000..b101181b8 --- /dev/null +++ b/explorer/frontend/mocks/rewards/activity.ts @@ -0,0 +1,56 @@ +import type { GetActivityRewardsResponse } from '@blockscout/points-types'; + +export const base: GetActivityRewardsResponse = { + items: [ + { + date: '2025-03-10', + end_date: '2025-03-16', + activity: 'sent_transactions', + amount: '60', + percentile: 0.5, + is_pending: true, + }, + { + date: '2025-03-10', + end_date: '2025-03-16', + activity: 'verified_contracts', + amount: '40', + percentile: 0.3, + is_pending: true, + }, + { + date: '2025-03-10', + end_date: '2025-03-16', + activity: 'blockscout_usage', + amount: '80', + percentile: 0.8, + is_pending: true, + }, + ], + last_week: [ + { + date: '2025-03-03', + end_date: '2025-03-09', + activity: 'sent_transactions', + amount: '40', + percentile: 0.25, + is_pending: false, + }, + { + date: '2025-03-03', + end_date: '2025-03-09', + activity: 'verified_contracts', + amount: '60', + percentile: 0.6, + is_pending: false, + }, + { + date: '2025-03-03', + end_date: '2025-03-09', + activity: 'blockscout_usage', + amount: '100', + percentile: 0.95, + is_pending: false, + }, + ], +}; diff --git a/explorer/frontend/mocks/rewards/balance.ts b/explorer/frontend/mocks/rewards/balance.ts new file mode 100644 index 000000000..1aaa2d0c4 --- /dev/null +++ b/explorer/frontend/mocks/rewards/balance.ts @@ -0,0 +1,10 @@ +import type { GetUserBalancesResponse } from '@blockscout/points-types'; + +export const base: GetUserBalancesResponse = { + total: '250', + staked: '0', + unstaked: '0', + total_staking_rewards: '0', + total_referral_rewards: '0', + pending_referral_rewards: '0', +}; diff --git a/explorer/frontend/mocks/rewards/dailyReward.ts b/explorer/frontend/mocks/rewards/dailyReward.ts new file mode 100644 index 000000000..7ed817ea0 --- /dev/null +++ b/explorer/frontend/mocks/rewards/dailyReward.ts @@ -0,0 +1,12 @@ +import type { DailyRewardCheckResponse } from '@blockscout/points-types'; + +export const base: DailyRewardCheckResponse = { + available: true, + daily_reward: '10', + streak_reward: '10', + pending_referral_rewards: '0', + total_reward: '20', + date: '', + reset_at: '', + streak: '6', +}; diff --git a/explorer/frontend/mocks/rewards/referrals.ts b/explorer/frontend/mocks/rewards/referrals.ts new file mode 100644 index 000000000..0d8f275e0 --- /dev/null +++ b/explorer/frontend/mocks/rewards/referrals.ts @@ -0,0 +1,7 @@ +import type { GetReferralDataResponse } from '@blockscout/points-types'; + +export const base: GetReferralDataResponse = { + code: 'QWERTY', + link: 'https://example.com?ref=QWERTY', + referrals: '15', +}; diff --git a/explorer/frontend/mocks/rewards/rewardsConfig.ts b/explorer/frontend/mocks/rewards/rewardsConfig.ts new file mode 100644 index 000000000..4dee16f92 --- /dev/null +++ b/explorer/frontend/mocks/rewards/rewardsConfig.ts @@ -0,0 +1,29 @@ +import type { GetConfigResponse } from '@blockscout/points-types'; + +export const base: GetConfigResponse = { + rewards: { + registration: '100', + registration_with_referral: '200', + daily_claim: '10', + referral_share: '0.1', + streak_bonuses: {}, + sent_transactions_activity_rewards: { + '1': '100', + }, + verified_contracts_activity_rewards: { + '1': '100', + }, + blockscout_usage_activity_rewards: { + '1': '100', + }, + blockscout_activity_pass_id: '1', + }, + auth: { + shared_siwe_login: true, + }, + activity: { + sent_transactions_activity_enabled: true, + verified_contracts_activity_enabled: true, + blockscout_usage_activity_enabled: true, + }, +}; diff --git a/explorer/frontend/mocks/scroll/messages.ts b/explorer/frontend/mocks/scroll/messages.ts new file mode 100644 index 000000000..d2bbf43a7 --- /dev/null +++ b/explorer/frontend/mocks/scroll/messages.ts @@ -0,0 +1,26 @@ +import type { ScrollL2MessagesResponse } from 'types/api/scrollL2'; + +export const baseResponse: ScrollL2MessagesResponse = { + items: [ + { + id: 930795, + origination_transaction_block_number: 20639178, + origination_transaction_hash: '0x70380f2c6ecd53aa6e0608e6c9d770acaa29c0508869ec296bae3e09678ea9f4', + origination_timestamp: '2024-08-30T05:03:23.000000Z', + completion_transaction_hash: null, + value: '5084131319054877748', + }, + { + id: 930748, + origination_transaction_block_number: 20638104, + origination_transaction_hash: '0x7e7b4d5ff0b7a6af5e52f4aa2ad9eca3c0c5664368cbb781e04b5b13c6109b2b', + origination_timestamp: '2024-08-30T01:26:35.000000Z', + completion_transaction_hash: '0x426b16ea3a42228f6d754ae55c348986122cdb1e4331b6fd454975776f513ea1', + value: '0', + }, + ], + next_page_params: { + items_count: 50, + id: 1, + }, +}; diff --git a/explorer/frontend/mocks/scroll/txnBatches.ts b/explorer/frontend/mocks/scroll/txnBatches.ts new file mode 100644 index 000000000..2f5abfdf7 --- /dev/null +++ b/explorer/frontend/mocks/scroll/txnBatches.ts @@ -0,0 +1,50 @@ +import type { ScrollL2BatchesResponse } from 'types/api/scrollL2'; + +export const batchData = { + number: 66928, + commitment_transaction: { + block_number: 19114878, + hash: '0x57552c0dbcf56383ee2efdf8fd6be143b355135fc300361924582c308877b8b7', + timestamp: '2024-01-29T21:31:35.000000Z', + }, + confirmation_transaction: { + block_number: null, + hash: null, + timestamp: null, + }, + data_availability: { + batch_data_container: 'in_blob4844' as const, + }, + start_block_number: 456000, + end_block_number: 789000, + transactions_count: 654, +}; + +export const baseResponse: ScrollL2BatchesResponse = { + items: [ + batchData, + { + number: 66879, + commitment_transaction: { + block_number: 19114386, + hash: '0x0d33245814b9e61c8f0ed6fd3fb7464f34be33d2c3aee69629d65e8995d77edc', + timestamp: '2024-01-29T19:52:35.000000Z', + }, + confirmation_transaction: { + block_number: 19114558, + hash: '0x6f9a19d503947ec91d6e9d5c2129913a7def86fd0f87061c06e5994cf857bee0', + timestamp: '2024-01-29T20:27:11.000000Z', + }, + data_availability: { + batch_data_container: 'in_calldata', + }, + start_block_number: 456000, + end_block_number: 789000, + transactions_count: 962, + }, + ], + next_page_params: { + items_count: 50, + number: 1, + }, +}; diff --git a/explorer/frontend/mocks/search/index.ts b/explorer/frontend/mocks/search/index.ts new file mode 100644 index 000000000..a7d4c2215 --- /dev/null +++ b/explorer/frontend/mocks/search/index.ts @@ -0,0 +1,201 @@ +import type { + SearchResultToken, + SearchResultBlock, + SearchResultAddressOrContract, + SearchResultTx, + SearchResultLabel, + SearchResult, + SearchResultUserOp, + SearchResultBlob, + SearchResultDomain, + SearchResultMetadataTag, +} from 'types/api/search'; + +export const token1: SearchResultToken = { + address_hash: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + name: 'Toms NFT', + symbol: 'TNT', + token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + type: 'token' as const, + icon_url: 'http://localhost:3000/token-icon.png', + token_type: 'ERC-721', + total_supply: '10000001', + exchange_rate: null, + is_verified_via_admin_panel: true, + is_smart_contract_verified: true, +}; + +export const token2: SearchResultToken = { + address_hash: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + name: 'TomToken', + symbol: 'pdE1B', + token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + type: 'token' as const, + icon_url: null, + token_type: 'ERC-20', + total_supply: '10000001', + exchange_rate: '1.11', + is_verified_via_admin_panel: false, + is_smart_contract_verified: false, +}; + +export const block1: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', + block_number: 8198536, + type: 'block' as const, + timestamp: '2022-12-11T17:55:20Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', +}; + +export const block2: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', + block_number: 8198536, + block_type: 'reorg', + type: 'block' as const, + timestamp: '2022-12-11T18:55:20Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', +}; + +export const block3: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3', + block_number: 8198536, + block_type: 'uncle', + type: 'block' as const, + timestamp: '2022-12-11T18:11:11Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3', +}; + +export const address1: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const address2: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + ens_info: { + address_hash: '0x1234567890123456789012345678901234567890', + expiry_date: '2022-12-11T17:55:20Z', + name: 'utko.eth', + names_count: 1, + }, +}; + +export const contract1: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'Unknown contract in this network', + type: 'contract' as const, + is_smart_contract_verified: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const contract2: SearchResultAddressOrContract = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'Super utko', + type: 'contract' as const, + is_smart_contract_verified: true, + certified: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const label1: SearchResultLabel = { + address_hash: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'utko', + type: 'label' as const, + is_smart_contract_verified: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const tx1: SearchResultTx = { + transaction_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', + type: 'transaction' as const, + timestamp: '2022-12-11T17:55:20Z', + url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', +}; + +export const userOp1: SearchResultUserOp = { + timestamp: '2024-01-11T14:15:48.000000Z', + type: 'user_operation', + user_operation_hash: '0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', + url: '/op/0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', +}; + +export const blob1: SearchResultBlob = { + blob_hash: '0x0108dd3e414da9f3255f7a831afa606e8dfaea93d082dfa9b15305583cbbdbbe', + type: 'blob' as const, + timestamp: null, +}; + +export const domain1: SearchResultDomain = { + address_hash: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + ens_info: { + address_hash: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + expiry_date: '2039-09-01T07:36:18.000Z', + name: 'vitalik.eth', + names_count: 1, + }, + is_smart_contract_verified: false, + name: null, + type: 'ens_domain', + url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', +}; + +export const metatag1: SearchResultMetadataTag = { + ...address1, + type: 'metadata_tag', + metadata: { + name: 'utko', + slug: 'utko', + meta: {}, + tagType: 'name', + ordinal: 1, + }, +}; + +export const metatag2: SearchResultMetadataTag = { + ...address2, + type: 'metadata_tag', + metadata: { + name: 'utko', + slug: 'utko', + meta: {}, + tagType: 'name', + ordinal: 1, + }, +}; + +export const metatag3: SearchResultMetadataTag = { + ...contract2, + type: 'metadata_tag', + metadata: { + name: 'super utko', + slug: 'super-utko', + meta: {}, + tagType: 'protocol', + ordinal: 1, + }, +}; + +export const baseResponse: SearchResult = { + items: [ + token1, + token2, + block1, + address1, + contract1, + tx1, + blob1, + domain1, + metatag1, + + ], + next_page_params: null, +}; diff --git a/explorer/frontend/mocks/shibarium/deposits.ts b/explorer/frontend/mocks/shibarium/deposits.ts new file mode 100644 index 000000000..7081042cd --- /dev/null +++ b/explorer/frontend/mocks/shibarium/deposits.ts @@ -0,0 +1,61 @@ +import type { ShibariumDepositsResponse } from 'types/api/shibarium'; + +export const data: ShibariumDepositsResponse = { + items: [ + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + ], + next_page_params: { + items_count: 50, + block_number: 8382363, + }, +}; diff --git a/explorer/frontend/mocks/shibarium/withdrawals.ts b/explorer/frontend/mocks/shibarium/withdrawals.ts new file mode 100644 index 000000000..6c1e87526 --- /dev/null +++ b/explorer/frontend/mocks/shibarium/withdrawals.ts @@ -0,0 +1,61 @@ +import type { ShibariumWithdrawalsResponse } from 'types/api/shibarium'; + +export const data: ShibariumWithdrawalsResponse = { + items: [ + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + ], + next_page_params: { + items_count: 50, + block_number: 8382363, + }, +}; diff --git a/explorer/frontend/mocks/stats/daily_txs.ts b/explorer/frontend/mocks/stats/daily_txs.ts new file mode 100644 index 000000000..804c3c485 --- /dev/null +++ b/explorer/frontend/mocks/stats/daily_txs.ts @@ -0,0 +1,149 @@ +export const base = { + chart_data: [ + { + date: '2022-11-28', + transactions_count: 26815, + }, + { + date: '2022-11-27', + transactions_count: 34784, + }, + { + date: '2022-11-26', + transactions_count: 77527, + }, + { + date: '2022-11-25', + transactions_count: 39687, + }, + { + date: '2022-11-24', + transactions_count: 40752, + }, + { + date: '2022-11-23', + transactions_count: 32569, + }, + { + date: '2022-11-22', + transactions_count: 34449, + }, + { + date: '2022-11-21', + transactions_count: 106047, + }, + { + date: '2022-11-20', + transactions_count: 107713, + }, + { + date: '2022-11-19', + transactions_count: 96311, + }, + { + date: '2022-11-18', + transactions_count: 30828, + }, + { + date: '2022-11-17', + transactions_count: 27422, + }, + { + date: '2022-11-16', + transactions_count: 75898, + }, + { + date: '2022-11-15', + transactions_count: 84084, + }, + { + date: '2022-11-14', + transactions_count: 62266, + }, + { + date: '2022-11-13', + transactions_count: 22338, + }, + { + date: '2022-11-12', + transactions_count: 86764, + }, + { + date: '2022-11-11', + transactions_count: 79493, + }, + { + date: '2022-11-10', + transactions_count: 92887, + }, + { + date: '2022-11-09', + transactions_count: 43691, + }, + { + date: '2022-11-08', + transactions_count: 74197, + }, + { + date: '2022-11-07', + transactions_count: 58131, + }, + { + date: '2022-11-06', + transactions_count: 62477, + }, + { + date: '2022-11-05', + transactions_count: 82897, + }, + { + date: '2022-11-04', + transactions_count: 91725, + }, + { + date: '2022-11-03', + transactions_count: 83667, + }, + { + date: '2022-11-02', + transactions_count: 63743, + }, + { + date: '2022-11-01', + transactions_count: 152059, + }, + { + date: '2022-10-31', + transactions_count: 62519, + }, + { + date: '2022-10-30', + transactions_count: 48569, + }, + { + date: '2022-10-29', + transactions_count: 36789, + }, + ], +}; + +export const partialData = { + chart_data: [ + { date: '2022-11-28', transactions_count: 26815 }, + { date: '2022-11-27', transactions_count: 34784 }, + { date: '2022-11-26', transactions_count: 77527 }, + { date: '2022-11-25', transactions_count: null }, + { date: '2022-11-24', transactions_count: null }, + { date: '2022-11-23', transactions_count: null }, + { date: '2022-11-22', transactions_count: 63433 }, + { date: '2022-11-21', transactions_count: null }, + ], +}; + +export const noData = { + chart_data: [ + { date: '2022-11-25', transactions_count: null }, + { date: '2022-11-24', transactions_count: null }, + { date: '2022-11-23', transactions_count: null }, + ], +}; diff --git a/explorer/frontend/mocks/stats/index.ts b/explorer/frontend/mocks/stats/index.ts new file mode 100644 index 000000000..725831d2d --- /dev/null +++ b/explorer/frontend/mocks/stats/index.ts @@ -0,0 +1,91 @@ +import { mapValues } from 'es-toolkit'; + +import type { HomeStats } from 'types/api/stats'; + +export const base: HomeStats = { + average_block_time: 6212.0, + coin_price: '0.00199678', + coin_price_change_percentage: -7.42, + coin_image: 'http://localhost:3100/utia.jpg', + gas_prices: { + average: { + fiat_price: '1.39', + price: 23.75, + time: 12030.25, + base_fee: 2.22222, + priority_fee: 12.424242, + }, + fast: { + fiat_price: '1.74', + price: 29.72, + time: 8763.25, + base_fee: 4.44444, + priority_fee: 22.242424, + }, + slow: { + fiat_price: '1.35', + price: 23.04, + time: 20100.25, + base_fee: 1.11111, + priority_fee: 7.8909, + }, + }, + gas_price_updated_at: '2022-11-11T11:09:49.051171Z', + gas_prices_update_in: 300000, + gas_used_today: '4108680603', + market_cap: '330809.96443288102524', + network_utilization_percentage: 1.55372064, + static_gas_price: '10', + total_addresses: '19667249', + total_blocks: '30215608', + total_gas_used: '0', + total_transactions: '82258122', + transactions_today: '26815', + tvl: '1767425.102766552', +}; + +export const withBtcLocked: HomeStats = { + ...base, + rootstock_locked_btc: '3337493406696977561374', +}; + +export const withoutFiatPrices: HomeStats = { + ...base, + gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null) : null, +}; + +export const withoutGweiPrices: HomeStats = { + ...base, + gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null) : null, +}; + +export const withoutBothPrices: HomeStats = { + ...base, + gas_prices: base.gas_prices ? mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null) : null, +}; + +export const withoutGasInfo: HomeStats = { + ...base, + gas_prices: null, +}; + +export const withSecondaryCoin: HomeStats = { + ...base, + secondary_coin_price: '3.398', + secondary_coin_image: 'http://localhost:3100/secondary_utia.jpg', +}; + +export const noChartData: HomeStats = { + ...base, + transactions_today: null, + coin_price: null, + market_cap: null, + tvl: null, +}; + +export const indexingStatus = { + finished_indexing_blocks: false, + indexed_blocks_ratio: '0.1', + finished_indexing: true, + indexed_internal_transactions_ratio: '1', +}; diff --git a/explorer/frontend/mocks/stats/line.ts b/explorer/frontend/mocks/stats/line.ts new file mode 100644 index 000000000..0c10eed84 --- /dev/null +++ b/explorer/frontend/mocks/stats/line.ts @@ -0,0 +1,198 @@ +import type * as stats from '@blockscout/stats-types'; + +export const averageGasPrice: stats.LineChart = { + chart: [ + { + date: '2023-12-22', + date_to: '2023-12-22', + value: '37.7804422597599', + is_approximate: false, + }, + { + date: '2023-12-23', + date_to: '2023-12-23', + value: '25.84889883009387', + is_approximate: false, + }, + { + date: '2023-12-24', + date_to: '2023-12-24', + value: '25.818463227198574', + is_approximate: false, + }, + { + date: '2023-12-25', + date_to: '2023-12-25', + value: '26.045513050051298', + is_approximate: false, + }, + { + date: '2023-12-26', + date_to: '2023-12-26', + value: '21.42600692652399', + is_approximate: false, + }, + { + date: '2023-12-27', + date_to: '2023-12-27', + value: '31.066730409846656', + is_approximate: false, + }, + { + date: '2023-12-28', + date_to: '2023-12-28', + value: '33.63955781902089', + is_approximate: false, + }, + { + date: '2023-12-29', + date_to: '2023-12-29', + value: '28.064736756058384', + is_approximate: false, + }, + { + date: '2023-12-30', + date_to: '2023-12-30', + value: '23.074500869678175', + is_approximate: false, + }, + { + date: '2023-12-31', + date_to: '2023-12-31', + value: '17.651005734615133', + is_approximate: false, + }, + { + date: '2024-01-01', + date_to: '2023-01-01', + value: '14.906085174476441', + is_approximate: false, + }, + { + date: '2024-01-02', + date_to: '2023-01-02', + value: '22.28459059038656', + is_approximate: false, + }, + { + date: '2024-01-03', + date_to: '2023-01-03', + value: '39.8311646806592', + is_approximate: false, + }, + { + date: '2024-01-04', + date_to: '2023-01-04', + value: '26.09989322256083', + is_approximate: false, + }, + { + date: '2024-01-05', + date_to: '2023-01-05', + value: '22.821996688111998', + is_approximate: false, + }, + { + date: '2024-01-06', + date_to: '2023-01-06', + value: '20.32680041262083', + is_approximate: false, + }, + { + date: '2024-01-07', + date_to: '2023-01-07', + value: '32.535045831809704', + is_approximate: false, + }, + { + date: '2024-01-08', + date_to: '2023-01-08', + value: '27.443477102139482', + is_approximate: false, + }, + { + date: '2024-01-09', + date_to: '2023-01-09', + value: '20.7911332558055', + is_approximate: false, + }, + { + date: '2024-01-10', + date_to: '2023-01-10', + value: '42.10740192523919', + is_approximate: false, + }, + { + date: '2024-01-11', + date_to: '2023-01-11', + value: '35.75215680343582', + is_approximate: false, + }, + { + date: '2024-01-12', + date_to: '2023-01-12', + value: '27.430414798093253', + is_approximate: false, + }, + { + date: '2024-01-13', + date_to: '2023-01-13', + value: '20.170934096589875', + is_approximate: false, + }, + { + date: '2024-01-14', + date_to: '2023-01-14', + value: '38.79660984371034', + is_approximate: false, + }, + { + date: '2024-01-15', + date_to: '2023-01-15', + value: '26.140740484554204', + is_approximate: false, + }, + { + date: '2024-01-16', + date_to: '2023-01-16', + value: '36.708543184194156', + is_approximate: false, + }, + { + date: '2024-01-17', + date_to: '2023-01-17', + value: '40.325438794298876', + is_approximate: false, + }, + { + date: '2024-01-18', + date_to: '2023-01-18', + value: '37.55145309930694', + is_approximate: false, + }, + { + date: '2024-01-19', + date_to: '2023-01-19', + value: '33.271450114434664', + is_approximate: false, + }, + { + date: '2024-01-20', + date_to: '2023-01-20', + value: '19.303304377685638', + is_approximate: false, + }, + { + date: '2024-01-21', + date_to: '2023-01-21', + value: '14.375908594704976', + is_approximate: false, + }, + ], + info: { + title: 'Chart title', + description: 'Chart description', + id: 'chart', + resolutions: [ 'DAY', 'MONTH' ], + }, +}; diff --git a/explorer/frontend/mocks/stats/lines.ts b/explorer/frontend/mocks/stats/lines.ts new file mode 100644 index 000000000..9dfccb0fd --- /dev/null +++ b/explorer/frontend/mocks/stats/lines.ts @@ -0,0 +1,161 @@ +import type * as stats from '@blockscout/stats-types'; + +export const base: stats.LineCharts = { + sections: [ + { + id: 'accounts', + title: 'Accounts', + charts: [ + { + id: 'accountsGrowth', + title: 'Accounts growth', + description: 'Cumulative accounts number per period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'activeAccounts', + title: 'Active accounts', + description: 'Active accounts number per period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'newAccounts', + title: 'New accounts', + description: 'New accounts number per day', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], + }, + { + id: 'transactions', + title: 'Transactions', + charts: [ + { + id: 'averageTxnFee', + title: 'Average transaction fee', + description: 'The average amount in ETH spent per transaction', + units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'newTxns', + title: 'New transactions', + description: 'New transactions number', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'txnsFee', + title: 'Transactions fees', + description: 'Amount of tokens paid as fees', + units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'txnsGrowth', + title: 'Transactions growth', + description: 'Cumulative transactions number', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'txnsSuccessRate', + title: 'Transactions success rate', + description: 'Successful transactions rate per day', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], + }, + { + id: 'blocks', + title: 'Blocks', + charts: [ + { + id: 'averageBlockRewards', + title: 'Average block rewards', + description: 'Average amount of distributed reward in tokens per day', + units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'averageBlockSize', + title: 'Average block size', + description: 'Average size of blocks in bytes', + units: 'Bytes', + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'newBlocks', + title: 'New blocks', + description: 'New blocks number', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], + }, + { + id: 'tokens', + title: 'Tokens', + charts: [ + { + id: 'newNativeCoinTransfers', + title: 'New ETH transfers', + description: 'New token transfers number for the period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], + }, + { + id: 'gas', + title: 'Gas', + charts: [ + { + id: 'averageGasLimit', + title: 'Average gas limit', + description: 'Average gas limit per block for the period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'averageGasPrice', + title: 'Average gas price', + description: 'Average gas price for the period (Gwei)', + units: 'Gwei', + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'gasUsedGrowth', + title: 'Gas used growth', + description: 'Cumulative gas used for the period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], + }, + { + id: 'contracts', + title: 'Contracts', + charts: [ + { + id: 'newVerifiedContracts', + title: 'New verified contracts', + description: 'New verified contracts number for the period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'verifiedContractsGrowth', + title: 'Verified contracts growth', + description: 'Cumulative number verified contracts for the period', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], + }, + ], +}; diff --git a/explorer/frontend/mocks/stats/main.tsx b/explorer/frontend/mocks/stats/main.tsx new file mode 100644 index 000000000..a3a6bbd01 --- /dev/null +++ b/explorer/frontend/mocks/stats/main.tsx @@ -0,0 +1,71 @@ +import type * as stats from '@blockscout/stats-types'; + +import { averageGasPrice } from './line'; + +export const base: stats.MainPageStats = { + average_block_time: { + id: 'averageBlockTime', + value: '14.909090909090908', + title: 'Average block time', + units: 's', + description: 'Average time taken in seconds for a block to be included in the blockchain', + }, + total_addresses: { + id: 'totalAddresses', + value: '113606435', + title: 'Total addresses', + description: 'Number of addresses that participated in the blockchain', + }, + total_blocks: { + id: 'totalBlocks', + value: '7660515', + title: 'Total blocks', + description: 'Number of blocks over all time', + }, + total_transactions: { + id: 'totalTxns', + value: '411264599', + title: 'Total txns', + description: 'All transactions including pending, dropped, replaced, failed transactions', + }, + yesterday_transactions: { + id: 'yesterdayTxns', + value: '213019', + title: 'Yesterday txns', + description: 'Number of transactions yesterday (0:00 - 23:59 UTC)', + }, + total_operational_transactions: { + id: 'totalOperationalTxns', + value: '403598877', + title: 'Total operational txns', + description: '\'Total txns\' without block creation transactions', + }, + yesterday_operational_transactions: { + id: 'yesterdayOperationalTxns', + value: '210852', + title: 'Yesterday operational txns', + description: 'Number of transactions yesterday (0:00 - 23:59 UTC) without block creation transactions', + }, + daily_new_transactions: { + chart: averageGasPrice.chart, + info: { + id: 'newTxnsWindow', + title: 'Daily transactions', + description: 'The chart displays daily transactions for the past 30 days', + resolutions: [ + 'DAY', + ], + }, + }, + daily_new_operational_transactions: { + chart: averageGasPrice.chart, + info: { + id: 'newOperationalTxnsWindow', + title: 'Daily operational transactions', + description: 'The chart displays daily transactions for the past 30 days (without block creation transactions)', + resolutions: [ + 'DAY', + ], + }, + }, +}; diff --git a/explorer/frontend/mocks/tokens/tokenHolders.ts b/explorer/frontend/mocks/tokens/tokenHolders.ts new file mode 100644 index 000000000..89f259548 --- /dev/null +++ b/explorer/frontend/mocks/tokens/tokenHolders.ts @@ -0,0 +1,39 @@ +import type { TokenHolders } from 'types/api/token'; + +import { withName, withoutName } from 'mocks/address/address'; + +export const tokenHoldersERC20: TokenHolders = { + items: [ + { + address: withName, + value: '107014805905725000000', + }, + { + address: withoutName, + value: '207014805905725000000', + }, + ], + next_page_params: { + value: '50', + items_count: 50, + }, +}; + +export const tokenHoldersERC1155: TokenHolders = { + items: [ + { + address: withName, + value: '107014805905725000000', + token_id: '12345', + }, + { + address: withoutName, + value: '207014805905725000000', + token_id: '12345', + }, + ], + next_page_params: { + value: '50', + items_count: 50, + }, +}; diff --git a/explorer/frontend/mocks/tokens/tokenInfo.ts b/explorer/frontend/mocks/tokens/tokenInfo.ts new file mode 100644 index 000000000..5b6a36831 --- /dev/null +++ b/explorer/frontend/mocks/tokens/tokenInfo.ts @@ -0,0 +1,212 @@ +import type { TokenCounters, TokenInfo } from 'types/api/token'; + +export const tokenInfo: TokenInfo = { + address_hash: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', + circulating_market_cap: '117629601.61913824', + decimals: '18', + exchange_rate: '2.0101', + holders_count: '46554', + name: 'ARIANEE', + symbol: 'ARIA', + type: 'ERC-20' as const, + total_supply: '1235', + icon_url: 'http://localhost:3000/token-icon.png', +}; + +export const tokenCounters: TokenCounters = { + token_holders_count: '8838883', + transfers_count: '88282281', +}; + +export const tokenInfoERC20a: TokenInfo<'ERC-20'> = { + address_hash: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456', + circulating_market_cap: '117268489.23970924', + decimals: '18', + exchange_rate: null, + holders_count: '23', + name: 'hyfi.token', + symbol: 'HyFi', + total_supply: '369000000000000000000000000', + type: 'ERC-20' as const, + icon_url: 'http://localhost:3000/token-icon.png', +}; + +export const tokenInfoERC20b: TokenInfo<'ERC-20'> = { + address_hash: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7', + circulating_market_cap: '115060192.36105014', + decimals: '6', + exchange_rate: '0.982', + holders_count: '17', + name: 'USD Coin', + symbol: 'USDC', + total_supply: '900000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC20c: TokenInfo<'ERC-20'> = { + address_hash: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A8', + circulating_market_cap: null, + decimals: '18', + exchange_rate: '1328.89', + holders_count: '17', + name: 'Ethereum', + symbol: 'ETH', + total_supply: '1000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC20d: TokenInfo<'ERC-20'> = { + address_hash: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9196', + circulating_market_cap: null, + decimals: '18', + exchange_rate: null, + holders_count: '102625', + name: 'Zeta', + symbol: 'ZETA', + total_supply: '2100000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = { + address_hash: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9197', + circulating_market_cap: '112855875.75888918', + decimals: '18', + exchange_rate: '1328.89', + holders_count: '102625', + name: 'Zeta', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + total_supply: '2100000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC721a: TokenInfo<'ERC-721'> = { + address_hash: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '7', + name: 'HyFi Athena', + symbol: 'HYFI_ATHENA', + total_supply: '105', + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC721b: TokenInfo<'ERC-721'> = { + address_hash: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '2', + name: 'World Of Women Galaxy', + symbol: 'WOWG', + total_supply: null, + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC721c: TokenInfo<'ERC-721'> = { + address_hash: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '12', + name: 'Puma', + symbol: 'PUMA', + total_supply: null, + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = { + address_hash: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4993', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '12', + name: 'Puma', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + total_supply: null, + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = { + address_hash: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '22', + name: 'HyFi Membership', + symbol: 'HYFI_MEMBERSHIP', + total_supply: '482', + type: 'ERC-1155' as const, + icon_url: null, +}; + +export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = { + address_hash: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '100', + name: 'WinkyVerse Collections', + symbol: 'WVC', + total_supply: '4943', + type: 'ERC-1155' as const, + icon_url: null, +}; + +export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = { + address_hash: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8a', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '22', + name: null, + symbol: null, + total_supply: '482', + type: 'ERC-1155' as const, + icon_url: null, +}; + +export const tokenInfoERC404: TokenInfo<'ERC-404'> = { + address_hash: '0xB5C457dDB4cE3312a6C5a2b056a1652bd542a208', + circulating_market_cap: '0.0', + decimals: '18', + exchange_rate: '1484.13', + holders_count: '81', + icon_url: null, + name: 'OMNI404', + symbol: 'O404', + total_supply: '6482275000000000000', + type: 'ERC-404' as const, +}; + +export const bridgedTokenA: TokenInfo<'ERC-20'> = { + ...tokenInfoERC20a, + is_bridged: true, + origin_chain_id: '1', + bridge_type: 'omni', + foreign_address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8b', +}; + +export const bridgedTokenB: TokenInfo<'ERC-20'> = { + ...tokenInfoERC20b, + is_bridged: true, + origin_chain_id: '56', + bridge_type: 'omni', + foreign_address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefd', +}; + +export const bridgedTokenC: TokenInfo<'ERC-20'> = { + ...tokenInfoERC20d, + is_bridged: true, + origin_chain_id: '99', + bridge_type: 'amb', + foreign_address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4994', +}; diff --git a/explorer/frontend/mocks/tokens/tokenInstance.ts b/explorer/frontend/mocks/tokens/tokenInstance.ts new file mode 100644 index 000000000..1dc1b9649 --- /dev/null +++ b/explorer/frontend/mocks/tokens/tokenInstance.ts @@ -0,0 +1,185 @@ +/* eslint-disable max-len */ +import type { TokenInstance } from 'types/api/token'; + +import * as addressMock from '../address/address'; + +export const base: TokenInstance = { + animation_url: null, + external_app_url: 'https://duck.nft/get-your-duck-today', + id: '32925298983216553915666621415831103694597106215670571463977478984525997408266', + image_url: 'https://example.com/image.jpg', + is_unique: false, + holder_address_hash: null, + metadata: { + attributes: [ + { + trait_type: 'skin', + value: '0', + }, + { + trait_type: 'eye', + value: '2', + }, + { + trait_type: 'nose', + value: '6', + }, + { + trait_type: 'spectacles', + value: '4', + }, + { + trait_type: 'hair', + value: '12', + }, + { + trait_type: 'shirt', + value: '1', + }, + { + trait_type: 'earrings', + value: '4', + }, + { + trait_type: 'mouth', + value: '5', + }, + { + trait_type: 'eventURL', + value: 'https://twitter.com/lilnounsdao?s=21&t=xAihrtwPd6avwdsQqeMXCw', + }, + { + trait_type: 'p1', + value: '57775', + }, + { + trait_type: 'p2', + value: '57772', + }, + { + display_type: 'number', + trait_type: 'difficulty', + value: 84, + }, + { + display_type: 'number', + trait_type: 'items', + value: 3, + }, + ], + description: '**GENESIS #188848**, **22a5f8bbb1602995** :: *84th* generation of *#57772 and #57775* :: **eGenetic Hash Code (eDNA)** = *3c457cc7f60f7853* :: [Click here for full biography.](https://vipsland.com/nft/collections/genesis/188848) :: crafted by [vipsland](https://vipsland.com/)', + external_url: 'https://vipsland.com/nft/collections/genesis/188848', + image: 'https://i.seadn.io/gcs/files/1ee1c5e1ead058322615e3206abb8ba3.png?w=500&auto=format', + name: 'GENESIS #188848, 22a5f8bbb1602995. Blockchain pixel PFP NFT + "on music video" trait inspired by God', + }, + owner: addressMock.withName, + thumbnails: null, +}; + +export const withRichMetadata: TokenInstance = { + ...base, + metadata: { + background_color: '000000', + chain: 'MATIC', + chain_address: '0x66edbdb80001da74cbf3e6c01ba91154f6e2fb7c', + name: 'Carmelo Anthony', + total_nfts: 0, + animation_url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/dee8734bbefb0d63d6156b6fa0e1385822480589daa1862cbd37a94f6bc2ba3a', + series_key: 'Series', + nft_id: 'c746af09-8dcb-4cec-aa8a-5ff02fffc3f1', + description: 'All-Conference and All-American honors await Carmelo Anthony during his Freshman season for Syracuse. However, Anthony must first defeat a worthy opponent in Georgetown with a double-double effort of 30 points and 15 rebounds.\n \n\n© Syracuse University', + immutable_uri: 'https://nftu.com/nft-content/metadata/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/7741920', + contract_address: '0x63cf7b3d5808cb190aa301b55aafd6b4bb95efbb', + is_pack: false, + pack_open_locked_until: '2022-03-05T16:58:30.998Z', + rarity_key: 'Rarity', + images: { + png: { + primary: { + url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/0c66645c4e119f9c5def80273b768138d797f00583f557065a50bb0dd491e8e3', + cid: 'Qmf9hHAP884ZwYngk3VdVU7rhKDToykTy24WmcoegapnG8', + }, + secondary: { + more: { + deeper: { + jpeg: { + url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/0c66645c4e119f9c5def80273b768138d797f00583f557065a50bb0dd491e8e3/pfp_3.png', + }, + }, + }, + }, + }, + mp4: { + primary: { + url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/dee8734bbefb0d63d6156b6fa0e1385822480589daa1862cbd37a94f6bc2ba3a', + cid: 'QmPGMksnyQemncHKQ67zGiuTAsnFi8HTJkY9ebQ6eVVQLv', + }, + }, + 'default': 'mp4', + webp: [ + 'QmPGMksnyQemncHKQ67zGiuTAsnFi8HTJkY9ebQ6eVVQLv', + 'https://nftu.com/nft/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/949', + { + label: 'fancy label', + data: [ + { + name: 'John', + email: 'john@foo.com', + }, + { + name: 'Mary', + email: 'mary@foo.com', + }, + [ 1, 2 ], + ], + }, + [ + { + address: 'unknown', + age: 523, + gender: 'male', + }, + { + address: 'bar', + age: 24, + gender: 'https://nftu.com/nft/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/949', + }, + ], + ], + }, + royalty_amount: 1000, + rarity: 'Premium', + set_key: 'Set', + external_url: 'https://nftu.com/nft/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/949', + attributes: [ + { + value: 'NCAABB', + trait_type: 'Sport', + }, + { + value: 'Player', + trait_type: 'Type', + }, + { + value: '15', + trait_type: 'Player Jersey Number', + display_type: 'number', + }, + ], + tags: [ 'foo', 123, true ], + token_id: '7741920', + serial_total: 1100, + blockchain_state: 'BURNING', + image: 'ipfs://dee8734bbefb0d63d6156b6fa0e1385822480589daa1862cbd37a94f6bc2ba3a', + revealed_nfts: null, + nft_data_id: '92ee5f5c-bce9-4d64-8a25-c7e1e6305572', + series: 'Tip-Off', + immutable_cid: 'QmVigZH1P3D6QWvp2SWVreTPKmDvUYUidNzcUrcYzATpyJ', + status: null, + }, +}; + +export const unique: TokenInstance = { + ...base, + is_unique: true, +}; diff --git a/explorer/frontend/mocks/tokens/tokenTransfer.ts b/explorer/frontend/mocks/tokens/tokenTransfer.ts new file mode 100644 index 000000000..3961c2ef2 --- /dev/null +++ b/explorer/frontend/mocks/tokens/tokenTransfer.ts @@ -0,0 +1,253 @@ +import type { TokenInfo } from 'types/api/token'; +import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer'; + +import * as tokenInstanceMock from './tokenInstance'; + +export const erc20: TokenTransfer = { + from: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51', + implementations: null, + is_contract: true, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address_hash: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', + circulating_market_cap: '117629601.61913824', + decimals: '18', + exchange_rate: '42', + holders_count: '46554', + name: 'ARIANEE', + symbol: 'ARIA', + type: 'ERC-20', + total_supply: '0', + icon_url: null, + }, + total: { + decimals: '18', + value: '31567373703130350', + }, + transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + type: 'token_transfer', + timestamp: '2022-10-10T14:34:30.000000Z', + block_number: '12345', + block_hash: '1', + log_index: '1', + method: 'updateSmartAsset', +}; + +export const erc721: TokenTransfer = { + from: { + hash: '0x621C2a125ec4A6D8A7C7A655A18a2868d35eb43C', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + to: { + hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + token: { + address_hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '63090', + name: 'Arianee Smart-Asset', + symbol: 'AriaSA', + type: 'ERC-721', + total_supply: '0', + icon_url: null, + }, + total: { + token_id: '875879856', + token_instance: tokenInstanceMock.base, + }, + transaction_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc', + type: 'token_transfer', + timestamp: '2022-10-10T14:34:30.000000Z', + block_number: '12345', + block_hash: '1', + log_index: '1', + method: 'updateSmartAsset', +}; + +export const erc1155A: TokenTransfer = { + from: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address_hash: '0xF56b7693E4212C584de4a83117f805B8E89224CB', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '1', + name: null, + symbol: 'MY_SYMBOL_IS_VERY_LONG', + type: 'ERC-1155', + total_supply: '0', + icon_url: null, + }, + total: { + token_id: '123', + value: '42', + decimals: null, + token_instance: null, + }, + transaction_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', + type: 'token_minting', + timestamp: '2022-10-10T14:34:30.000000Z', + block_number: '12345', + block_hash: '1', + log_index: '1', +}; + +export const erc1155B: TokenTransfer = { + ...erc1155A, + token: { + ...(erc1155A.token as TokenInfo<'ERC-1155'>), + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '12345678', value: '100000000000000000000', decimals: null, token_instance: null }, +}; + +export const erc1155C: TokenTransfer = { + ...erc1155A, + token: { + ...(erc1155A.token as TokenInfo<'ERC-1155'>), + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null, token_instance: null }, +}; + +export const erc1155D: TokenTransfer = { + ...erc1155A, + token: { + ...(erc1155A.token as TokenInfo<'ERC-1155'>), + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '456', value: '42', decimals: null, token_instance: null }, +}; + +export const erc404A: TokenTransfer = { + from: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address_hash: '0xF56b7693E4212C584de4a83117f805B8E89224CB', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '1', + name: null, + symbol: 'MY_SYMBOL_IS_VERY_LONG', + type: 'ERC-404', + total_supply: '0', + icon_url: null, + }, + total: { + value: '42000000000000000000000000', + decimals: '18', + token_id: null, + token_instance: null, + }, + transaction_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', + type: 'token_transfer', + method: 'swap', + timestamp: '2022-10-10T14:34:30.000000Z', + block_number: '12345', + block_hash: '1', + log_index: '1', +}; + +export const erc404B: TokenTransfer = { + ...erc404A, + token: { + ...(erc404A.token as TokenInfo<'ERC-404'>), + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '4625304364899952', token_instance: null }, +}; + +export const mixTokens: TokenTransferResponse = { + items: [ + erc20, + erc721, + erc1155A, + erc1155B, + erc1155C, + erc1155D, + erc404A, + erc404B, + ], + next_page_params: null, +}; diff --git a/explorer/frontend/mocks/txs/decodedInputData.ts b/explorer/frontend/mocks/txs/decodedInputData.ts new file mode 100644 index 000000000..bbb127425 --- /dev/null +++ b/explorer/frontend/mocks/txs/decodedInputData.ts @@ -0,0 +1,49 @@ +import type { DecodedInput } from 'types/api/decodedInput'; + +export const withoutIndexedFields: DecodedInput = { + method_call: 'CreditSpended(uint256 _type, uint256 _quantity)', + method_id: '58cdf94a', + parameters: [ + { + name: '_type', + type: 'uint256', + value: '3', + }, + { + name: '_quantity', + type: 'uint256', + value: '1', + }, + ], +}; + +export const withIndexedFields: DecodedInput = { + method_call: 'Transfer(address indexed from, address indexed to, uint256 value)', + method_id: 'ddf252ad', + parameters: [ + { + indexed: true, + name: 'from', + type: 'address', + value: '0xd789a607ceac2f0e14867de4eb15b15c9ffb5859', + }, + { + indexed: true, + name: 'to', + type: 'address', + value: '0x7d20a8d54f955b4483a66ab335635ab66e151c51', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + value: '31567373703130350', + }, + { + indexed: true, + name: 'inputArray', + type: 'uint256[2][2]', + value: [ [ '1', '1' ], [ '1', '1' ] ], + }, + ], +}; diff --git a/explorer/frontend/mocks/txs/internalTxs.ts b/explorer/frontend/mocks/txs/internalTxs.ts new file mode 100644 index 000000000..7917e0851 --- /dev/null +++ b/explorer/frontend/mocks/txs/internalTxs.ts @@ -0,0 +1,80 @@ +import type { InternalTransaction, InternalTransactionsResponse } from 'types/api/internalTransaction'; + +export const base: InternalTransaction = { + block_number: 29611822, + created_contract: null, + error: null, + from: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + gas_limit: '757586', + index: 1, + success: true, + timestamp: '2022-10-10T14:43:05.000000Z', + to: { + hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeCreditHistory', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61', + type: 'call', + value: '42000000000000000000', +}; + +export const typeStaticCall: InternalTransaction = { + ...base, + type: 'staticcall', + to: { + ...base.to, + name: null, + }, + gas_limit: '63424243', + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff62', +}; + +export const withContractCreated: InternalTransaction = { + ...base, + type: 'delegatecall', + to: null, + from: { + ...base.from, + name: null, + }, + created_contract: { + hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a', + implementations: null, + is_contract: true, + is_verified: null, + name: 'Shavuha token', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + value: '1420000000000000000', + gas_limit: '5433', + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff63', +}; + +export const baseResponse: InternalTransactionsResponse = { + items: [ + base, + typeStaticCall, + withContractCreated, + ], + next_page_params: null, +}; diff --git a/explorer/frontend/mocks/txs/state.ts b/explorer/frontend/mocks/txs/state.ts new file mode 100644 index 000000000..850b7af35 --- /dev/null +++ b/explorer/frontend/mocks/txs/state.ts @@ -0,0 +1,197 @@ +import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges'; + +export const mintToken: TxStateChange = { + address: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: null, + balance_before: null, + change: [ + { + direction: 'from', + total: { + token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065', + token_instance: null, + }, + }, + ], + is_miner: false, + token: { + address_hash: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '9191', + name: 'ParaSpace Derivative Token MOONBIRD', + symbol: 'nMOONBIRD', + total_supply: '10645', + type: 'ERC-721', + icon_url: null, + }, + type: 'token' as const, +}; + +export const receiveMintedToken: TxStateChange = { + address: { + hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '1', + balance_before: '0', + change: [ + { + direction: 'to', + total: { + token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065', + token_instance: null, + }, + }, + ], + is_miner: false, + token: { + address_hash: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '9191', + name: 'ParaSpace Derivative Token MOONBIRD', + symbol: 'nMOONBIRD', + total_supply: '10645', + type: 'ERC-721', + icon_url: null, + }, + type: 'token' as const, +}; + +export const transfer1155Token: TxStateChange = { + address: { + hash: '0x51243E83Db20F8FC2761D894067A2A9eb7B158DE', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '1', + balance_before: '0', + change: '1', + is_miner: false, + token: { + address_hash: '0x56Cc277717106E528A9FcC2CD342Ff98db758041', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders_count: '50413', + icon_url: null, + name: null, + symbol: null, + total_supply: null, + type: 'ERC-1155', + }, + token_id: '1', + type: 'token' as const, +}; + +export const receiveCoin: TxStateChange = { + address: { + hash: '0x8dC847Af872947Ac18d5d63fA646EB65d4D99560', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '443787514723917012805', + balance_before: '443787484997510408745', + change: '29726406604060', + is_miner: true, + token: null, + type: 'coin' as const, +}; + +export const sendCoin: TxStateChange = { + address: { + hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '828282622733717191', + balance_before: '832127467556437753', + change: '-3844844822720562', + is_miner: false, + token: null, + type: 'coin' as const, +}; + +export const sendERC20Token: TxStateChange = { + address: { + hash: '0x7f6479df95Aa3036a3BE02DB6300ea201ABd9981', + ens_domain_name: null, + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + balance_after: '6814903154', + balance_before: '9814903154', + change: '-3000000000', + is_miner: false, + token: { + address_hash: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + circulating_market_cap: '82978861367.28714', + decimals: '6', + exchange_rate: '0.992839', + holders_count: null, + icon_url: 'https://gateway.tkn.xyz/ipfs/bafybeihrrubjya5nnwgqdm6mfqisxfnv76tl3yd452lkmgomn5n64gzbxu/', + name: 'Tether USD', + symbol: 'USDT', + total_supply: '39030615894320966', + type: 'ERC-20' as const, + }, + type: 'token' as const, +}; + +export const baseResponse: TxStateChanges = { + items: [ + mintToken, + receiveMintedToken, + sendCoin, + receiveCoin, + transfer1155Token, + sendERC20Token, + ], + next_page_params: { + items_count: 50, + state_changes: null, + }, +}; diff --git a/explorer/frontend/mocks/txs/stats.ts b/explorer/frontend/mocks/txs/stats.ts new file mode 100644 index 000000000..7b05dc975 --- /dev/null +++ b/explorer/frontend/mocks/txs/stats.ts @@ -0,0 +1,8 @@ +import type { TransactionsStats } from 'types/api/transaction'; + +export const base: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/explorer/frontend/mocks/txs/tx.ts b/explorer/frontend/mocks/txs/tx.ts new file mode 100644 index 000000000..fd38a045b --- /dev/null +++ b/explorer/frontend/mocks/txs/tx.ts @@ -0,0 +1,452 @@ +/* eslint-disable max-len */ +import type { Transaction } from 'types/api/transaction'; + +import * as addressMock from 'mocks/address/address'; +import { publicTag, privateTag, watchlistName } from 'mocks/address/tag'; +import * as interopMock from 'mocks/interop/interop'; +import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; +import * as decodedInputDataMock from 'mocks/txs/decodedInputData'; + +export const base: Transaction = { + base_fee_per_gas: '10000000000', + block_number: 29611750, + confirmation_duration: [ + 0, + 6364, + ], + confirmations: 508299, + created_contract: null, + decoded_input: decodedInputDataMock.withoutIndexedFields, + exchange_rate: '0.00254428', + fee: { + type: 'actual', + value: '7143168000000000', + }, + from: { + hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + implementations: null, + is_contract: false, + name: null, + is_verified: null, + private_tags: [ ], + public_tags: [ publicTag ], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + gas_limit: '800000', + gas_price: '48000000000', + gas_used: '148816', + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + max_fee_per_gas: '40190625000', + max_priority_fee_per_gas: '28190625000', + method: 'updateSmartAsset', + nonce: 27831, + position: 7, + priority_fee: '1299672384375000', + raw_input: '0xfa4b78b90000000000000000000000000000000000000000000000000000000005001bcfe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791000000000000000000000000a79b29ad7e0196c95b87f4663ded82fbf2e3add8', + result: 'success', + revert_reason: null, + status: 'ok', + timestamp: '2022-10-10T14:34:30.000000Z', + to: { + hash: addressMock.hash, + implementations: null, + is_contract: false, + is_verified: true, + name: null, + private_tags: [ privateTag ], + public_tags: [], + watchlist_names: [ watchlistName ], + ens_domain_name: null, + }, + token_transfers: [], + token_transfers_overflow: false, + transaction_burnt_fee: '461030000000000', + transaction_tag: null, + transaction_types: [ + 'contract_call', + ], + type: 2, + value: '42000000000000000000', + actions: [], + has_error_in_internal_transactions: false, +}; + +export const withWatchListNames: Transaction = { + ...base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194', + from: { + ...base.from, + watchlist_names: [ + { label: 'from #1', display_name: 'from utka' }, + { label: 'kitty', display_name: 'kitty kitty kitty cat where are you' }, + ], + }, + to: { + ...base.to, + watchlist_names: [ { label: 'to #1', display_name: 'to utka' } ], + } as Transaction['to'], +}; + +export const withContractCreation: Transaction = { + ...base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3195', + to: null, + created_contract: { + hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a', + implementations: null, + is_contract: true, + is_verified: null, + name: 'Shavuha token', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + transaction_types: [ + 'contract_creation', + ], +}; + +export const withTokenTransfer: Transaction = { + ...base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3196', + to: { + hash: addressMock.hash, + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [ privateTag ], + public_tags: [], + watchlist_names: [ watchlistName ], + ens_domain_name: null, + }, + token_transfers: [ + tokenTransferMock.erc20, + tokenTransferMock.erc721, + tokenTransferMock.erc1155A, + tokenTransferMock.erc1155B, + tokenTransferMock.erc1155C, + tokenTransferMock.erc1155D, + tokenTransferMock.erc404A, + tokenTransferMock.erc404B, + ], + token_transfers_overflow: true, + transaction_types: [ + 'token_transfer', + ], +}; + +export const withDecodedRevertReason: Transaction = { + ...base, + status: 'error', + result: 'Reverted', + revert_reason: { + method_call: 'SomeCustomError(address addr, uint256 balance)', + method_id: '50289a9f', + parameters: [ + { + name: 'addr', + type: 'address', + value: '0xf26594f585de4eb0ae9de865d9053fee02ac6ef1', + }, + { + name: 'balance', + type: 'uint256', + value: '123', + }, + ], + }, +}; + +export const withRawRevertReason: Transaction = { + ...base, + status: 'error', + result: 'Reverted', + revert_reason: { + raw: '4f6e6c79206368616972706572736f6e2063616e206769766520726967687420746f20766f74652e', + }, + to: { + hash: addressMock.hash, + implementations: null, + is_verified: true, + is_contract: true, + name: 'Bad guy', + private_tags: [ ], + public_tags: [], + watchlist_names: [ ], + ens_domain_name: null, + }, +}; + +export const pending: Transaction = { + ...base, + base_fee_per_gas: null, + block_number: null, + confirmation_duration: [], + confirmations: 0, + decoded_input: null, + gas_used: null, + max_fee_per_gas: null, + max_priority_fee_per_gas: null, + method: null, + position: null, + priority_fee: null, + result: 'pending', + revert_reason: null, + status: null, + timestamp: null, + transaction_burnt_fee: null, + transaction_tag: null, + type: null, + value: '0', +}; + +export const withActionsUniswap: Transaction = { + ...base, + actions: [ + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '7143.488560357232097378', + amount1: '10', + symbol0: 'Ring ding ding daa baa Baa aramba baa bom baa barooumba Wh-wha-what's going on-on? Ding, ding This is the Crazy Frog Ding, ding Bem', + symbol1: 'Ether', + }, + protocol: 'uniswap_v3', + type: 'mint', + }, + { + data: { + address: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88', + ids: [ + '53699', + '53700123456', + '42', + ], + name: 'Uniswap V3: Positions NFT', + symbol: 'UNI-V3-POS', + to: '0x6d872Fb5F5B2B1f71fA9AadE159bc3976c1946B7', + }, + protocol: 'uniswap_v3', + type: 'mint_nft', + }, + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '42876.488560357232', + amount1: '345.908098203434', + symbol0: 'SHAVUHA', + symbol1: 'BOB', + }, + protocol: 'uniswap_v3', + type: 'swap', + }, + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '42', + amount1: '0.523523223232', + symbol0: 'VIC', + symbol1: 'USDT', + }, + protocol: 'uniswap_v3', + type: 'burn', + }, + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '42', + amount1: '0.523523223232', + symbol0: 'BOB', + symbol1: 'UNI', + }, + protocol: 'uniswap_v3', + type: 'collect', + }, + ], +}; + +export const l2tx: Transaction = { + ...base, + l1_gas_price: '82702201886', + l1_fee_scalar: '1.0', + l1_gas_used: '17060', + l1_fee: '1584574188135760', +}; + +export const stabilityTx: Transaction = { + ...base, + stability_fee: { + dapp_address: { + hash: '0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + dapp_fee: '34381250000000', + token: { + address_hash: '0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43', + circulating_market_cap: null, + decimals: '18', + exchange_rate: '123.567', + holders_count: '92', + icon_url: 'https://example.com/icon.png', + name: 'Stability Gas', + symbol: 'GAS', + total_supply: '10000000000000000000000000', + type: 'ERC-20', + }, + total_fee: '68762500000000', + validator_address: { + hash: '0x1432997a4058acbBe562F3c1E79738c142039044', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + validator_fee: '34381250000000', + }, +}; + +export const celoTxn: Transaction = { + ...base, + celo: { + gas_token: { + address_hash: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', + circulating_market_cap: null, + decimals: '18', + exchange_rate: '0.42', + holders_count: '205738', + icon_url: 'https://example.com/icon.png', + name: 'Celo Dollar', + symbol: 'cUSD', + total_supply: '7145754483836626799435133', + type: 'ERC-20', + }, + }, +}; + +export const arbitrumTxn: Transaction = { + ...base, + arbitrum: { + batch_number: 743991, + commitment_transaction: { + hash: '0x71a25e01dde129a308704de217d200ea42e0f5b8c221c8ba8b2b680ff347f708', + status: 'unfinalized', + timestamp: '2024-11-19T14:26:23.000000Z', + }, + confirmation_transaction: { + hash: null, + status: null, + timestamp: null, + }, + contains_message: null, + gas_used_for_l1: '129773', + gas_used_for_l2: '128313', + message_related_info: { + associated_l1_transaction_hash: null, + message_status: 'Relayed', + }, + network_fee: '1283130000000', + poster_fee: '1297730000000', + status: 'Sent to base', + }, +}; + +export const base2 = { + ...base, + hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + from: { + ...base.from, + hash: addressMock.hash, + }, +}; + +export const base3 = { + ...base, + hash: '0x12d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + from: { + ...base.from, + hash: addressMock.hash, + }, +}; + +export const base4 = { + ...base, + hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', +}; + +export const withBlob = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '131072', + blob_versioned_hashes: [ + '0x01a8c328b0370068aaaef49c107f70901cd79adcda81e3599a88855532122e09', + '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd1', + ], + burnt_blob_fee: '2820464441688064', + max_fee_per_blob_gas: '60000000000', + transaction_types: [ 'blob_transaction' as const ], + type: 3, +}; + +export const withRecipientName = { + ...base, + to: addressMock.withName, +}; + +export const withRecipientEns = { + ...base, + to: addressMock.withEns, +}; + +export const withRecipientNameTag = { + ...withRecipientEns, + to: addressMock.withNameTag, +}; + +export const withRecipientContract = { + ...withRecipientEns, + to: addressMock.contract, +}; + +export const withInteropInMessage: Transaction = { + ...base, + op_interop: { + init_chain: interopMock.chain, + nonce: 1, + payload: '0x', + init_transaction_hash: '0x01a8c328b0370068aaaef49c107f70901cd79adcda81e3599a88855532122e09', + sender: addressMock.hash, + status: 'Sent', + target: addressMock.hash, + }, +}; + +export const withInteropOutMessage: Transaction = { + ...base, + op_interop: { + relay_chain: interopMock.chain, + nonce: 1, + payload: '0xfa4b78b90000000000000000000000000000000000000000000000000000000005001bcfe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791000000000000000000000000a79b29ad7e0196c95b87f4663ded82fbf2e3add8', + relay_transaction_hash: '0x01a8c328b0370068aaaef49c107f70901cd79adcda81e3599a88855532122e09', + sender: addressMock.hash, + status: 'Sent', + target: addressMock.hash, + }, +}; diff --git a/explorer/frontend/mocks/txs/txInterpretation.ts b/explorer/frontend/mocks/txs/txInterpretation.ts new file mode 100644 index 000000000..df8de4528 --- /dev/null +++ b/explorer/frontend/mocks/txs/txInterpretation.ts @@ -0,0 +1,48 @@ +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; + +import { hash } from 'mocks/address/address'; + +export const txInterpretation: TxInterpretationResponse = { + data: { + summaries: [ { + summary_template: `{action_type} {amount} {token} to {to_address} on {timestamp}`, + summary_template_variables: { + action_type: { type: 'string', value: 'Transfer' }, + amount: { type: 'currency', value: '100' }, + token: { + type: 'token', + value: { + name: 'Duck', + type: 'ERC-20', + symbol: 'DUCK', + address_hash: '0x486a3c5f34cDc4EF133f248f1C81168D78da52e8', + holders_count: '1152', + decimals: '18', + icon_url: null, + total_supply: '210000000000000000000000000', + exchange_rate: null, + circulating_market_cap: null, + }, + }, + to_address: { + type: 'address', + value: { + hash: hash, + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + }, + timestamp: { + type: 'timestamp', + value: '1687005431', + }, + }, + } ], + }, +}; diff --git a/explorer/frontend/mocks/user/profile.ts b/explorer/frontend/mocks/user/profile.ts new file mode 100644 index 000000000..7794447c2 --- /dev/null +++ b/explorer/frontend/mocks/user/profile.ts @@ -0,0 +1,25 @@ +import type { UserInfo } from 'types/api/account'; + +export const base: UserInfo = { + avatar: 'https://avatars.githubusercontent.com/u/22130104', + email: 'tom@ohhhh.me', + name: 'tom goriunov', + nickname: 'tom2drum', + address_hash: null, +}; + +export const withoutEmail: UserInfo = { + avatar: 'https://avatars.githubusercontent.com/u/22130104', + email: null, + name: 'tom goriunov', + nickname: 'tom2drum', + address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', +}; + +export const withEmailAndWallet: UserInfo = { + avatar: 'https://avatars.githubusercontent.com/u/22130104', + email: 'tom@ohhhh.me', + name: 'tom goriunov', + nickname: 'tom2drum', + address_hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', +}; diff --git a/explorer/frontend/mocks/userOps/userOp.ts b/explorer/frontend/mocks/userOps/userOp.ts new file mode 100644 index 000000000..d13401b59 --- /dev/null +++ b/explorer/frontend/mocks/userOps/userOp.ts @@ -0,0 +1,111 @@ +/* eslint-disable max-len */ +import type { UserOp } from 'types/api/userOps'; + +export const userOpData: UserOp = { + timestamp: '2024-01-19T12:42:12.000000Z', + transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a', + user_logs_start_index: 40, + fee: '187125856691380', + call_gas_limit: '26624', + gas: '258875', + status: true, + aggregator_signature: null, + block_hash: '0xff5f41ec89e5fb3dfcf103bbbd67469fed491a7dd7cffdf00bd9e3bf45d8aeab', + pre_verification_gas: '48396', + factory: null, + signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b', + verification_gas_limit: '61285', + max_fee_per_gas: '1575000898', + aggregator: null, + hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83', + gas_price: '1575000898', + user_logs_count: 1, + block_number: '10399597', + gas_used: '118810', + sender: { + ens_domain_name: null, + hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + nonce: '0x000000000000000000000000000000000000000000000000000000000000004f', + entry_point: { + ens_domain_name: null, + hash: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + sponsor_type: 'paymaster_sponsor', + raw: { + + call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', + call_gas_limit: '26624', + init_code: '0x', + max_fee_per_gas: '1575000898', + max_priority_fee_per_gas: '1575000898', + nonce: '79', + paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b', + pre_verification_gas: '48396', + sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b', + verification_gas_limit: '61285', + }, + max_priority_fee_per_gas: '1575000898', + revert_reason: null, + bundler: { + ens_domain_name: null, + hash: '0xd53Eb5203e367BbDD4f72338938224881Fc501Ab', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + }, + call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', + execute_call_data: '0x3cf80e6c', + execute_target: { + ens_domain_name: null, + hash: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + implementations: null, + is_contract: true, + is_verified: true, + name: 'FiatTokenProxy', + }, + decoded_call_data: { + method_call: 'execute(address dest, uint256 value, bytes func)', + method_id: 'b61d27f6', + parameters: [ + { + name: 'dest', + type: 'address', + value: '0xb0ccffd05f5a87c4c3ceffaa217900422a249915', + }, + { + name: 'value', + type: 'uint256', + value: '0', + }, + { + name: 'func', + type: 'bytes', + value: '0x3cf80e6c', + }, + ], + }, + decoded_execute_call_data: { + method_call: 'advanceEpoch()', + method_id: '3cf80e6c', + parameters: [], + }, + paymaster: { + ens_domain_name: null, + hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, +}; diff --git a/explorer/frontend/mocks/userOps/userOps.ts b/explorer/frontend/mocks/userOps/userOps.ts new file mode 100644 index 000000000..9a44e6259 --- /dev/null +++ b/explorer/frontend/mocks/userOps/userOps.ts @@ -0,0 +1,58 @@ +import type { UserOpsResponse } from 'types/api/userOps'; + +export const userOpsData: UserOpsResponse = { + items: [ + { + address: { + ens_domain_name: null, + hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399597', + fee: '187125856691380', + hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83', + status: true, + timestamp: '2022-01-19T12:42:12.000000Z', + transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a', + }, + { + address: + { ens_domain_name: null, + hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399596', + fee: '381895502291373', + hash: '0xcb945ae86608bdc88c3318245403c81a880fcb1e49fef18ac59477761c056cea', + status: false, + timestamp: '2022-01-19T12:42:00.000000Z', + transaction_hash: '0x558d699e7cbc235461d48ed04b8c3892d598a4000f20851760d00dc3513c2e48', + }, + { + address: { + ens_domain_name: null, + hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399560', + fee: '165019501210143', + hash: '0x84c1270b12af3f0ffa204071f1bf503ebf9b1ccf6310680383be5a2b6fd1d8e5', + status: true, + timestamp: '2022-01-19T12:32:00.000000Z', + transaction_hash: '0xc4c1c38680ec63139411aa2193275e8de44be15217c4256db9473bf0ea2b6264', + }, + ], + next_page_params: { + page_size: 50, + page_token: '10396582,0x9bf4d2a28813c5c244884cb20cdfe01dabb3f927234ae961eab6e38502de7a28', + }, +}; diff --git a/explorer/frontend/mocks/validators/blackfort.ts b/explorer/frontend/mocks/validators/blackfort.ts new file mode 100644 index 000000000..22f396c23 --- /dev/null +++ b/explorer/frontend/mocks/validators/blackfort.ts @@ -0,0 +1,41 @@ +import type { + ValidatorBlackfort, + ValidatorsBlackfortCountersResponse, + ValidatorsBlackfortResponse, +} from 'types/api/validators'; + +import * as addressMock from '../address/address'; + +export const validator1: ValidatorBlackfort = { + address: addressMock.withName, + name: 'testnet-3', + commission: 10, + delegated_amount: '0', + self_bonded_amount: '10000', +}; + +export const validator2: ValidatorBlackfort = { + address: addressMock.withEns, + name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG', + commission: 5000, + delegated_amount: '10000', + self_bonded_amount: '100', +}; + +export const validator3: ValidatorBlackfort = { + address: addressMock.withoutName, + name: 'testnet-1', + commission: 0, + delegated_amount: '0', + self_bonded_amount: '10000', +}; + +export const validatorsResponse: ValidatorsBlackfortResponse = { + items: [ validator1, validator2, validator3 ], + next_page_params: null, +}; + +export const validatorsCountersResponse: ValidatorsBlackfortCountersResponse = { + new_validators_count_24h: '11', + validators_count: '140', +}; diff --git a/explorer/frontend/mocks/validators/stability.ts b/explorer/frontend/mocks/validators/stability.ts new file mode 100644 index 000000000..655cb3784 --- /dev/null +++ b/explorer/frontend/mocks/validators/stability.ts @@ -0,0 +1,37 @@ +import type { + ValidatorStability, + ValidatorsStabilityCountersResponse, + ValidatorsStabilityResponse, +} from 'types/api/validators'; + +import * as addressMock from '../address/address'; + +export const validator1: ValidatorStability = { + address: addressMock.withName, + blocks_validated_count: 7334224, + state: 'active', +}; + +export const validator2: ValidatorStability = { + address: addressMock.withEns, + blocks_validated_count: 8937453, + state: 'probation', +}; + +export const validator3: ValidatorStability = { + address: addressMock.withoutName, + blocks_validated_count: 1234, + state: 'inactive', +}; + +export const validatorsResponse: ValidatorsStabilityResponse = { + items: [ validator1, validator2, validator3 ], + next_page_params: null, +}; + +export const validatorsCountersResponse: ValidatorsStabilityCountersResponse = { + active_validators_count: '42', + active_validators_percentage: 7.14, + new_validators_count_24h: '11', + validators_count: '140', +}; diff --git a/explorer/frontend/mocks/validators/zilliqa.ts b/explorer/frontend/mocks/validators/zilliqa.ts new file mode 100644 index 000000000..4f7a434a9 --- /dev/null +++ b/explorer/frontend/mocks/validators/zilliqa.ts @@ -0,0 +1,60 @@ +import type { ValidatorZilliqa, ValidatorsZilliqaItem, ValidatorsZilliqaResponse } from 'types/api/validators'; + +export const validator1: ValidatorsZilliqaItem = { + index: 420, + bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100', + balance: '1000000000000000000', +}; + +export const validatorsResponse: ValidatorsZilliqaResponse = { + items: [ validator1 ], + next_page_params: null, +}; + +export const validatorDetails: ValidatorZilliqa = { + added_at_block_number: 7527600, + balance: '20000000000000000000000000', + bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100', + control_address: { + ens_domain_name: null, + hash: '0xB4492C468Fe97CB73Ea70a9A712cdd5B5aB621c3', + implementations: [], + is_contract: false, + is_verified: null, + metadata: null, + name: null, + private_tags: [], + proxy_type: null, + public_tags: [], + watchlist_names: [], + }, + index: 1, + peer_id: '0x002408011220a8ce8c9a146f3dc411cd72ba845b76722824c55824ac74b3362f070a332d85f2', + reward_address: { + ens_domain_name: null, + hash: '0x0000000000000000000000000000000000000000', + implementations: [], + is_contract: false, + is_verified: null, + metadata: null, + name: null, + private_tags: [], + proxy_type: null, + public_tags: [], + watchlist_names: [], + }, + signing_address: { + ens_domain_name: null, + hash: '0x0000000000000000000000000000000000000026', + implementations: [], + is_contract: false, + is_verified: null, + metadata: null, + name: null, + private_tags: [], + proxy_type: null, + public_tags: [], + watchlist_names: [], + }, + stake_updated_at_block_number: 7527642, +}; diff --git a/explorer/frontend/mocks/withdrawals/withdrawals.ts b/explorer/frontend/mocks/withdrawals/withdrawals.ts new file mode 100644 index 000000000..97742fe3d --- /dev/null +++ b/explorer/frontend/mocks/withdrawals/withdrawals.ts @@ -0,0 +1,53 @@ +import type { AddressParam } from 'types/api/addressParams'; +import type { WithdrawalsResponse } from 'types/api/withdrawals'; + +export const data: WithdrawalsResponse = { + items: [ + { + amount: '192175000000000', + block_number: 43242, + index: 11688, + receiver: { + hash: '0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + } as AddressParam, + timestamp: '2022-06-07T18:12:24.000000Z', + validator_index: 49622, + }, + { + amount: '192175000000000', + block_number: 43242, + index: 11687, + receiver: { + hash: '0xf97e987c050e5Ab072211Ad2C213Eb5AEE4DF134', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + } as AddressParam, + timestamp: '2022-05-07T18:12:24.000000Z', + validator_index: 49621, + }, + { + amount: '182773000000000', + block_number: 43242, + index: 11686, + receiver: { + hash: '0xf97e123c050e5Ab072211Ad2C213Eb5AEE4DF134', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + } as AddressParam, + timestamp: '2022-04-07T18:12:24.000000Z', + validator_index: 49620, + }, + ], + next_page_params: { + index: 11639, + items_count: 50, + }, +}; diff --git a/explorer/frontend/mocks/zkEvm/deposits.ts b/explorer/frontend/mocks/zkEvm/deposits.ts new file mode 100644 index 000000000..91ecc077c --- /dev/null +++ b/explorer/frontend/mocks/zkEvm/deposits.ts @@ -0,0 +1,28 @@ +import type { ZkEvmL2DepositsResponse } from 'types/api/zkEvmL2'; + +export const baseResponse: ZkEvmL2DepositsResponse = { + items: [ + { + block_number: 19681943, + index: 182177, + l1_transaction_hash: '0x29074452f976064aca1ca5c6e7c82d890c10454280693e6eca0257ae000c8e85', + l2_transaction_hash: null, + symbol: 'DAI', + timestamp: '2022-04-18T11:08:11.000000Z', + value: '0.003', + }, + { + block_number: 19681894, + index: 182176, + l1_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + l2_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + symbol: 'ETH', + timestamp: '2022-04-18T10:58:23.000000Z', + value: '0.0046651390188845', + }, + ], + next_page_params: { + items_count: 50, + index: 1, + }, +}; diff --git a/explorer/frontend/mocks/zkEvm/txnBatches.ts b/explorer/frontend/mocks/zkEvm/txnBatches.ts new file mode 100644 index 000000000..d7e73dd77 --- /dev/null +++ b/explorer/frontend/mocks/zkEvm/txnBatches.ts @@ -0,0 +1,40 @@ +import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2'; + +export const txnBatchData: ZkEvmL2TxnBatch = { + acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3', + global_exit_root: '0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5', + number: 5, + sequence_transaction_hash: '0x7ae010e9758441b302db10282807358af460f38c49c618d26a897592f64977f7', + state_root: '0x183b4a38a4a6027947ceb93b323cc94c548c8c05cf605d73ca88351d77cae1a3', + status: 'Finalized', + timestamp: '2023-10-20T10:08:18.000000Z', + transactions: [ + '0xb5d432c270057c223b973f3b5f00dbad32823d9ef26f3e8d97c819c7c573453a', + ], + verify_transaction_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f', +}; + +export const txnBatchesData: ZkEvmL2TxnBatchesResponse = { + items: [ + { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Finalized', + verify_transaction_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8', + sequence_transaction_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b', + number: 5218590, + transactions_count: 9, + }, + { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Unfinalized', + verify_transaction_hash: null, + sequence_transaction_hash: null, + number: 5218591, + transactions_count: 9, + }, + ], + next_page_params: { + number: 5902834, + items_count: 50, + }, +}; diff --git a/explorer/frontend/mocks/zkEvm/withdrawals.ts b/explorer/frontend/mocks/zkEvm/withdrawals.ts new file mode 100644 index 000000000..c89635f4e --- /dev/null +++ b/explorer/frontend/mocks/zkEvm/withdrawals.ts @@ -0,0 +1,28 @@ +import type { ZkEvmL2WithdrawalsResponse } from 'types/api/zkEvmL2'; + +export const baseResponse: ZkEvmL2WithdrawalsResponse = { + items: [ + { + block_number: 11722417, + index: 47040, + l1_transaction_hash: null, + l2_transaction_hash: '0x68c378e412e51553524545ef1d3f00f69496fb37827c0b3b7e0870d245970408', + symbol: 'ETH', + timestamp: '2022-04-18T09:20:37.000000Z', + value: '0.025', + }, + { + block_number: 11722480, + index: 47041, + l1_transaction_hash: '0xbf76feb85b8b8f24dacb17f962dd359f82efc512928d7b11ffca92fb812ad6a5', + l2_transaction_hash: '0xfe3c168ac1751b8399f1e819f1d83ee4cf764128bc604d454abee29114dabf49', + symbol: 'ETH', + timestamp: '2022-04-18T09:23:45.000000Z', + value: '4', + }, + ], + next_page_params: { + items_count: 50, + index: 1, + }, +}; diff --git a/explorer/frontend/mocks/zkSync/zkSyncTxnBatch.ts b/explorer/frontend/mocks/zkSync/zkSyncTxnBatch.ts new file mode 100644 index 000000000..244337c68 --- /dev/null +++ b/explorer/frontend/mocks/zkSync/zkSyncTxnBatch.ts @@ -0,0 +1,20 @@ +import type { ZkSyncBatch } from 'types/api/zkSyncL2'; + +export const base: ZkSyncBatch = { + commit_transaction_hash: '0x7cd80c88977c2b310f79196b0b2136da18012be015ce80d0d9e9fe6cfad52b16', + commit_transaction_timestamp: '2022-03-19T09:37:38.726996Z', + end_block_number: 1245490, + execute_transaction_hash: '0x110b9a19afbabd5818a996ab2b493a9b23c888d73d95f1ab5272dbae503e103a', + execute_transaction_timestamp: '2022-03-19T10:29:05.358066Z', + l1_gas_price: '4173068062', + l1_transactions_count: 0, + l2_fair_gas_price: '100000000', + l2_transactions_count: 287, + number: 8051, + prove_transaction_hash: '0xb424162ba5afe17c710dceb5fc8d15d7d46a66223454dae8c74aa39f6802625b', + prove_transaction_timestamp: '2022-03-19T10:29:05.279179Z', + root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792', + start_block_number: 1245209, + status: 'Executed on L1', + timestamp: '2022-03-19T09:05:49.000000Z', +}; diff --git a/explorer/frontend/mocks/zkSync/zkSyncTxnBatches.ts b/explorer/frontend/mocks/zkSync/zkSyncTxnBatches.ts new file mode 100644 index 000000000..5f64c8821 --- /dev/null +++ b/explorer/frontend/mocks/zkSync/zkSyncTxnBatches.ts @@ -0,0 +1,49 @@ +import type { ZkSyncBatchesItem, ZkSyncBatchesResponse } from 'types/api/zkSyncL2'; + +export const sealed: ZkSyncBatchesItem = { + commit_transaction_hash: null, + commit_transaction_timestamp: null, + execute_transaction_hash: null, + execute_transaction_timestamp: null, + number: 8055, + prove_transaction_hash: null, + prove_transaction_timestamp: null, + status: 'Sealed on L2', + timestamp: '2022-03-19T12:53:36.000000Z', + transactions_count: 738, +}; + +export const sent: ZkSyncBatchesItem = { + commit_transaction_hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + commit_transaction_timestamp: '2022-03-19T13:09:07.357570Z', + execute_transaction_hash: null, + execute_transaction_timestamp: null, + number: 8054, + prove_transaction_hash: null, + prove_transaction_timestamp: null, + status: 'Sent to L1', + timestamp: '2022-03-19T11:36:45.000000Z', + transactions_count: 766, +}; + +export const executed: ZkSyncBatchesItem = { + commit_transaction_hash: '0xa2628f477e1027ac1c60fa75c186b914647769ac1cb9c7e1cab50b13506a0035', + commit_transaction_timestamp: '2022-03-19T11:52:18.963659Z', + execute_transaction_hash: '0xb7bd6b2b17498c66d3f6e31ac3685133a81b7f728d4f6a6f42741daa257d0d68', + execute_transaction_timestamp: '2022-03-19T13:28:16.712656Z', + number: 8053, + prove_transaction_hash: '0x9d44f2b775bd771f8a53205755b3897929aa672d2cd419b3b988c16d41d4f21e', + prove_transaction_timestamp: '2022-03-19T13:28:16.603104Z', + status: 'Executed on L1', + timestamp: '2022-03-19T10:01:52.000000Z', + transactions_count: 1071, +}; + +export const baseResponse: ZkSyncBatchesResponse = { + items: [ + sealed, + sent, + executed, + ], + next_page_params: null, +}; diff --git a/explorer/frontend/next-env.d.ts b/explorer/frontend/next-env.d.ts new file mode 100644 index 000000000..52e831b43 --- /dev/null +++ b/explorer/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/explorer/frontend/next.config.js b/explorer/frontend/next.config.js new file mode 100644 index 000000000..cb22f0668 --- /dev/null +++ b/explorer/frontend/next.config.js @@ -0,0 +1,51 @@ +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.BUNDLE_ANALYZER === 'true', +}); + +const withRoutes = require('nextjs-routes/config')({ + outDir: 'nextjs', +}); + +const headers = require('./nextjs/headers'); +const redirects = require('./nextjs/redirects'); +const rewrites = require('./nextjs/rewrites'); + +/** @type {import('next').NextConfig} */ +const moduleExports = { + transpilePackages: [ + 'react-syntax-highlighter', + 'swagger-client', + 'swagger-ui-react', + ], + reactStrictMode: true, + webpack(config) { + config.module.rules.push( + { + test: /\.svg$/, + use: [ '@svgr/webpack' ], + }, + ); + config.resolve.fallback = { fs: false, net: false, tls: false }; + config.externals.push('pino-pretty', 'lokijs', 'encoding'); + + return config; + }, + // NOTE: all config functions should be static and not depend on any environment variables + // since all variables will be passed to the app only at runtime and there is now way to change Next.js config at this time + // if you are stuck and strongly believe what you need some sort of flexibility here please fill free to join the discussion + // https://github.com/blockscout/frontend/discussions/167 + rewrites, + redirects, + headers, + output: 'standalone', + productionBrowserSourceMaps: true, + serverExternalPackages: ["@opentelemetry/sdk-node", "@opentelemetry/auto-instrumentations-node"], + experimental: { + staleTimes: { + dynamic: 30, + 'static': 180, + }, + }, +}; + +module.exports = withBundleAnalyzer(withRoutes(moduleExports)); diff --git a/explorer/frontend/nextjs/PageMetadata.tsx b/explorer/frontend/nextjs/PageMetadata.tsx new file mode 100644 index 000000000..237c7b98c --- /dev/null +++ b/explorer/frontend/nextjs/PageMetadata.tsx @@ -0,0 +1,41 @@ +import Head from 'next/head'; +import React from 'react'; + +import type { Route } from 'nextjs-routes'; +import type { Props as PageProps } from 'nextjs/getServerSideProps'; + +import config from 'configs/app'; +import * as metadata from 'lib/metadata'; + +interface Props { + pathname: Pathname; + query?: PageProps['query']; + apiData?: PageProps['apiData']; +} + +const PageMetadata = (props: Props) => { + const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData); + + return ( + + { title } + + { canonical && } + + { /* OG TAGS */ } + + { opengraph.description && } + + + + { /* Twitter Meta Tags */ } + + + + { opengraph.description && } + + + ); +}; + +export default PageMetadata; diff --git a/explorer/frontend/nextjs/PageNextJs.tsx b/explorer/frontend/nextjs/PageNextJs.tsx new file mode 100644 index 000000000..d5cd16af5 --- /dev/null +++ b/explorer/frontend/nextjs/PageNextJs.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import type { Route } from 'nextjs-routes'; +import type { Props as PageProps } from 'nextjs/getServerSideProps'; +import PageMetadata from 'nextjs/PageMetadata'; + +import useAdblockDetect from 'lib/hooks/useAdblockDetect'; +import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; +import * as mixpanel from 'lib/mixpanel'; + +interface Props { + pathname: Pathname; + children: React.ReactNode; + query?: PageProps['query']; + apiData?: PageProps['apiData']; +} + +const PageNextJs = (props: Props) => { + const isMounted = useIsMounted(); + + useGetCsrfToken(); + useAdblockDetect(); + useNotifyOnNavigation(); + + const isMixpanelInited = mixpanel.useInit(); + mixpanel.useLogPageView(isMixpanelInited); + + return ( + <> + + { isMounted ? props.children : null } + + ); +}; + +export default React.memo(PageNextJs); diff --git a/explorer/frontend/nextjs/csp/generateCspPolicy.ts b/explorer/frontend/nextjs/csp/generateCspPolicy.ts new file mode 100644 index 000000000..891a03af5 --- /dev/null +++ b/explorer/frontend/nextjs/csp/generateCspPolicy.ts @@ -0,0 +1,27 @@ +import * as descriptors from './policies'; +import { makePolicyString, mergeDescriptors } from './utils'; + +function generateCspPolicy() { + const policyDescriptor = mergeDescriptors( + descriptors.app(), + descriptors.ad(), + descriptors.cloudFlare(), + descriptors.gasHawk(), + descriptors.googleAnalytics(), + descriptors.googleFonts(), + descriptors.googleReCaptcha(), + descriptors.growthBook(), + descriptors.helia(), + descriptors.marketplace(), + descriptors.mixpanel(), + descriptors.monaco(), + descriptors.rollbar(), + descriptors.safe(), + descriptors.usernameApi(), + descriptors.walletConnect(), + ); + + return makePolicyString(policyDescriptor); +} + +export default generateCspPolicy; diff --git a/explorer/frontend/nextjs/csp/policies/ad.ts b/explorer/frontend/nextjs/csp/policies/ad.ts new file mode 100644 index 000000000..d90cf1b21 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/ad.ts @@ -0,0 +1,61 @@ +import Base64 from 'crypto-js/enc-base64'; +import sha256 from 'crypto-js/sha256'; +import type CspDev from 'csp-dev'; + +import { connectAdbutler, placeAd } from 'ui/shared/ad/adbutlerScript'; +import { hypeInit } from 'ui/shared/ad/hypeBannerScript'; + +export function ad(): CspDev.DirectiveDescriptor { + return { + 'connect-src': [ + // coinzilla + 'coinzilla.com', + '*.coinzilla.com', + 'https://request-global.czilladx.com', + + // adbutler + 'servedbyadbutler.com', + + // slise + '*.slise.xyz', + + // hype + 'api.hypelab.com', + '*.ixncdn.com', + '*.cloudfront.net', + ], + 'frame-src': [ + // coinzilla + 'https://request-global.czilladx.com', + ], + 'script-src': [ + // coinzilla + 'coinzillatag.com', + + // adbutler + 'servedbyadbutler.com', + `'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`, + `'sha256-${ Base64.stringify(sha256(placeAd(undefined) ?? '')) }'`, + `'sha256-${ Base64.stringify(sha256(placeAd('mobile') ?? '')) }'`, + + // slise + '*.slise.xyz', + + //hype + `'sha256-${ Base64.stringify(sha256(hypeInit ?? '')) }'`, + 'https://api.hypelab.com', + 'd1q98dzwj6s2rb.cloudfront.net', + ], + 'img-src': [ + // coinzilla + 'cdn.coinzilla.io', + + // adbutler + 'servedbyadbutler.com', + ], + 'font-src': [ + // coinzilla + 'https://request-global.czilladx.com', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/app.ts b/explorer/frontend/nextjs/csp/policies/app.ts new file mode 100644 index 000000000..3654fb331 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/app.ts @@ -0,0 +1,9 @@ +import type CspDev from 'csp-dev'; + +export function app(): CspDev.DirectiveDescriptor { + return { + 'default-src': [ + '*', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/cloudFlare.ts b/explorer/frontend/nextjs/csp/policies/cloudFlare.ts new file mode 100644 index 000000000..e86085042 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/cloudFlare.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import { KEY_WORDS } from '../utils'; + +// CloudFlare analytics +export function cloudFlare(): CspDev.DirectiveDescriptor { + return { + 'script-src': [ + 'static.cloudflareinsights.com', + ], + 'style-src': [ + KEY_WORDS.DATA, + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/gasHawk.ts b/explorer/frontend/nextjs/csp/policies/gasHawk.ts new file mode 100644 index 000000000..6a1a8e624 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/gasHawk.ts @@ -0,0 +1,30 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +const feature = config.features.saveOnGas; + +export function gasHawk(): CspDev.DirectiveDescriptor { + if (!feature.isEnabled) { + return {}; + } + + const apiOrigin = (() => { + try { + const url = new URL(feature.apiUrlTemplate); + return url.origin; + } catch (error) { + return ''; + } + })(); + + if (!apiOrigin) { + return {}; + } + + return { + 'connect-src': [ + apiOrigin, + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/googleAnalytics.ts b/explorer/frontend/nextjs/csp/policies/googleAnalytics.ts new file mode 100644 index 000000000..a6d3f7b84 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/googleAnalytics.ts @@ -0,0 +1,29 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function googleAnalytics(): CspDev.DirectiveDescriptor { + if (!config.features.googleAnalytics.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.google-analytics.com', + '*.analytics.google.com', + 'https://www.googletagmanager.com', + 'https://stats.g.doubleclick.net', + ], + 'script-src': [ + // inline script hash, see ui/shared/GoogleAnalytics.tsx + '\'sha256-WXRwCtfSfMoCPzPUIOUAosSaADdGgct0/Lhmnbm7MCA=\'', + 'https://www.googletagmanager.com', + '*.google-analytics.com', + '*.analytics.google.com', + ], + 'img-src': [ + '*.google-analytics.com', + '*.analytics.google.com', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/googleFonts.ts b/explorer/frontend/nextjs/csp/policies/googleFonts.ts new file mode 100644 index 000000000..f79b0d523 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/googleFonts.ts @@ -0,0 +1,18 @@ +import type CspDev from 'csp-dev'; + +export function googleFonts(): CspDev.DirectiveDescriptor { + // we use Inter and Poppins in the app + + return { + 'connect-src': [ + 'fonts.gstatic.com', + ], + 'style-src': [ + 'fonts.googleapis.com', + ], + 'font-src': [ + 'fonts.gstatic.com', + 'fonts.googleapis.com', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/googleReCaptcha.ts b/explorer/frontend/nextjs/csp/policies/googleReCaptcha.ts new file mode 100644 index 000000000..4fb40df08 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/googleReCaptcha.ts @@ -0,0 +1,32 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function googleReCaptcha(): CspDev.DirectiveDescriptor { + if (!config.services.reCaptchaV2.siteKey) { + return {}; + } + + return { + 'connect-src': [ + 'https://www.google.com/recaptcha/api2/clr', + ], + 'script-src': [ + 'https://www.google.com/recaptcha/api.js', + 'https://www.gstatic.com', + 'https://translate.google.com', + '\'sha256-FDyPg8CqqIpPAfGVKx1YeKduyLs0ghNYWII21wL+7HM=\'', + ], + 'style-src': [ + 'https://www.gstatic.com', + ], + 'img-src': [ + 'https://translate.google.com', + 'https://www.gstatic.com', + ], + 'frame-src': [ + 'https://www.google.com/recaptcha/api2/anchor', + 'https://www.google.com/recaptcha/api2/bframe', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/growthBook.ts b/explorer/frontend/nextjs/csp/policies/growthBook.ts new file mode 100644 index 000000000..ae0f055fb --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/growthBook.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function growthBook(): CspDev.DirectiveDescriptor { + if (!config.features.growthBook.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + 'cdn.growthbook.io', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/helia.ts b/explorer/frontend/nextjs/csp/policies/helia.ts new file mode 100644 index 000000000..c6e925d37 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/helia.ts @@ -0,0 +1,16 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function helia(): CspDev.DirectiveDescriptor { + if (!config.UI.views.nft.verifiedFetch.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + 'https://delegated-ipfs.dev', + 'https://trustless-gateway.link', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/index.ts b/explorer/frontend/nextjs/csp/policies/index.ts new file mode 100644 index 000000000..387a58b2d --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/index.ts @@ -0,0 +1,16 @@ +export { ad } from './ad'; +export { app } from './app'; +export { cloudFlare } from './cloudFlare'; +export { gasHawk } from './gasHawk'; +export { googleAnalytics } from './googleAnalytics'; +export { googleFonts } from './googleFonts'; +export { googleReCaptcha } from './googleReCaptcha'; +export { growthBook } from './growthBook'; +export { helia } from './helia'; +export { marketplace } from './marketplace'; +export { mixpanel } from './mixpanel'; +export { monaco } from './monaco'; +export { rollbar } from './rollbar'; +export { safe } from './safe'; +export { usernameApi } from './usernameApi'; +export { walletConnect } from './walletConnect'; diff --git a/explorer/frontend/nextjs/csp/policies/marketplace.ts b/explorer/frontend/nextjs/csp/policies/marketplace.ts new file mode 100644 index 000000000..08474a4bc --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/marketplace.ts @@ -0,0 +1,22 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +const feature = config.features.marketplace; + +export function marketplace(): CspDev.DirectiveDescriptor { + if (!feature.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + 'api' in feature ? feature.api.endpoint : '', + feature.rating ? 'https://api.airtable.com' : '', + ], + + 'frame-src': [ + '*', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/mixpanel.ts b/explorer/frontend/nextjs/csp/policies/mixpanel.ts new file mode 100644 index 000000000..0e2d8fee3 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/mixpanel.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function mixpanel(): CspDev.DirectiveDescriptor { + if (!config.features.mixpanel.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.mixpanel.com', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/monaco.ts b/explorer/frontend/nextjs/csp/policies/monaco.ts new file mode 100644 index 000000000..64d6086d9 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/monaco.ts @@ -0,0 +1,30 @@ +import type CspDev from 'csp-dev'; + +import { KEY_WORDS } from '../utils'; + +export function monaco(): CspDev.DirectiveDescriptor { + return { + 'script-src': [ + KEY_WORDS.BLOB, + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/loader.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.nls.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/solidity/solidity.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/elixir/elixir.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/javascript/javascript.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/typescript/typescript.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/rust/rust.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/language/json/jsonMode.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/language/json/jsonWorker.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/language/typescript/tsMode.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/language/typescript/tsWorker.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/worker/workerMain.js', + ], + 'style-src': [ + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.css', + ], + 'font-src': [ + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/browser/ui/codicons/codicon/codicon.ttf', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/rollbar.ts b/explorer/frontend/nextjs/csp/policies/rollbar.ts new file mode 100644 index 000000000..87470735f --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/rollbar.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function rollbar(): CspDev.DirectiveDescriptor { + if (!config.features.rollbar.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + 'api.rollbar.com', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/safe.ts b/explorer/frontend/nextjs/csp/policies/safe.ts new file mode 100644 index 000000000..c7c9c05e2 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/safe.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function safe(): CspDev.DirectiveDescriptor { + if (!config.features.safe.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.safe.global', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/usernameApi.ts b/explorer/frontend/nextjs/csp/policies/usernameApi.ts new file mode 100644 index 000000000..4b2c2bd91 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/usernameApi.ts @@ -0,0 +1,26 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +const feature = config.features.addressProfileAPI; + +export function usernameApi(): CspDev.DirectiveDescriptor { + if (!feature.isEnabled) { + return {}; + } + + const apiOrigin = (() => { + try { + const url = new URL(feature.apiUrlTemplate); + return url.origin; + } catch (error) { + return ''; + } + })(); + + return { + 'connect-src': [ + apiOrigin, + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/policies/walletConnect.ts b/explorer/frontend/nextjs/csp/policies/walletConnect.ts new file mode 100644 index 000000000..728c7eab4 --- /dev/null +++ b/explorer/frontend/nextjs/csp/policies/walletConnect.ts @@ -0,0 +1,31 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +import { KEY_WORDS } from '../utils'; + +export function walletConnect(): CspDev.DirectiveDescriptor { + if (!config.features.blockchainInteraction.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.web3modal.com', + '*.web3modal.org', + '*.walletconnect.com', + '*.walletconnect.org', + 'wss://relay.walletconnect.com', + 'wss://relay.walletconnect.org', + 'wss://www.walletlink.org', + ], + 'frame-ancestors': [ + '*.walletconnect.org', + '*.walletconnect.com', + ], + 'img-src': [ + KEY_WORDS.BLOB, + '*.walletconnect.com', + ], + }; +} diff --git a/explorer/frontend/nextjs/csp/utils.ts b/explorer/frontend/nextjs/csp/utils.ts new file mode 100644 index 000000000..6c5dc5579 --- /dev/null +++ b/explorer/frontend/nextjs/csp/utils.ts @@ -0,0 +1,48 @@ +import type CspDev from 'csp-dev'; +import { uniq } from 'es-toolkit'; + +export const KEY_WORDS = { + BLOB: 'blob:', + DATA: 'data:', + NONE: '\'none\'', + REPORT_SAMPLE: `'report-sample'`, + SELF: '\'self\'', + STRICT_DYNAMIC: `'strict-dynamic'`, + UNSAFE_INLINE: '\'unsafe-inline\'', + UNSAFE_EVAL: '\'unsafe-eval\'', +}; + +export function mergeDescriptors(...descriptors: Array) { + return descriptors.reduce((result, item) => { + for (const _key in item) { + const key = _key as CspDev.Directive; + const value = item[key]; + + if (!value) { + continue; + } + + if (result[key]) { + result[key]?.push(...value); + } else { + result[key] = [ ...value ]; + } + } + + return result; + }, {} as CspDev.DirectiveDescriptor); +} + +export function makePolicyString(policyDescriptor: CspDev.DirectiveDescriptor) { + return Object.entries(policyDescriptor) + .map(([ key, value ]) => { + if (!value || value.length === 0) { + return; + } + + const uniqueValues = uniq(value); + return [ key, uniqueValues.join(' ') ].join(' '); + }) + .filter(Boolean) + .join(';'); +} diff --git a/explorer/frontend/nextjs/getServerSideProps.ts b/explorer/frontend/nextjs/getServerSideProps.ts new file mode 100644 index 000000000..e9ea5faab --- /dev/null +++ b/explorer/frontend/nextjs/getServerSideProps.ts @@ -0,0 +1,402 @@ +import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; + +import type { AdBannerProviders } from 'types/client/adProviders'; +import type { RollupType } from 'types/client/rollup'; + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; +const rollupFeature = config.features.rollup; +const adBannerFeature = config.features.adsBanner; +import isNeedProxy from 'lib/api/isNeedProxy'; +import * as cookies from 'lib/cookies'; +import type * as metadata from 'lib/metadata'; + +export interface Props { + query: Route['query']; + cookies: string; + referrer: string; + adBannerProvider: AdBannerProviders | null; + // if apiData is undefined, Next.js will complain that it is not serializable + // so we force it to be always present in the props but it can be null + apiData: metadata.ApiData | null; + uuid: string; +} + +export const base = async ({ req, res, query }: GetServerSidePropsContext): +Promise>> => { + const adBannerProvider = (() => { + if (adBannerFeature.isEnabled) { + if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) { + // we need to get a random ad provider on the server side to keep it consistent with the client side + const randomIndex = Math.round(Math.random()); + return [ adBannerFeature.provider, adBannerFeature.additionalProvider ][randomIndex]; + } else { + return adBannerFeature.provider; + } + } + return null; + })(); + + let uuid = cookies.getFromCookieString(req.headers.cookie || '', cookies.NAMES.UUID); + if (!uuid) { + uuid = crypto.randomUUID(); + res.setHeader('Set-Cookie', `${ cookies.NAMES.UUID }=${ uuid }`); + } + + const isTrackingDisabled = process.env.DISABLE_TRACKING === 'true'; + + if (!isTrackingDisabled) { + // log pageview + const hostname = req.headers.host; + const timestamp = new Date().toISOString(); + const chainId = process.env.NEXT_PUBLIC_NETWORK_ID; + const chainName = process.env.NEXT_PUBLIC_NETWORK_NAME; + const publicRPC = process.env.NEXT_PUBLIC_NETWORK_RPC_URL; + + fetch('https://monitor.blockscout.com/count', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + hostname, + timestamp, + chainId, + chainName, + publicRPC, + uuid, + }), + }); + } + + return { + props: { + query, + cookies: req.headers.cookie || '', + referrer: req.headers.referer || '', + adBannerProvider: adBannerProvider, + apiData: null, + uuid, + }, + }; +}; + +export const account: GetServerSideProps = async(context) => { + if (!config.features.account.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const verifiedAddresses: GetServerSideProps = async(context) => { + if (!config.features.addressVerification.isEnabled) { + return { + notFound: true, + }; + } + + return account(context); +}; + +const DEPOSITS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum', 'scroll' ]; +export const deposits: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && DEPOSITS_ROLLUP_TYPES.includes(rollupFeature.type))) { + return { + notFound: true, + }; + } + + return base(context); +}; + +const WITHDRAWALS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum', 'scroll' ]; +export const withdrawals: GetServerSideProps = async(context) => { + if ( + !config.features.beaconChain.isEnabled && + !(rollupFeature.isEnabled && WITHDRAWALS_ROLLUP_TYPES.includes(rollupFeature.type)) + ) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const txnWithdrawals: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && rollupFeature.type === 'arbitrum')) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const rollup: GetServerSideProps = async(context) => { + if (!config.features.rollup.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const outputRoots: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && rollupFeature.outputRootsEnabled)) { + return { + notFound: true, + }; + } + + return base(context); +}; + +const BATCH_ROLLUP_TYPES: Array = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic', 'scroll' ]; +export const batch: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const batchCelestia: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && (rollupFeature.type === 'arbitrum' || rollupFeature.type === 'optimistic'))) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const marketplace = async (context: GetServerSidePropsContext): +Promise>> => { + if (!config.features.marketplace.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const apiDocs: GetServerSideProps = async(context) => { + if (!config.features.restApiDocs.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const graphIQl: GetServerSideProps = async(context) => { + if (!config.features.graphqlApiDocs.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const csvExport: GetServerSideProps = async(context) => { + if (!config.features.csvExport.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const stats: GetServerSideProps = async(context) => { + if (!config.features.stats.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const suave: GetServerSideProps = async(context) => { + if (!config.features.suave.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const nameService: GetServerSideProps = async(context) => { + if (!config.features.nameService.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const accounts: GetServerSideProps = async(context) => { + if (config.UI.views.address.hiddenViews?.top_accounts) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const accountsLabelSearch: GetServerSideProps = async(context) => { + if (!config.features.addressMetadata.isEnabled || !context.query.tagType) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const userOps: GetServerSideProps = async(context) => { + if (!config.features.userOps.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const validators: GetServerSideProps = async(context) => { + if (!config.features.validators.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const validatorDetails: GetServerSideProps = async(context) => { + const feature = config.features.validators; + if (!feature.isEnabled || feature.chainType !== 'zilliqa') { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const gasTracker: GetServerSideProps = async(context) => { + if (!config.features.gasTracker.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const advancedFilter: GetServerSideProps = async(context) => { + if (!config.features.advancedFilter.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const dataAvailability: GetServerSideProps = async(context) => { + if (!config.features.dataAvailability.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const login: GetServerSideProps = async(context) => { + + if (!isNeedProxy()) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const dev: GetServerSideProps = async(context) => { + if (!config.app.isDev) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const publicTagsSubmit: GetServerSideProps = async(context) => { + + if (!config.features.publicTagsSubmission.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const disputeGames: GetServerSideProps = async(context) => { + if (!config.features.faultProofSystem.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const mud: GetServerSideProps = async(context) => { + if (!config.features.mudFramework.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const interopMessages: GetServerSideProps = async(context) => { + const rollupFeature = config.features.rollup; + if (!rollupFeature.isEnabled || !rollupFeature.interopEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const pools: GetServerSideProps = async(context) => { + if (!config.features.pools.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; diff --git a/explorer/frontend/nextjs/headers.js b/explorer/frontend/nextjs/headers.js new file mode 100644 index 000000000..c38f29487 --- /dev/null +++ b/explorer/frontend/nextjs/headers.js @@ -0,0 +1,36 @@ +async function headers() { + return [ + { + source: '/:path*', + headers: [ + // security headers from here - https://nextjs.org/docs/advanced-features/security-headers + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + { + key: 'Referrer-Policy', + value: 'origin-when-cross-origin', + }, + ], + }, + ]; +} + +module.exports = headers; diff --git a/explorer/frontend/nextjs/middlewares/account.ts b/explorer/frontend/nextjs/middlewares/account.ts new file mode 100644 index 000000000..7ab562542 --- /dev/null +++ b/explorer/frontend/nextjs/middlewares/account.ts @@ -0,0 +1,28 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +import config from 'configs/app'; +import * as cookies from 'lib/cookies'; + +export function account(req: NextRequest) { + const feature = config.features.account; + if (!feature.isEnabled) { + return; + } + + const apiTokenCookie = req.cookies.get(cookies.NAMES.API_TOKEN); + + // if user doesn't have api token cookie and he is trying to access account page + // do redirect to auth page + if (!apiTokenCookie) { + // we don't have any info from router here, so just do straight forward sub-string search (sorry) + const isAccountRoute = + req.nextUrl.pathname.includes('/account/') || + (req.nextUrl.pathname === '/txs' && req.nextUrl.searchParams.get('tab') === 'watchlist'); + const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile'); + + if ((isAccountRoute || isProfileRoute)) { + return NextResponse.redirect(config.app.baseUrl); + } + } +} diff --git a/explorer/frontend/nextjs/middlewares/addressFormat.ts b/explorer/frontend/nextjs/middlewares/addressFormat.ts new file mode 100644 index 000000000..bfd8bf14e --- /dev/null +++ b/explorer/frontend/nextjs/middlewares/addressFormat.ts @@ -0,0 +1,20 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import type { AddressFormat } from 'types/views/address'; + +import config from 'configs/app'; +import * as cookiesLib from 'lib/cookies'; + +export default function addressFormatMiddleware(req: NextRequest, res: NextResponse) { + const addressFormatCookie = req.cookies.get(cookiesLib.NAMES.ADDRESS_FORMAT); + const defaultFormat = config.UI.views.address.hashFormat.availableFormats[0]; + + if (addressFormatCookie) { + const isValidCookie = config.UI.views.address.hashFormat.availableFormats.includes(addressFormatCookie.value as AddressFormat); + if (!isValidCookie) { + res.cookies.set(cookiesLib.NAMES.ADDRESS_FORMAT, defaultFormat, { path: '/' }); + } + } else { + res.cookies.set(cookiesLib.NAMES.ADDRESS_FORMAT, defaultFormat, { path: '/' }); + } +} diff --git a/explorer/frontend/nextjs/middlewares/colorTheme.ts b/explorer/frontend/nextjs/middlewares/colorTheme.ts new file mode 100644 index 000000000..dcb8314ca --- /dev/null +++ b/explorer/frontend/nextjs/middlewares/colorTheme.ts @@ -0,0 +1,15 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import appConfig from 'configs/app'; +import * as cookiesLib from 'lib/cookies'; + +export default function colorThemeMiddleware(req: NextRequest, res: NextResponse) { + const colorModeCookie = req.cookies.get(cookiesLib.NAMES.COLOR_MODE); + + if (!colorModeCookie) { + if (appConfig.UI.colorTheme.default) { + res.cookies.set(cookiesLib.NAMES.COLOR_MODE, appConfig.UI.colorTheme.default.colorMode, { path: '/' }); + res.cookies.set(cookiesLib.NAMES.COLOR_MODE_HEX, appConfig.UI.colorTheme.default.hex, { path: '/' }); + } + } +} diff --git a/explorer/frontend/nextjs/middlewares/index.ts b/explorer/frontend/nextjs/middlewares/index.ts new file mode 100644 index 000000000..763e81a7f --- /dev/null +++ b/explorer/frontend/nextjs/middlewares/index.ts @@ -0,0 +1,4 @@ +export { account } from './account'; +export { default as colorTheme } from './colorTheme'; +export { default as addressFormat } from './addressFormat'; +export { default as scamTokens } from './scamTokens'; diff --git a/explorer/frontend/nextjs/middlewares/scamTokens.ts b/explorer/frontend/nextjs/middlewares/scamTokens.ts new file mode 100644 index 000000000..d28f2d6cd --- /dev/null +++ b/explorer/frontend/nextjs/middlewares/scamTokens.ts @@ -0,0 +1,14 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import config from 'configs/app'; +import * as cookiesLib from 'lib/cookies'; + +export default function scamTokensMiddleware(req: NextRequest, res: NextResponse) { + if (config.UI.views.token.hideScamTokensEnabled) { + const showScamTokensCookie = req.cookies.get(cookiesLib.NAMES.SHOW_SCAM_TOKENS); + + if (!showScamTokensCookie) { + res.cookies.set(cookiesLib.NAMES.SHOW_SCAM_TOKENS, 'false', { path: '/' }); + } + } +} diff --git a/explorer/frontend/nextjs/nextjs-routes.d.ts b/explorer/frontend/nextjs/nextjs-routes.d.ts new file mode 100644 index 000000000..c3c15a4ea --- /dev/null +++ b/explorer/frontend/nextjs/nextjs-routes.d.ts @@ -0,0 +1,200 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +// This file will be automatically regenerated when your Next.js server is running. +// nextjs-routes version: 1.0.8 +/* eslint-disable */ + +// prettier-ignore +declare module "nextjs-routes" { + export type Route = + | StaticRoute<"/404"> + | StaticRoute<"/account/api-key"> + | StaticRoute<"/account/custom-abi"> + | StaticRoute<"/account/merits"> + | StaticRoute<"/account/tag-address"> + | StaticRoute<"/account/verified-addresses"> + | StaticRoute<"/account/watchlist"> + | StaticRoute<"/accounts"> + | DynamicRoute<"/accounts/label/[slug]", { "slug": string }> + | DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }> + | DynamicRoute<"/address/[hash]", { "hash": string }> + | StaticRoute<"/advanced-filter"> + | StaticRoute<"/api/channels"> + | StaticRoute<"/api/config"> + | StaticRoute<"/api/csrf"> + | StaticRoute<"/api/healthz"> + | StaticRoute<"/api/latest-channels"> + | StaticRoute<"/api/latest-entries"> + | StaticRoute<"/api/latest-ledger-entries"> + | StaticRoute<"/api/ledger-entries"> + | StaticRoute<"/api/log"> + | StaticRoute<"/api/media-type"> + | StaticRoute<"/api/metrics"> + | StaticRoute<"/api/monitoring/invalid-api-schema"> + | StaticRoute<"/api/proxy"> + | StaticRoute<"/api/quick-search"> + | StaticRoute<"/api/test"> + | StaticRoute<"/api-docs"> + | DynamicRoute<"/apps/[id]", { "id": string }> + | StaticRoute<"/apps"> + | StaticRoute<"/auth/profile"> + | DynamicRoute<"/batches/[number]", { "number": string }> + | DynamicRoute<"/batches/celestia/[height]/[commitment]", { "height": string; "commitment": string }> + | StaticRoute<"/batches"> + | DynamicRoute<"/blobs/[hash]", { "hash": string }> + | DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }> + | DynamicRoute<"/block/countdown/[height]", { "height": string }> + | StaticRoute<"/block/countdown"> + | StaticRoute<"/blocks"> + | StaticRoute<"/chakra"> + | StaticRoute<"/channels"> + | StaticRoute<"/contract-verification"> + | StaticRoute<"/csv-export"> + | StaticRoute<"/deposits"> + | StaticRoute<"/dispute-games"> + | StaticRoute<"/gas-tracker"> + | StaticRoute<"/graphiql"> + | StaticRoute<"/"> + | StaticRoute<"/internal-txs"> + | StaticRoute<"/interop-messages"> + | StaticRoute<"/ledger"> + | StaticRoute<"/login"> + | StaticRoute<"/mud-worlds"> + | DynamicRoute<"/name-domains/[name]", { "name": string }> + | StaticRoute<"/name-domains"> + | DynamicRoute<"/op/[hash]", { "hash": string }> + | StaticRoute<"/ops"> + | StaticRoute<"/output-roots"> + | DynamicRoute<"/pools/[hash]", { "hash": string }> + | StaticRoute<"/pools"> + | StaticRoute<"/public-tags/submit"> + | StaticRoute<"/search-results"> + | StaticRoute<"/sprite"> + | DynamicRoute<"/stats/[id]", { "id": string }> + | StaticRoute<"/stats"> + | DynamicRoute<"/token/[hash]", { "hash": string }> + | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> + | StaticRoute<"/token-transfers"> + | StaticRoute<"/tokens"> + | DynamicRoute<"/tx/[hash]", { "hash": string }> + | StaticRoute<"/txn-withdrawals"> + | StaticRoute<"/txs"> + | DynamicRoute<"/txs/kettle/[hash]", { "hash": string }> + | DynamicRoute<"/validators/[id]", { "id": string }> + | StaticRoute<"/validators"> + | StaticRoute<"/verified-contracts"> + | StaticRoute<"/visualize/sol2uml"> + | StaticRoute<"/withdrawals">; + + interface StaticRoute { + pathname: Pathname; + query?: Query | undefined; + hash?: string | null | undefined; + } + + interface DynamicRoute { + pathname: Pathname; + query: Parameters & Query; + hash?: string | null | undefined; + } + + interface Query { + [key: string]: string | string[] | undefined; + }; + + export type RoutedQuery

= Extract< + Route, + { pathname: P } + >["query"]; + + export type Locale = undefined; + + /** + * A typesafe utility function for generating paths in your application. + * + * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". + */ + export declare function route(r: Route): string; +} + +// prettier-ignore +declare module "next/link" { + import type { Route } from "nextjs-routes"; + import type { LinkProps as NextLinkProps } from "next/dist/client/link"; + import type { + AnchorHTMLAttributes, + DetailedReactHTMLElement, + MouseEventHandler, + PropsWithChildren, + } from "react"; + export * from "next/dist/client/link"; + + type Query = { query?: { [key: string]: string | string[] | undefined } }; + type StaticRoute = Exclude["pathname"]; + + export interface LinkProps + extends Omit, + AnchorHTMLAttributes { + href: Route | StaticRoute | Query; + locale?: false; + } + + type LinkReactElement = DetailedReactHTMLElement< + { + onMouseEnter?: MouseEventHandler | undefined; + onClick: MouseEventHandler; + href?: string | undefined; + ref?: any; + }, + HTMLElement + >; + + declare function Link(props: PropsWithChildren): LinkReactElement; + + export default Link; +} + +// prettier-ignore +declare module "next/router" { + import type { Locale, Route, RoutedQuery } from "nextjs-routes"; + import type { NextRouter as Router } from "next/dist/client/router"; + export * from "next/dist/client/router"; + export { default } from "next/dist/client/router"; + + type NextTransitionOptions = NonNullable[2]>; + type StaticRoute = Exclude["pathname"]; + type Query = { query?: { [key: string]: string | string[] | undefined } }; + + interface TransitionOptions extends Omit { + locale?: false; + } + + export type NextRouter

= + Extract & + Omit< + Router, + | "push" + | "replace" + | "locale" + | "locales" + | "defaultLocale" + | "domainLocales" + > & { + defaultLocale?: undefined; + domainLocales?: undefined; + locale?: Locale; + locales?: undefined; + push( + url: Route | StaticRoute | Query, + as?: string, + options?: TransitionOptions + ): Promise; + replace( + url: Route | StaticRoute | Query, + as?: string, + options?: TransitionOptions + ): Promise; + route: P; + }; + + export function useRouter

(): NextRouter

; +} diff --git a/explorer/frontend/nextjs/redirects.js b/explorer/frontend/nextjs/redirects.js new file mode 100644 index 000000000..d995fb395 --- /dev/null +++ b/explorer/frontend/nextjs/redirects.js @@ -0,0 +1,275 @@ +const oldUrls = [ + // ACCOUNT + { + source: '/account/tag_address', + destination: '/account/tag-address', + }, + { + source: '/account/tag_address/new', + destination: '/account/tag-address', + }, + { + source: '/account/tag_transaction', + destination: '/account/tag-address?tab=tx', + }, + { + source: '/account/tag_transaction/new', + destination: '/account/tag-address?tab=tx', + }, + { + source: '/account/watchlist_address/:id/edit', + destination: '/account/watchlist', + }, + { + source: '/account/watchlist_address/new', + destination: '/account/watchlist', + }, + { + source: '/account/api_key', + destination: '/account/api-key', + }, + { + source: '/account/api_key/:id/edit', + destination: '/account/api-key', + }, + { + source: '/account/api_key/new', + destination: '/account/api-key', + }, + { + source: '/account/custom_abi', + destination: '/account/custom-abi', + }, + { + source: '/account/custom_abi/:id/edit', + destination: '/account/custom-abi', + }, + { + source: '/account/custom_abi/new', + destination: '/account/custom-abi', + }, + { + source: '/account/public-tags-request', + destination: '/public-tags/submit', + }, + { + source: '/account/rewards', + destination: '/account/merits', + }, + + // TRANSACTIONS + { + source: '/pending-transactions', + destination: '/txs?tab=pending', + }, + { + source: '/tx/:hash/internal-transactions', + destination: '/tx/:hash?tab=internal', + }, + { + source: '/tx/:hash/logs', + destination: '/tx/:hash?tab=logs', + }, + { + source: '/tx/:hash/raw-trace', + destination: '/tx/:hash?tab=raw_trace', + }, + { + source: '/tx/:hash/state', + destination: '/tx/:hash?tab=state', + }, + { + source: '/tx/:hash/token-transfers', + destination: '/tx/:hash?tab=token_transfers', + }, + + // BLOCKS + { + source: '/blocks/:height/:path*', + destination: '/block/:height/:path*', + }, + { + source: '/uncles', + destination: '/blocks?tab=uncles', + }, + { + source: '/reorgs', + destination: '/blocks?tab=reorgs', + }, + { + source: '/block/:height/transactions', + destination: '/block/:height?tab=txs', + }, + { + source: '/block/:height/withdrawals', + destination: '/block/:height?tab=withdrawals', + }, + + // ADDRESS + { + source: '/address/:hash/transactions', + destination: '/address/:hash', + }, + { + source: '/address/:hash/token-transfers', + destination: '/address/:hash?tab=token_transfers', + }, + { + source: '/address/:hash/tokens', + destination: '/address/:hash?tab=tokens', + }, + { + source: '/address/:hash/internal-transactions', + destination: '/address/:hash?tab=internal_txns', + }, + { + source: '/address/:hash/coin-balances', + destination: '/address/:hash?tab=coin_balance_history', + }, + { + source: '/address/:hash/logs', + destination: '/address/:hash?tab=logs', + }, + { + source: '/address/:hash/validations', + destination: '/address/:hash?tab=blocks_validated', + }, + { + source: '/address/:hash/contracts', + destination: '/address/:hash?tab=contract', + }, + { + source: '/address/:hash/read-contract', + destination: '/address/:hash?tab=read_contract', + }, + { + source: '/address/:hash/read-proxy', + destination: '/address/:hash?tab=read_proxy', + }, + { + source: '/address/:hash/write-contract', + destination: '/address/:hash?tab=write_contract', + }, + { + source: '/address/:hash/write-proxy', + destination: '/address/:hash?tab=write_proxy', + }, + { + source: '/address/:hash/tokens/:token_hash/token-transfers', + destination: '/address/:hash?tab=token_transfers&token=:token_hash', + }, + + // CONTRACT VERIFICATION + { + source: '/address/:hash/contract_verifications/new', + destination: '/address/:hash/contract_verification', + }, + { + source: '/address/:hash/verify-via-flattened-code/new', + destination: '/address/:hash/contract_verification?method=flatten_source_code', + }, + { + source: '/address/:hash/verify-via-standard-json-input/new', + destination: '/address/:hash/contract_verification?method=standard_input', + }, + { + source: '/address/:hash/verify-via-metadata-json/new', + destination: '/address/:hash/contract_verification?method=sourcify', + }, + { + source: '/address/:hash/verify-via-multi-part-files/new', + destination: '/address/:hash/contract_verification?method=multi_part_file', + }, + { + source: '/address/:hash/verify-vyper-contract/new', + destination: '/address/:hash/contract_verification?method=vyper_contract', + }, + + // TOKENS + { + source: '/bridged-tokens', + destination: '/tokens/?tab=bridged', + }, + { + source: '/bridged-tokens/:chain_name', + destination: '/tokens/?tab=bridged', + }, + { + source: '/tokens/:hash/:path*', + destination: '/token/:hash/:path*', + }, + { + source: '/token/:hash/token-transfers', + destination: '/token/:hash/?tab=token_transfers', + }, + { + source: '/token/:hash/token-holders', + destination: '/token/:hash/?tab=holders', + }, + { + source: '/token/:hash/inventory', + destination: '/token/:hash/?tab=inventory', + }, + { + source: '/token/:hash/instance/:id/token-transfers', + destination: '/token/:hash/instance/:id', + }, + { + source: '/token/:hash/instance/:id/token-holders', + destination: '/token/:hash/instance/:id?tab=holders', + }, + { + source: '/token/:hash/instance/:id/metadata', + destination: '/token/:hash/instance/:id?tab=metadata', + }, + { + source: '/token/:hash/read-contract', + destination: '/token/:hash?tab=read_contract', + }, + { + source: '/token/:hash/read-proxy', + destination: '/token/:hash?tab=read_proxy', + }, + { + source: '/token/:hash/write-contract', + destination: '/token/:hash?tab=write_contract', + }, + { + source: '/token/:hash/write-proxy', + destination: '/token/:hash?tab=write_proxy', + }, + + // ROLLUPs + { + source: '/l2-txn-batches', + destination: '/batches', + }, + { + source: '/zkevm-l2-txn-batches', + destination: '/batches', + }, + { + source: '/zkevm-l2-txn-batch/:path*', + destination: '/batches/:path*', + }, + { + source: '/l2-deposits', + destination: '/deposits', + }, + { + source: '/l2-withdrawals', + destination: '/withdrawals', + }, + { + source: '/l2-output-roots', + destination: '/output-roots', + }, +]; + +async function redirects() { + return [ + ...oldUrls.map((item) => ({ ...item, permanent: false })), + ]; +} + +module.exports = redirects; diff --git a/explorer/frontend/nextjs/rewrites.js b/explorer/frontend/nextjs/rewrites.js new file mode 100644 index 000000000..f779a07f9 --- /dev/null +++ b/explorer/frontend/nextjs/rewrites.js @@ -0,0 +1,8 @@ +async function rewrites() { + return [ + { source: '/node-api/proxy/:slug*', destination: '/api/proxy' }, + { source: '/node-api/:slug*', destination: '/api/:slug*' }, + ].filter(Boolean); +} + +module.exports = rewrites; diff --git a/explorer/frontend/nextjs/types.ts b/explorer/frontend/nextjs/types.ts new file mode 100644 index 000000000..a6f2a38a4 --- /dev/null +++ b/explorer/frontend/nextjs/types.ts @@ -0,0 +1,13 @@ +import type { NextPage } from 'next'; +import type React from 'react'; + +import type { Route } from 'nextjs-routes'; + +export type NextPageWithLayout

= NextPage & { + getLayout?: (page: React.ReactElement) => React.ReactNode; +}; + +export interface RouteParams { + pathname: Pathname; + query?: Route['query']; +} diff --git a/explorer/frontend/nextjs/utils/buildUrl.ts b/explorer/frontend/nextjs/utils/buildUrl.ts new file mode 100644 index 000000000..a068e9d09 --- /dev/null +++ b/explorer/frontend/nextjs/utils/buildUrl.ts @@ -0,0 +1,22 @@ +import { compile } from 'path-to-regexp'; + +import getResourceParams from 'lib/api/getResourceParams'; +import type { ResourceName } from 'lib/api/resources'; + +export default function buildUrl( + _resource: ResourceName, + pathParams?: Record, + queryParams?: Record, +) { + const { resource, api } = getResourceParams(_resource); + const baseUrl = api.endpoint; + const basePath = api.basePath ?? ''; + const path = basePath + resource.path; + const url = new URL(compile(path)(pathParams), baseUrl); + + queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { + value && url.searchParams.append(key, String(value)); + }); + + return url.toString(); +} diff --git a/explorer/frontend/nextjs/utils/detectBotRequest.ts b/explorer/frontend/nextjs/utils/detectBotRequest.ts new file mode 100644 index 000000000..d87a3972c --- /dev/null +++ b/explorer/frontend/nextjs/utils/detectBotRequest.ts @@ -0,0 +1,52 @@ +import type { IncomingMessage } from 'http'; + +type SocialPreviewBot = 'twitter' | 'facebook' | 'telegram' | 'slack'; +type SearchEngineBot = 'google' | 'bing' | 'yahoo' | 'duckduckgo'; + +type ReturnType = { + type: 'social_preview'; + bot: SocialPreviewBot; +} | { + type: 'search_engine'; + bot: SearchEngineBot; +} | undefined; + +export default function detectBotRequest(req: IncomingMessage): ReturnType { + const userAgent = req.headers['user-agent']; + + if (!userAgent) { + return; + } + + if (userAgent.toLowerCase().includes('twitter')) { + return { type: 'social_preview', bot: 'twitter' }; + } + + if (userAgent.toLowerCase().includes('facebook')) { + return { type: 'social_preview', bot: 'facebook' }; + } + + if (userAgent.toLowerCase().includes('telegram')) { + return { type: 'social_preview', bot: 'telegram' }; + } + + if (userAgent.toLowerCase().includes('slack')) { + return { type: 'social_preview', bot: 'slack' }; + } + + if (userAgent.toLowerCase().includes('googlebot')) { + return { type: 'search_engine', bot: 'google' }; + } + + if (userAgent.toLowerCase().includes('bingbot')) { + return { type: 'search_engine', bot: 'bing' }; + } + + if (userAgent.toLowerCase().includes('yahoo')) { + return { type: 'search_engine', bot: 'yahoo' }; + } + + if (userAgent.toLowerCase().includes('duckduck')) { + return { type: 'search_engine', bot: 'duckduckgo' }; + } +} diff --git a/explorer/frontend/nextjs/utils/fetchApi.ts b/explorer/frontend/nextjs/utils/fetchApi.ts new file mode 100644 index 000000000..807d49f67 --- /dev/null +++ b/explorer/frontend/nextjs/utils/fetchApi.ts @@ -0,0 +1,45 @@ +import fetch, { AbortError } from 'node-fetch'; + +import buildUrl from 'nextjs/utils/buildUrl'; +import { httpLogger } from 'nextjs/utils/logger'; + +import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources'; +import metrics from 'lib/monitoring/metrics'; +import { SECOND } from 'toolkit/utils/consts'; + +type Params = { + resource: R; + pathParams?: ResourcePathParams; + queryParams?: Record; + timeout?: number; +}; + +export default async function fetchApi>(params: Params): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + }, params.timeout || SECOND); + + const url = buildUrl(params.resource, params.pathParams, params.queryParams); + + const end = metrics?.apiRequestDuration.startTimer(); + + try { + const response = await fetch(url, { signal: controller.signal }); + + const duration = end?.({ route: params.resource, code: response.status }); + if (response.status === 200) { + httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration }); + } else { + httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration }); + } + + return await response.json() as Promise; + } catch (error) { + const code = error instanceof AbortError ? 504 : 500; + const duration = end?.({ route: params.resource, code }); + httpLogger.logger.error({ message: 'API fetch', url, code, duration }); + } finally { + clearTimeout(timeout); + } +} diff --git a/explorer/frontend/nextjs/utils/fetchProxy.ts b/explorer/frontend/nextjs/utils/fetchProxy.ts new file mode 100644 index 000000000..ee1d4be13 --- /dev/null +++ b/explorer/frontend/nextjs/utils/fetchProxy.ts @@ -0,0 +1,59 @@ +import { pick } from 'es-toolkit'; +import type { IncomingMessage } from 'http'; +import type { NextApiRequest } from 'next'; +import type { NextApiRequestCookies } from 'next/dist/server/api-utils'; +import type { RequestInit, Response } from 'node-fetch'; +import nodeFetch from 'node-fetch'; + +import { httpLogger } from 'nextjs/utils/logger'; + +export default function fetchFactory( + _req: NextApiRequest | (IncomingMessage & { cookies: NextApiRequestCookies }), +) { + // first arg can be only a string + // FIXME migrate to RequestInfo later if needed + return function fetch(url: string, init?: RequestInit): Promise { + const cookie = Object.entries(_req.cookies) + .map(([ key, value ]) => `${ key }=${ value }`) + .join('; '); + + const headers = { + accept: _req.headers['accept'] || 'application/json', + 'content-type': _req.headers['content-type'] || 'application/json', + cookie, + ...pick(_req.headers, [ + 'x-csrf-token', + 'Authorization', // the old value, just in case + 'authorization', // Node.js automatically lowercases headers + // feature flags + 'updated-gas-oracle', + ]) as Record, + }; + + httpLogger.logger.info({ + message: 'API fetch via Next.js proxy', + url, + // headers, + // init, + }); + + const body = (() => { + const _body = init?.body; + if (!_body) { + return; + } + + if (typeof _body === 'string') { + return _body; + } + + return JSON.stringify(_body); + })(); + + return nodeFetch(url, { + ...init, + headers, + body, + }); + }; +} diff --git a/explorer/frontend/nextjs/utils/logRequestFromBot.ts b/explorer/frontend/nextjs/utils/logRequestFromBot.ts new file mode 100644 index 000000000..6c22b6325 --- /dev/null +++ b/explorer/frontend/nextjs/utils/logRequestFromBot.ts @@ -0,0 +1,28 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + +import metrics from 'lib/monitoring/metrics'; + +import detectBotRequest from './detectBotRequest'; + +export default async function logRequestFromBot(req: IncomingMessage | undefined, res: ServerResponse | undefined, pathname: string) { + if (!req || !res || !metrics) { + return; + } + + const botInfo = detectBotRequest(req); + + if (!botInfo) { + return; + } + + switch (botInfo.type) { + case 'search_engine': { + metrics.searchEngineBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + case 'social_preview': { + metrics.socialPreviewBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + } +} diff --git a/explorer/frontend/nextjs/utils/logger.ts b/explorer/frontend/nextjs/utils/logger.ts new file mode 100644 index 000000000..3882ce95b --- /dev/null +++ b/explorer/frontend/nextjs/utils/logger.ts @@ -0,0 +1,3 @@ +import pino from 'pino-http'; + +export const httpLogger = pino(); diff --git a/explorer/frontend/nextjs/utils/serverTiming.ts b/explorer/frontend/nextjs/utils/serverTiming.ts new file mode 100644 index 000000000..5a1a45589 --- /dev/null +++ b/explorer/frontend/nextjs/utils/serverTiming.ts @@ -0,0 +1,10 @@ +import type { ServerResponse } from 'http'; + +export function appendValue(res: ServerResponse | undefined, name: string, value: number) { + const currentValue = res?.getHeader('Server-Timing') || ''; + const nextValue = [ + currentValue, + `${ name };dur=${ value }`, + ].filter(Boolean).join(','); + res?.setHeader('Server-Timing', nextValue); +} diff --git a/explorer/frontend/package.json b/explorer/frontend/package.json new file mode 100644 index 000000000..7f71201a2 --- /dev/null +++ b/explorer/frontend/package.json @@ -0,0 +1,199 @@ +{ + "name": "blockscout-frontend", + "version": "1.0.0", + "private": false, + "homepage": "https://github.com/blockscout/frontend#readme", + "engines": { + "node": "22.11.0", + "npm": "10.9.0" + }, + "scripts": { + "dev": "next dev", + "dev:preset": "./tools/scripts/dev.preset.sh", + "dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js", + "build": "next build", + "build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && yarn svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build", + "build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./", + "start": "next start", + "start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout-frontend:local", + "start:docker:preset": "./tools/scripts/docker.preset.sh", + "chakra:snippets:add": "chakra snippet add --outdir ./toolkit/chakra", + "chakra:typegen": "chakra typegen ./toolkit/theme/theme.ts", + "lint:eslint": "eslint .", + "lint:eslint:fix": "eslint . --fix", + "lint:tsc": "tsc -p ./tsconfig.json", + "lint:envs-validator:test": "cd ./deploy/tools/envs-validator && ./test.sh", + "prepare": "husky install", + "svg:format": "svgo -r ./icons", + "svg:build-sprite": "./deploy/scripts/build_sprite.sh", + "test:pw": "./tools/scripts/pw.sh", + "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", + "test:pw:docker": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.49.0-noble ./tools/scripts/pw.docker.sh", + "test:pw:docker:deps": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.49.0-noble ./tools/scripts/pw.docker.deps.sh", + "test:pw:ci": "yarn test:pw --project=$PW_PROJECT", + "test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js", + "test:jest": "jest", + "test:jest:watch": "jest --watch", + "favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh", + "og-image:generate:dev": "./tools/scripts/og-image-generator.dev.sh", + "sitemap:generate:dev": "./tools/scripts/sitemap-generator.dev.sh", + "monitoring:prometheus:local": "docker run --name blockscout_prometheus -d -p 127.0.0.1:9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus", + "monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise", + "postinstall": "chakra typegen ./toolkit/theme/theme.ts" + }, + "dependencies": { + "@blockscout/bens-types": "1.4.1", + "@blockscout/points-types": "1.3.0-alpha.2", + "@blockscout/stats-types": "2.5.0-alpha", + "@blockscout/visualizer-types": "0.2.0", + "@chakra-ui/react": "3.15.0", + "@cloudnouns/kit": "1.1.6", + "@emotion/react": "11.14.0", + "@growthbook/growthbook-react": "0.21.0", + "@helia/verified-fetch": "2.6.12", + "@hypelab/sdk-react": "^1.0.0", + "@metamask/post-message-stream": "^7.0.0", + "@metamask/providers": "^10.2.1", + "@monaco-editor/react": "^4.4.6", + "@next/bundle-analyzer": "15.0.3", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/auto-instrumentations-node": "0.43.0", + "@opentelemetry/exporter-jaeger": "1.27.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.49.1", + "@opentelemetry/exporter-trace-otlp-http": "0.49.1", + "@opentelemetry/resources": "1.22.0", + "@opentelemetry/sdk-node": "0.49.1", + "@opentelemetry/sdk-trace-node": "1.22.0", + "@opentelemetry/semantic-conventions": "1.22.0", + "@prisma/client": "6.8.2", + "@reown/appkit": "1.7.0", + "@reown/appkit-adapter-wagmi": "1.7.0", + "@rollbar/react": "0.12.1", + "@scure/base": "1.1.9", + "@slise/embed-react": "^2.2.0", + "@tanstack/react-query": "5.55.4", + "@tanstack/react-query-devtools": "5.55.4", + "@types/papaparse": "^5.3.5", + "@types/react-scroll": "^1.8.4", + "@uidotdev/usehooks": "2.4.1", + "airtable": "^0.12.2", + "bignumber.js": "^9.1.0", + "blo": "^1.1.1", + "chakra-react-select": "^4.4.3", + "crypto-js": "^4.2.0", + "d3": "^7.6.1", + "dappscout-iframe": "0.2.6", + "dayjs": "^1.11.5", + "dom-to-image": "^2.6.0", + "es-toolkit": "1.31.0", + "focus-visible": "^5.2.0", + "gradient-avatar": "git+https://github.com/blockscout/gradient-avatar.git", + "graphiql": "^2.2.0", + "graphql": "^16.8.1", + "graphql-ws": "^5.11.3", + "js-cookie": "^3.0.1", + "magic-bytes.js": "1.8.0", + "mixpanel-browser": "^2.47.0", + "monaco-editor": "^0.34.1", + "next": "15.2.3", + "next-themes": "0.4.4", + "nextjs-routes": "^1.0.8", + "node-fetch": "^3.2.9", + "papaparse": "^5.3.2", + "path-to-regexp": "8.1.0", + "phoenix": "^1.6.15", + "pino-http": "^8.2.1", + "pino-pretty": "^9.1.1", + "prom-client": "15.1.1", + "qrcode": "^1.5.1", + "react": "18.3.1", + "react-device-detect": "^2.2.3", + "react-dom": "18.3.1", + "react-google-recaptcha": "3.1.0", + "react-hook-form": "7.52.1", + "react-icons": "5.4.0", + "react-identicons": "^1.2.5", + "react-intersection-observer": "^9.16.0", + "react-jazzicon": "^1.0.4", + "react-number-format": "^5.3.1", + "react-scroll": "^1.8.7", + "rollbar": "2.26.4", + "swagger-ui-react": "5.20.3", + "use-font-face-observer": "^1.2.1", + "valibot": "0.38.0", + "viem": "2.23.14", + "wagmi": "2.14.15", + "xss": "^1.0.14" + }, + "devDependencies": { + "@chakra-ui/cli": "3.15.0", + "@eslint/compat": "1.2.2", + "@eslint/js": "9.14.0", + "@next/eslint-plugin-next": "15.0.3", + "@playwright/experimental-ct-react": "1.49.0", + "@playwright/test": "1.49.0", + "@stylistic/eslint-plugin": "2.10.1", + "@svgr/webpack": "^6.5.1", + "@tanstack/eslint-plugin-query": "5.60.1", + "@testing-library/react": "^14.0.0", + "@total-typescript/ts-reset": "^0.4.0", + "@types/crypto-js": "^4.1.1", + "@types/csp-dev": "^1.0.0", + "@types/d3": "^7.4.0", + "@types/dom-to-image": "^2.6.4", + "@types/jest": "29.2.1", + "@types/js-cookie": "^3.0.2", + "@types/mixpanel-browser": "^2.38.1", + "@types/node": "20.16.7", + "@types/phoenix": "^1.5.4", + "@types/qrcode": "^1.5.0", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@types/react-google-recaptcha": "^2.1.5", + "@types/swagger-ui-react": "5.18.0", + "@types/ws": "^8.5.3", + "@typescript-eslint/eslint-plugin": "^5.60.0", + "@vitejs/plugin-react": "^4.0.0", + "css-loader": "^6.7.3", + "dotenv-cli": "^6.0.0", + "eslint": "9.14.0", + "eslint-config-next": "15.0.3", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-import-helpers": "2.0.1", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-no-cyrillic-string": "^1.0.5", + "eslint-plugin-playwright": "2.0.1", + "eslint-plugin-react": "7.37.2", + "eslint-plugin-react-hooks": "5.0.0", + "eslint-plugin-regexp": "2.6.0", + "globals": "15.12.0", + "husky": "^8.0.0", + "jest": "^29.2.1", + "jest-environment-jsdom": "^29.2.1", + "jest-fetch-mock": "^3.0.3", + "lint-staged": ">=10", + "mockdate": "^3.0.5", + "prisma": "^6.8.2", + "style-loader": "^3.3.1", + "svg-icons-cli": "^0.0.5", + "svgo": "^2.8.0", + "ts-jest": "^29.0.3", + "ts-node": "^10.9.2", + "typescript": "5.4.2", + "typescript-eslint": "8.14.0", + "vite-plugin-svgr": "^2.2.2", + "vite-tsconfig-paths": "4.3.2", + "ws": "^8.17.1" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": "eslint --cache --fix" + }, + "resolutions": { + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1" + }, + "prisma": { + "seed": "ts-node -P prisma/tsconfig.seed.json prisma/seed.ts" + } +} diff --git a/explorer/frontend/pages/404.tsx b/explorer/frontend/pages/404.tsx new file mode 100644 index 000000000..f19180f98 --- /dev/null +++ b/explorer/frontend/pages/404.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import type { NextPageWithLayout } from 'nextjs/types'; + +import PageNextJs from 'nextjs/PageNextJs'; + +import AppError from 'ui/shared/AppError/AppError'; +import LayoutError from 'ui/shared/layout/LayoutError'; + +const error = new Error('Not found', { cause: { status: 404 } }); + +const Page: NextPageWithLayout = () => { + return ( + + + + ); +}; + +Page.getLayout = function getLayout(page: React.ReactElement) { + return ( + + { page } + + ); +}; + +export default Page; diff --git a/explorer/frontend/pages/_app.tsx b/explorer/frontend/pages/_app.tsx new file mode 100644 index 000000000..d5a4ca4f2 --- /dev/null +++ b/explorer/frontend/pages/_app.tsx @@ -0,0 +1,105 @@ +import type { HTMLChakraProps } from '@chakra-ui/react'; +import { GrowthBookProvider } from '@growthbook/growthbook-react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import type { AppProps } from 'next/app'; +import React from 'react'; + +import type { NextPageWithLayout } from 'nextjs/types'; + +import config from 'configs/app'; +import useQueryClientConfig from 'lib/api/useQueryClientConfig'; +import { AppContextProvider } from 'lib/contexts/app'; +import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; +import { RewardsContextProvider } from 'lib/contexts/rewards'; +import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; +import { SettingsContextProvider } from 'lib/contexts/settings'; +import { initGrowthBook } from 'lib/growthbook/init'; +import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; +import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/rollbar'; +import { Provider as ChakraProvider } from 'toolkit/chakra/provider'; +import { Toaster } from 'toolkit/chakra/toaster'; +import RewardsLoginModal from 'ui/rewards/login/RewardsLoginModal'; +import RewardsActivityTracker from 'ui/rewards/RewardsActivityTracker'; +import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; +import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer'; +import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; +import Layout from 'ui/shared/layout/Layout'; +import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; + +import 'lib/setLocale'; +// import 'focus-visible/dist/focus-visible'; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +}; + +const ERROR_SCREEN_STYLES: HTMLChakraProps<'div'> = { + h: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'center', + width: 'fit-content', + maxW: '800px', + margin: { base: '0 auto', lg: '0 auto' }, + p: { base: 4, lg: 0 }, +}; + +function MyApp({ Component, pageProps }: AppPropsWithLayout) { + + const growthBook = initGrowthBook(pageProps.uuid); + useLoadFeatures(growthBook); + + const queryClient = useQueryClientConfig(); + + const content = (() => { + const getLayout = Component.getLayout ?? ((page) => { page }); + + return ( + <> + { getLayout() } + + { config.features.rewards.isEnabled && ( + <> + + + + ) } + + ); + })(); + + return ( + + + + + + + + + + + + { content } + + + + + + + + + + + + + + ); +} + +export default MyApp; diff --git a/explorer/frontend/pages/_document.tsx b/explorer/frontend/pages/_document.tsx new file mode 100644 index 000000000..254eb57f5 --- /dev/null +++ b/explorer/frontend/pages/_document.tsx @@ -0,0 +1,64 @@ +import type { DocumentContext } from 'next/document'; +import Document, { Html, Head, Main, NextScript } from 'next/document'; +import React from 'react'; + +import logRequestFromBot from 'nextjs/utils/logRequestFromBot'; +import * as serverTiming from 'nextjs/utils/serverTiming'; + +import config from 'configs/app'; + +class MyDocument extends Document { + static async getInitialProps(ctx: DocumentContext) { + const originalRenderPage = ctx.renderPage; + ctx.renderPage = async() => { + const start = Date.now(); + const result = await originalRenderPage(); + const end = Date.now(); + + serverTiming.appendValue(ctx.res, 'renderPage', end - start); + + return result; + }; + + await logRequestFromBot(ctx.req, ctx.res, ctx.pathname); + + const initialProps = await Document.getInitialProps(ctx); + + return initialProps; + } + + render() { + return ( + + + { /* FONTS */ } + + + + { /* eslint-disable-next-line @next/next/no-sync-scripts */ } + + + + + diff --git a/explorer/frontend/playwright/index.ts b/explorer/frontend/playwright/index.ts new file mode 100644 index 000000000..31ab5f53d --- /dev/null +++ b/explorer/frontend/playwright/index.ts @@ -0,0 +1,28 @@ +import './fonts.css'; +import './index.css'; +import { beforeMount } from '@playwright/experimental-ct-react/hooks'; +import MockDate from 'mockdate'; +import * as router from 'next/router'; + +const NEXT_ROUTER_MOCK = { + query: {}, + pathname: '', + push: () => Promise.resolve(), + replace: () => Promise.resolve(), +}; + +beforeMount(async({ hooksConfig }: { hooksConfig?: { router: typeof router } }) => { + // Before mount, redefine useRouter to return mock value from test. + + // @ts-ignore: I really want to redefine this property :) + // eslint-disable-next-line no-import-assign + router.useRouter = () => ({ + ...NEXT_ROUTER_MOCK, + ...hooksConfig?.router, + }); + + // set current date + MockDate.set('2022-11-11T12:00:00Z'); +}); + +export {}; diff --git a/explorer/frontend/playwright/lib.tsx b/explorer/frontend/playwright/lib.tsx new file mode 100644 index 000000000..97448315e --- /dev/null +++ b/explorer/frontend/playwright/lib.tsx @@ -0,0 +1,76 @@ +/* eslint-disable no-console */ +import { test as base } from '@playwright/experimental-ct-react'; +import type { Page } from '@playwright/test'; + +import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider'; +import * as mockApiResponse from './fixtures/mockApiResponse'; +import * as mockAssetResponse from './fixtures/mockAssetResponse'; +import * as mockConfigResponse from './fixtures/mockConfigResponse'; +import * as mockContractReadResponse from './fixtures/mockContractReadResponse'; +import * as mockEnvs from './fixtures/mockEnvs'; +import * as mockFeatures from './fixtures/mockFeatures'; +import * as mockRpcResponse from './fixtures/mockRpcResponse'; +import * as mockTextAd from './fixtures/mockTextAd'; +import * as render from './fixtures/render'; +import * as socketServer from './fixtures/socketServer'; + +export interface Fixtures { + render: render.RenderFixture; + mockApiResponse: mockApiResponse.MockApiResponseFixture; + mockAssetResponse: mockAssetResponse.MockAssetResponseFixture; + mockConfigResponse: mockConfigResponse.MockConfigResponseFixture; + mockContractReadResponse: mockContractReadResponse.MockContractReadResponseFixture; + mockEnvs: mockEnvs.MockEnvsFixture; + mockFeatures: mockFeatures.MockFeaturesFixture; + mockRpcResponse: mockRpcResponse.MockRpcResponseFixture; + createSocket: socketServer.CreateSocketFixture; + injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider; + mockTextAd: mockTextAd.MockTextAdFixture; +} + +export type TestFnArgs = Fixtures & { page: Page }; + +const test = base.extend({ + render: render.default, + mockApiResponse: mockApiResponse.default, + mockAssetResponse: mockAssetResponse.default, + mockConfigResponse: mockConfigResponse.default, + mockContractReadResponse: mockContractReadResponse.default, + mockEnvs: mockEnvs.default, + mockFeatures: mockFeatures.default, + mockRpcResponse: mockRpcResponse.default, + // FIXME: for some reason Playwright does not intercept requests to text ad provider when running multiple tests in parallel + // even if we have a global request interceptor (maybe it is related to service worker issue, maybe not) + // so we have to inject mockTextAd fixture in each test and mock the response where it is needed + mockTextAd: mockTextAd.default, + createSocket: socketServer.createSocket, + injectMetaMaskProvider: injectMetaMaskProvider.default, +}); + +test.beforeEach(async({ page, mockTextAd }) => { + // debug + const isDebug = process.env.PWDEBUG === '1'; + + if (isDebug) { + page.on('console', msg => console.log(msg.text())); + page.on('request', request => console.info('\x1b[34m%s\x1b[0m', '>>', request.method(), request.url())); + page.on('response', response => console.info('\x1b[35m%s\x1b[0m', '<<', String(response.status()), response.url())); + } + + // Abort all other requests to external resources + await page.route('**', (route) => { + if (!route.request().url().startsWith('http://localhost')) { + isDebug && console.info('Aborting request to', route.request().url()); + route.abort(); + } else { + route.continue(); + } + }); + + // with few exceptions: + // 1. mock text AD requests + await mockTextAd(); +}); + +export * from '@playwright/experimental-ct-react'; +export { test }; diff --git a/explorer/frontend/playwright/mocks/file_mock_1.json b/explorer/frontend/playwright/mocks/file_mock_1.json new file mode 100644 index 000000000..13d905de8 --- /dev/null +++ b/explorer/frontend/playwright/mocks/file_mock_1.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} \ No newline at end of file diff --git a/explorer/frontend/playwright/mocks/file_mock_2.json b/explorer/frontend/playwright/mocks/file_mock_2.json new file mode 100644 index 000000000..9532cafdf --- /dev/null +++ b/explorer/frontend/playwright/mocks/file_mock_2.json @@ -0,0 +1,4 @@ +{ + "foo": "bar", + "baz": ["baz","baz","baz"] +} \ No newline at end of file diff --git a/explorer/frontend/playwright/mocks/file_mock_with_very_long_name.json b/explorer/frontend/playwright/mocks/file_mock_with_very_long_name.json new file mode 100644 index 000000000..c6ca79c34 --- /dev/null +++ b/explorer/frontend/playwright/mocks/file_mock_with_very_long_name.json @@ -0,0 +1,19 @@ +{ + "id": 1, + "title": "iPhone 9", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce nec leo odio. Vivamus iaculis faucibus tempus. Duis dapibus, ligula eu consequat ornare, sapien nisl laoreet arcu, eu pharetra dolor urna eget ligula. Sed facilisis pretium risus in finibus. Maecenas egestas orci euismod venenatis aliquam. Vivamus sed pretium dui, vitae porta ipsum. Donec elit tortor, imperdiet eu nulla at, condimentum consequat nisi. Phasellus mollis sem aliquam dolor lacinia bibendum. Mauris pulvinar vestibulum sodales. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam vitae facilisis odio. Mauris in ultricies nulla. Sed ornare consequat nulla. Cras pellentesque, erat at gravida rutrum, erat neque facilisis magna, et iaculis enim tortor ornare felis. Quisque mollis dignissim magna. In vitae ultricies felis. Quisque in ipsum non lorem ornare elementum vel vitae nisl. Integer luctus pulvinar dui a eleifend. Aliquam iaculis odio vitae metus ullamcorper, sed semper turpis consectetur. In convallis, justo a aliquet rhoncus, erat metus fringilla turpis, eget imperdiet purus est quis magna. Curabitur ornare mattis malesuada. In vitae lacinia est. Sed sit amet magna eget dolor tempus euismod.", + "price": 549, + "discountPercentage": 12.96, + "rating": 4.69, + "stock": 94, + "brand": "Apple", + "category": "smartphones", + "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg", + "images": [ + "https://i.dummyjson.com/data/products/1/1.jpg", + "https://i.dummyjson.com/data/products/1/2.jpg", + "https://i.dummyjson.com/data/products/1/3.jpg", + "https://i.dummyjson.com/data/products/1/4.jpg", + "https://i.dummyjson.com/data/products/1/thumbnail.jpg" + ] +} \ No newline at end of file diff --git a/explorer/frontend/playwright/mocks/image_long.jpg b/explorer/frontend/playwright/mocks/image_long.jpg new file mode 100644 index 000000000..f830ed57c Binary files /dev/null and b/explorer/frontend/playwright/mocks/image_long.jpg differ diff --git a/explorer/frontend/playwright/mocks/image_md.jpg b/explorer/frontend/playwright/mocks/image_md.jpg new file mode 100644 index 000000000..88e2472a3 Binary files /dev/null and b/explorer/frontend/playwright/mocks/image_md.jpg differ diff --git a/explorer/frontend/playwright/mocks/image_s.jpg b/explorer/frontend/playwright/mocks/image_s.jpg new file mode 100644 index 000000000..c03a06203 Binary files /dev/null and b/explorer/frontend/playwright/mocks/image_s.jpg differ diff --git a/explorer/frontend/playwright/mocks/image_svg.svg b/explorer/frontend/playwright/mocks/image_svg.svg new file mode 100644 index 000000000..59b2b6ef6 --- /dev/null +++ b/explorer/frontend/playwright/mocks/image_svg.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/frontend/playwright/mocks/lib/growthbook/useFeatureValue.js b/explorer/frontend/playwright/mocks/lib/growthbook/useFeatureValue.js new file mode 100644 index 000000000..e731be89f --- /dev/null +++ b/explorer/frontend/playwright/mocks/lib/growthbook/useFeatureValue.js @@ -0,0 +1,13 @@ +const useFeatureValue = (name, fallback) => { + try { + const value = JSON.parse(localStorage.getItem(`pw_feature:${ name }`)); + if (value === null) { + throw new Error(); + } + return { isLoading: false, value }; + } catch (error) { + return { isLoading: false, value: fallback }; + } +}; + +export default useFeatureValue; diff --git a/explorer/frontend/playwright/mocks/modules/@metamask/post-message-stream.js b/explorer/frontend/playwright/mocks/modules/@metamask/post-message-stream.js new file mode 100644 index 000000000..0212528dc --- /dev/null +++ b/explorer/frontend/playwright/mocks/modules/@metamask/post-message-stream.js @@ -0,0 +1,9 @@ +class WindowPostMessageStream { + constructor() { + return null; + } +} + +export { + WindowPostMessageStream, +}; diff --git a/explorer/frontend/playwright/mocks/modules/@metamask/providers.js b/explorer/frontend/playwright/mocks/modules/@metamask/providers.js new file mode 100644 index 000000000..4c2f48b6f --- /dev/null +++ b/explorer/frontend/playwright/mocks/modules/@metamask/providers.js @@ -0,0 +1,5 @@ +function initializeProvider() {} + +export { + initializeProvider, +}; diff --git a/explorer/frontend/playwright/mocks/modules/@reown/appkit/react.js b/explorer/frontend/playwright/mocks/modules/@reown/appkit/react.js new file mode 100644 index 000000000..4cc9ebd0d --- /dev/null +++ b/explorer/frontend/playwright/mocks/modules/@reown/appkit/react.js @@ -0,0 +1,26 @@ +function useAppKit() { + return { + open: () => {}, + }; +} + +function useAppKitState() { + return { + isOpen: false, + }; +} + +function useAppKitTheme() { + return { + setThemeMode: () => {}, + }; +} + +function createAppKit() {} + +export { + createAppKit, + useAppKit, + useAppKitState, + useAppKitTheme, +}; diff --git a/explorer/frontend/playwright/mocks/network-logo.svg b/explorer/frontend/playwright/mocks/network-logo.svg new file mode 100644 index 000000000..38f2583fc --- /dev/null +++ b/explorer/frontend/playwright/mocks/network-logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/playwright/mocks/page.html b/explorer/frontend/playwright/mocks/page.html new file mode 100644 index 000000000..55da628a8 --- /dev/null +++ b/explorer/frontend/playwright/mocks/page.html @@ -0,0 +1,18 @@ + + + + + + this is HTML page + + \ No newline at end of file diff --git a/explorer/frontend/playwright/mocks/ui/shared/recaptcha/useReCaptcha.js b/explorer/frontend/playwright/mocks/ui/shared/recaptcha/useReCaptcha.js new file mode 100644 index 000000000..7d4b9de47 --- /dev/null +++ b/explorer/frontend/playwright/mocks/ui/shared/recaptcha/useReCaptcha.js @@ -0,0 +1,8 @@ +const useReCaptcha = () => { + return { + ref: { current: null }, + executeAsync: () => Promise.resolve('recaptcha_token'), + }; +}; + +export default useReCaptcha; diff --git a/explorer/frontend/playwright/utils/config.ts b/explorer/frontend/playwright/utils/config.ts new file mode 100644 index 000000000..3e8547509 --- /dev/null +++ b/explorer/frontend/playwright/utils/config.ts @@ -0,0 +1,12 @@ +import { devices } from '@playwright/test'; + +export const viewport = { + mobile: devices['iPhone 13 Pro'].viewport, + md: { width: 1001, height: 800 }, + xl: { width: 1600, height: 1000 }, + xxl: { width: 1920, height: 1200 }, +}; + +export const maskColor = '#4299E1'; // blue.400 + +export const adsBannerSelector = '.adsbyslise'; diff --git a/explorer/frontend/playwright/utils/socket.ts b/explorer/frontend/playwright/utils/socket.ts new file mode 100644 index 000000000..26a8a2202 --- /dev/null +++ b/explorer/frontend/playwright/utils/socket.ts @@ -0,0 +1 @@ +export const port = 3200; diff --git a/explorer/frontend/prisma/migrations/20250531191949_prague/migration.sql b/explorer/frontend/prisma/migrations/20250531191949_prague/migration.sql new file mode 100644 index 000000000..417539d87 --- /dev/null +++ b/explorer/frontend/prisma/migrations/20250531191949_prague/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "LedgerEntry" ( + "id" INTEGER NOT NULL, + "account_id" TEXT NOT NULL, + "account_type" INTEGER NOT NULL, + "asset" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL, + "credit" DECIMAL(18,8) NOT NULL, + "debit" DECIMAL(18,8) NOT NULL, + "participant" TEXT NOT NULL, + + CONSTRAINT "LedgerEntry_pkey" PRIMARY KEY ("id") +); diff --git a/explorer/frontend/prisma/migrations/20250531225216_add_channel_model/migration.sql b/explorer/frontend/prisma/migrations/20250531225216_add_channel_model/migration.sql new file mode 100644 index 000000000..2113b26fc --- /dev/null +++ b/explorer/frontend/prisma/migrations/20250531225216_add_channel_model/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "Channel" ( + "channel_id" TEXT NOT NULL, + "adjudicator" TEXT NOT NULL, + "amount" BIGINT NOT NULL, + "chain_id" INTEGER NOT NULL, + "challenge" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL, + "nonce" BIGINT NOT NULL, + "participant" TEXT NOT NULL, + "status" TEXT NOT NULL, + "token" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL, + "version" INTEGER NOT NULL, + "wallet" TEXT NOT NULL, + + CONSTRAINT "Channel_pkey" PRIMARY KEY ("channel_id") +); + +-- CreateIndex +CREATE INDEX "Channel_chain_id_idx" ON "Channel"("chain_id"); + +-- CreateIndex +CREATE INDEX "Channel_token_idx" ON "Channel"("token"); diff --git a/explorer/frontend/prisma/migrations/migration_lock.toml b/explorer/frontend/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..044d57cdb --- /dev/null +++ b/explorer/frontend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/explorer/frontend/prisma/schema.prisma b/explorer/frontend/prisma/schema.prisma new file mode 100644 index 000000000..6d2e4f9ff --- /dev/null +++ b/explorer/frontend/prisma/schema.prisma @@ -0,0 +1,38 @@ +datasource db { + provider = "postgresql" // You can change this to "mysql", "sqlite", "sqlserver", etc. + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model LedgerEntry { + id Int @id // Using the ID from your provided data + accountId String @map("account_id") + accountType Int @map("account_type") + asset String + createdAt DateTime @map("created_at") + credit Decimal @db.Decimal(18, 8) // Suitable for financial data, adjust precision (total digits, decimal places) as needed + debit Decimal @db.Decimal(18, 8) // Suitable for financial data, adjust precision as needed + participant String +} + +model Channel { + channelId String @id @map("channel_id") + adjudicator String + amount BigInt + chainId Int @map("chain_id") + challenge Int + createdAt DateTime @map("created_at") + nonce BigInt + participant String + status String + token String + updatedAt DateTime @map("updated_at") + version Int + wallet String + + @@index([chainId]) + @@index([token]) +} \ No newline at end of file diff --git a/explorer/frontend/prisma/seed.ts b/explorer/frontend/prisma/seed.ts new file mode 100644 index 000000000..5eebcdf00 --- /dev/null +++ b/explorer/frontend/prisma/seed.ts @@ -0,0 +1,118 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +interface ChannelDataEntry { + adjudicator: string; + amount: string | number; + chain_id: number; + challenge: number; + channel_id: string; + created_at: string; + nonce: string | number; + participant: string; + status: string; + token: string; + updated_at: string; + version: number; + wallet: string; +} + +// Generate 500 channels with varied data +const generateChannelData = (): Array => { + const adjudicators = [ + '0x6D3B5EFa1f81f65037cD842F48E44BcBCa48CBEF', + '0x5F4A4B1D293A973a1Bc0daD3BB3692Bd51058FCF', + '0x4E3C2B1A0D9F8E7C6B5A4D3C2E1F0A9B8C7D6E5', + ]; + + const tokens: Record = { + '137': '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // Polygon + '11155111': '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', // Sepolia + '1': '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // Ethereum + }; + + const chainIds = [ 1, 137, 11155111 ]; + const statuses = [ 'open', 'closed', 'disputed' ]; + const baseTime = new Date('2025-05-01T00:00:00Z').getTime(); + const channels: Array = []; + + for (let i = 0; i < 500; i++) { + const chainId = chainIds[Math.floor(Math.random() * chainIds.length)]; + const status = statuses[Math.floor(Math.random() * statuses.length)]; + const createdAt = new Date(baseTime + Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000)); + const updatedAt = new Date(createdAt.getTime() + Math.floor(Math.random() * 24 * 60 * 60 * 1000)); + + channels.push({ + adjudicator: adjudicators[Math.floor(Math.random() * adjudicators.length)], + amount: Math.floor(Math.random() * 10000000) + 100000, // Random amount between 100k and 10M + chain_id: chainId, + challenge: 3600, + channel_id: '0x' + Array.from({ length: 64 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''), + created_at: createdAt.toISOString(), + nonce: BigInt(Math.floor(Math.random() * 1e16)).toString(), + participant: '0x' + Array.from({ length: 40 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''), + status, + token: tokens[chainId], + updated_at: updatedAt.toISOString(), + version: status === 'open' ? 1 : Math.floor(Math.random() * 3) + 1, + wallet: '0x' + Array.from({ length: 40 }, () => '0123456789abcdef'[Math.floor(Math.random() * 16)]).join(''), + }); + } + + return channels; +}; + +const channelData: Array = generateChannelData(); + +async function main() { + // Clear both tables before seeding + await prisma.ledgerEntry.deleteMany(); + await prisma.channel.deleteMany(); + // eslint-disable-next-line no-console + console.log('Start seeding ...'); + + // Seed Channel data + for (const entry of channelData) { + try { + // eslint-disable-next-line no-console + console.log(`Creating channel with ID: ${ entry.channel_id }`); + await prisma.channel.create({ + data: { + channelId: entry.channel_id, + adjudicator: entry.adjudicator, + amount: BigInt(entry.amount), + chainId: entry.chain_id, + challenge: entry.challenge, + createdAt: new Date(entry.created_at), + nonce: BigInt(entry.nonce), + participant: entry.participant, + status: entry.status, + token: entry.token, + updatedAt: new Date(entry.updated_at), + version: entry.version, + wallet: entry.wallet, + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to create channel ${ entry.channel_id }:`, error); + throw error; // Re-throw to stop the seeding process + } + } + + // eslint-disable-next-line no-console + console.log('Seeding finished.'); +} + +main() + .then(async() => { + await prisma.$disconnect(); + }) + .catch(async(e) => { + // eslint-disable-next-line no-console + console.error(e); + + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/explorer/frontend/prisma/tsconfig.seed.json b/explorer/frontend/prisma/tsconfig.seed.json new file mode 100644 index 000000000..559a1847e --- /dev/null +++ b/explorer/frontend/prisma/tsconfig.seed.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "target": "es2017" + }, + "include": [ + "seed.ts" + ] +} \ No newline at end of file diff --git a/explorer/frontend/prometheus.yml b/explorer/frontend/prometheus.yml new file mode 100644 index 000000000..89b95e0ec --- /dev/null +++ b/explorer/frontend/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + # - "first.rules" + # - "second.rules" + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ['localhost:9090'] + - job_name: frontend + metrics_path: /node-api/metrics + static_configs: + - targets: ['host.docker.internal:3000'] \ No newline at end of file diff --git a/explorer/frontend/public/README.md b/explorer/frontend/public/README.md new file mode 100644 index 000000000..3fd1a97a4 --- /dev/null +++ b/explorer/frontend/public/README.md @@ -0,0 +1,4 @@ +**Directories** +- `/icons` - Folder for SVG-sprite assets, generated at build time. +- `/static` - Folder for static assets that are consistent between app re-runs but may differ from one build version to another. +- `/assets` - Folder for dynamically generated assets during the app start, such as the favicon bundle, ENV variables file, and external app configurations. \ No newline at end of file diff --git a/explorer/frontend/public/assets/favicon/android-chrome-192x192.png b/explorer/frontend/public/assets/favicon/android-chrome-192x192.png new file mode 100644 index 000000000..0ebdd5e3b Binary files /dev/null and b/explorer/frontend/public/assets/favicon/android-chrome-192x192.png differ diff --git a/explorer/frontend/public/assets/favicon/apple-touch-icon-180x180.png b/explorer/frontend/public/assets/favicon/apple-touch-icon-180x180.png new file mode 100644 index 000000000..cf659834c Binary files /dev/null and b/explorer/frontend/public/assets/favicon/apple-touch-icon-180x180.png differ diff --git a/explorer/frontend/public/assets/favicon/favicon-16x16.png b/explorer/frontend/public/assets/favicon/favicon-16x16.png new file mode 100644 index 000000000..09e80da7d Binary files /dev/null and b/explorer/frontend/public/assets/favicon/favicon-16x16.png differ diff --git a/explorer/frontend/public/assets/favicon/favicon-32x32.png b/explorer/frontend/public/assets/favicon/favicon-32x32.png new file mode 100644 index 000000000..064b2edd7 Binary files /dev/null and b/explorer/frontend/public/assets/favicon/favicon-32x32.png differ diff --git a/explorer/frontend/public/assets/favicon/favicon-48x48.png b/explorer/frontend/public/assets/favicon/favicon-48x48.png new file mode 100644 index 000000000..d1897ec81 Binary files /dev/null and b/explorer/frontend/public/assets/favicon/favicon-48x48.png differ diff --git a/explorer/frontend/public/assets/favicon/favicon.ico b/explorer/frontend/public/assets/favicon/favicon.ico new file mode 100644 index 000000000..d786dbc3b Binary files /dev/null and b/explorer/frontend/public/assets/favicon/favicon.ico differ diff --git a/explorer/frontend/public/assets/favicon/logo-icon.png b/explorer/frontend/public/assets/favicon/logo-icon.png new file mode 100644 index 000000000..0ebdd5e3b Binary files /dev/null and b/explorer/frontend/public/assets/favicon/logo-icon.png differ diff --git a/explorer/frontend/public/icons/name.d.ts b/explorer/frontend/public/icons/name.d.ts new file mode 100644 index 000000000..4e54c8b46 --- /dev/null +++ b/explorer/frontend/public/icons/name.d.ts @@ -0,0 +1,192 @@ +// This file is generated by npm run build:icons + + export type IconName = + | "ABI_slim" + | "ABI" + | "API_slim" + | "API" + | "apps_list" + | "apps_slim" + | "apps" + | "arrows/down-right" + | "arrows/east-mini" + | "arrows/east" + | "arrows/north-east" + | "arrows/south-east" + | "arrows/up-down" + | "arrows/up-head" + | "beta_xs" + | "beta" + | "blob" + | "blobs/image" + | "blobs/raw" + | "blobs/text" + | "block_countdown" + | "block_slim" + | "block" + | "brands/blockscout" + | "brands/celenium" + | "brands/graph" + | "brands/safe" + | "brands/solidity_scan" + | "burger" + | "certified" + | "check" + | "checkered_flag" + | "clock-light" + | "clock" + | "close" + | "coins/bitcoin" + | "collection" + | "columns" + | "contracts/proxy" + | "contracts/regular_many" + | "contracts/regular" + | "contracts/verified_many" + | "contracts/verified" + | "copy_check" + | "copy" + | "cross" + | "delete" + | "dex-tracker" + | "docs" + | "donate" + | "dots" + | "edit" + | "email" + | "empty_search_result" + | "ENS_slim" + | "ENS" + | "error-pages/403" + | "error-pages/404" + | "error-pages/422" + | "error-pages/429" + | "error-pages/500" + | "explorer" + | "files/csv" + | "files/image" + | "files/json" + | "files/placeholder" + | "files/sol" + | "files/yul" + | "filter" + | "flame" + | "games" + | "gas_xl" + | "gas" + | "gear_slim" + | "gear" + | "globe-b" + | "globe" + | "graphQL" + | "heart_filled" + | "heart_outline" + | "hourglass" + | "info_filled" + | "info" + | "integration/full" + | "integration/partial" + | "internal_txns" + | "interop" + | "key" + | "lightning_navbar" + | "lightning" + | "link_external" + | "link" + | "lock" + | "merits_colored" + | "merits_slim" + | "merits_with_dot_slim" + | "merits_with_dot" + | "merits" + | "minus" + | "monaco/cargo" + | "monaco/file" + | "monaco/folder-open" + | "monaco/folder" + | "monaco/rust" + | "monaco/solidity" + | "monaco/toml" + | "monaco/vyper" + | "moon-with-star" + | "moon" + | "MUD_menu" + | "MUD" + | "networks" + | "networks/icon-placeholder" + | "networks/logo-placeholder" + | "nft_shield" + | "open-link" + | "output_roots" + | "payment_link" + | "plus" + | "private_tags_slim" + | "privattags" + | "profile" + | "publictags_slim" + | "publictags" + | "qr_code" + | "refresh" + | "repeat" + | "restAPI" + | "rocket_xl" + | "rocket" + | "RPC" + | "scope" + | "score/score-not-ok" + | "score/score-ok" + | "search" + | "share" + | "sign_out" + | "social/coingecko" + | "social/coinmarketcap" + | "social/defi_llama" + | "social/discord_filled" + | "social/discord" + | "social/facebook_filled" + | "social/git" + | "social/github_filled" + | "social/linkedin_filled" + | "social/medium_filled" + | "social/opensea_filled" + | "social/reddit_filled" + | "social/slack_filled" + | "social/stats" + | "social/telega" + | "social/telegram_filled" + | "social/twitter_filled" + | "social/twitter" + | "star_filled" + | "star_outline" + | "stats" + | "status/error" + | "status/pending" + | "status/success" + | "status/warning" + | "sun" + | "swap" + | "testnet" + | "token-placeholder" + | "token-transfers" + | "token" + | "tokens" + | "tokens/xdai" + | "top-accounts" + | "transactions_slim" + | "transactions" + | "txn_batches_slim" + | "txn_batches" + | "uniswap" + | "user_op_slim" + | "user_op" + | "validator" + | "verification-steps/finalized" + | "verification-steps/unfinalized" + | "verified_slim" + | "verified" + | "wallet" + | "wallets/coinbase" + | "wallets/metamask" + | "wallets/token-pocket" + | "watchlist"; + \ No newline at end of file diff --git a/explorer/frontend/public/static/apple_calendar.svg b/explorer/frontend/public/static/apple_calendar.svg new file mode 100644 index 000000000..a55a0d5f3 --- /dev/null +++ b/explorer/frontend/public/static/apple_calendar.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/capibara/capybaraSprite.png b/explorer/frontend/public/static/capibara/capybaraSprite.png new file mode 100644 index 000000000..a5d6a07ce Binary files /dev/null and b/explorer/frontend/public/static/capibara/capybaraSprite.png differ diff --git a/explorer/frontend/public/static/capibara/capybaraSpriteX2.png b/explorer/frontend/public/static/capibara/capybaraSpriteX2.png new file mode 100644 index 000000000..0455a2997 Binary files /dev/null and b/explorer/frontend/public/static/capibara/capybaraSpriteX2.png differ diff --git a/explorer/frontend/public/static/capibara/index.js b/explorer/frontend/public/static/capibara/index.js new file mode 100644 index 000000000..00d5d13dc --- /dev/null +++ b/explorer/frontend/public/static/capibara/index.js @@ -0,0 +1,2648 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// extract from chromium source code by @liuwayong +(function () { + 'use strict'; + /** + * T-Rex runner. + * @param {string} outerContainerId Outer containing element id. + * @param {Object} opt_config + * @constructor + * @export + */ + function Runner(outerContainerId, opt_config) { + // Singleton + if (Runner.instance_) { + return Runner.instance_; + } + Runner.instance_ = this; + + this.outerContainerEl = document.querySelector(outerContainerId); + this.containerEl = null; + this.snackbarEl = null; + this.detailsButton = this.outerContainerEl.querySelector('#details-button'); + + this.config = opt_config || Runner.config; + + this.dimensions = Runner.defaultDimensions; + + this.canvas = null; + this.canvasCtx = null; + + this.tRex = null; + + this.distanceMeter = null; + this.distanceRan = 0; + + this.highestScore = 0; + + this.time = 0; + this.runningTime = 0; + this.msPerFrame = 1000 / FPS; + this.currentSpeed = this.config.SPEED; + + this.obstacles = []; + + this.activated = false; // Whether the easter egg has been activated. + this.playing = false; // Whether the game is currently in play state. + this.crashed = false; + this.paused = false; + this.inverted = false; + this.invertTimer = 0; + this.resizeTimerId_ = null; + + this.playCount = 0; + + // Sound FX. + // this.audioBuffer = null; + // this.soundFx = {}; + + // Global web audio context for playing sounds. + this.audioContext = null; + + // Images. + this.images = {}; + this.imagesLoaded = 0; + + this.highScoreReached = false; // Add this new flag + + if (this.isDisabled()) { + this.setupDisabledRunner(); + } else { + this.loadImages(); + } + } + window['Runner'] = Runner; + + + /** + * Default game width. + * @const + */ + var DEFAULT_WIDTH = 600; + + /** + * Frames per second. + * @const + */ + var FPS = 60; + + /** @const */ + var IS_HIDPI = 2; + + /** @const */ + var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform); + + /** @const */ + var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS; + + /** @const */ + var IS_TOUCH_ENABLED = 'ontouchstart' in window; + + /** + * Default game configuration. + * @enum {number} + */ + Runner.config = { + ACCELERATION: 0.001, + BG_CLOUD_SPEED: 0.2, + BOTTOM_PAD: 10, + CLEAR_TIME: 3000, + CLOUD_FREQUENCY: 0.5, + GAMEOVER_CLEAR_TIME: 750, + GAP_COEFFICIENT: 0.6, + GRAVITY: 0.6, + INITIAL_JUMP_VELOCITY: 12, + INVERT_FADE_DURATION: 12000, + INVERT_DISTANCE: 700, + MAX_CLOUDS: 6, + MAX_OBSTACLE_LENGTH: 3, + MAX_OBSTACLE_DUPLICATION: 2, + MAX_SPEED: 13, + MIN_JUMP_HEIGHT: 35, + MOBILE_SPEED_COEFFICIENT: 1.2, + RESOURCE_TEMPLATE_ID: 'audio-resources', + SPEED: 6, + SPEED_DROP_COEFFICIENT: 3 + }; + + + /** + * Default dimensions. + * @enum {string} + */ + Runner.defaultDimensions = { + WIDTH: DEFAULT_WIDTH, + HEIGHT: 150 + }; + + + /** + * CSS class names. + * @enum {string} + */ + Runner.classes = { + CANVAS: 'runner-canvas', + CONTAINER: 'runner-container', + CRASHED: 'crashed', + INVERTED: 'inverted', + SNACKBAR: 'snackbar', + SNACKBAR_SHOW: 'snackbar-show', + TOUCH_CONTROLLER: 'controller' + }; + + + /** + * Sprite definition layout of the spritesheet. + * @enum {Object} + */ + Runner.spriteDefinition = { + LDPI: { + CACTUS_LARGE: { x: 332, y: 2 }, + CACTUS_SMALL: { x: 228, y: 2 }, + CLOUD: { x: 86, y: 2 }, + HORIZON: { x: 2, y: 54 }, + MOON: { x: 484, y: 2 }, + RESTART: { x: 2, y: 2 }, + TEXT_SPRITE: { x: 655, y: 2 }, + TREX: { x: 848, y: 2 }, + STAR: { x: 645, y: 2 } + }, + HDPI: { + CACTUS_LARGE: { x: 652, y: 2 }, + CACTUS_SMALL: { x: 446, y: 2 }, + CLOUD: { x: 166, y: 2 }, + HORIZON: { x: 2, y: 104 }, + MOON: { x: 954, y: 2 }, + RESTART: { x: 2, y: 2 }, + TEXT_SPRITE: { x: 1292, y: 2 }, + TREX: { x: 1678, y: 2 }, + STAR: { x: 1276, y: 2 } + } + }; + + + /** + * Key code mapping. + * @enum {Object} + */ + Runner.keycodes = { + JUMP: { '38': 1, '32': 1 }, // Up, spacebar + DUCK: { '40': 1 }, // Down + RESTART: { '13': 1 } // Enter + }; + + + /** + * Runner event names. + * @enum {string} + */ + Runner.events = { + ANIM_END: 'webkitAnimationEnd', + CLICK: 'click', + KEYDOWN: 'keydown', + KEYUP: 'keyup', + MOUSEDOWN: 'mousedown', + MOUSEUP: 'mouseup', + RESIZE: 'resize', + TOUCHEND: 'touchend', + TOUCHSTART: 'touchstart', + VISIBILITY: 'visibilitychange', + BLUR: 'blur', + FOCUS: 'focus', + LOAD: 'load' + }; + + + Runner.prototype = { + /** + * Whether the easter egg has been disabled. CrOS enterprise enrolled devices. + * @return {boolean} + */ + isDisabled: function () { + // return loadTimeData && loadTimeData.valueExists('disabledEasterEgg'); + return false; + }, + + /** + * For disabled instances, set up a snackbar with the disabled message. + */ + setupDisabledRunner: function () { + this.containerEl = document.createElement('div'); + this.containerEl.className = Runner.classes.SNACKBAR; + this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg'); + this.outerContainerEl.appendChild(this.containerEl); + + // Show notification when the activation key is pressed. + document.addEventListener(Runner.events.KEYDOWN, function (e) { + if (Runner.keycodes.JUMP[e.keyCode]) { + this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW); + document.querySelector('.icon').classList.add('icon-disabled'); + } + }.bind(this)); + }, + + /** + * Setting individual settings for debugging. + * @param {string} setting + * @param {*} value + */ + updateConfigSetting: function (setting, value) { + if (setting in this.config && value != undefined) { + this.config[setting] = value; + + switch (setting) { + case 'GRAVITY': + case 'MIN_JUMP_HEIGHT': + case 'SPEED_DROP_COEFFICIENT': + this.tRex.config[setting] = value; + break; + case 'INITIAL_JUMP_VELOCITY': + this.tRex.setJumpVelocity(value); + break; + case 'SPEED': + this.setSpeed(value); + break; + } + } + }, + + /** + * Cache the appropriate image sprite from the page and get the sprite sheet + * definition. + */ + loadImages: function () { + if (IS_HIDPI) { + Runner.imageSprite = document.getElementById('offline-resources-2x'); + this.spriteDef = Runner.spriteDefinition.HDPI; + } else { + Runner.imageSprite = document.getElementById('offline-resources-2x'); + this.spriteDef = Runner.spriteDefinition.LDPI; + } + + if (Runner.imageSprite.complete) { + this.init(); + } else { + // If the images are not yet loaded, add a listener. + Runner.imageSprite.addEventListener(Runner.events.LOAD, + this.init.bind(this)); + } + }, + + /** + * Load and decode base 64 encoded sounds. + // */ + // loadSounds: function () { + // if (!IS_IOS) { + // this.audioContext = new AudioContext(); + + // var resourceTemplate = + // document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content; + + // for (var sound in Runner.sounds) { + // var soundSrc = + // resourceTemplate.getElementById(Runner.sounds[sound]).src; + // soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1); + // var buffer = decodeBase64ToArrayBuffer(soundSrc); + + // // Async, so no guarantee of order in array. + // this.audioContext.decodeAudioData(buffer, function (index, audioData) { + // this.soundFx[index] = audioData; + // }.bind(this, sound)); + // } + // } + // }, + + /** + * Sets the game speed. Adjust the speed accordingly if on a smaller screen. + * @param {number} opt_speed + */ + setSpeed: function (opt_speed) { + var speed = opt_speed || this.currentSpeed; + + // Reduce the speed on smaller mobile screens. + if (this.dimensions.WIDTH < DEFAULT_WIDTH) { + var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH * + this.config.MOBILE_SPEED_COEFFICIENT; + this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed; + } else if (opt_speed) { + this.currentSpeed = opt_speed; + } + }, + + /** + * Game initialiser. + */ + init: function () { + + this.adjustDimensions(); + this.setSpeed(); + + this.containerEl = document.createElement('div'); + this.containerEl.className = Runner.classes.CONTAINER; + + // Player canvas container. + this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH, + this.dimensions.HEIGHT, Runner.classes.PLAYER); + + this.canvasCtx = this.canvas.getContext('2d'); + this.canvasCtx.fillStyle = '#f7f7f7'; + this.canvasCtx.fill(); + Runner.updateCanvasScaling(this.canvas); + + // Horizon contains clouds, obstacles and the ground. + this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, + this.config.GAP_COEFFICIENT); + + // Distance meter + this.distanceMeter = new DistanceMeter(this.canvas, + this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH); + + // Draw t-rex + this.tRex = new Trex(this.canvas, this.spriteDef.TREX); + + this.outerContainerEl.appendChild(this.containerEl); + + if (IS_MOBILE) { + this.createTouchController(); + } + + this.startListening(); + this.update(); + + window.addEventListener(Runner.events.RESIZE, + this.debounceResize.bind(this)); + }, + + /** + * Create the touch controller. A div that covers whole screen. + */ + createTouchController: function () { + this.touchController = document.createElement('div'); + this.touchController.className = Runner.classes.TOUCH_CONTROLLER; + this.outerContainerEl.appendChild(this.touchController); + }, + + /** + * Debounce the resize event. + */ + debounceResize: function () { + if (!this.resizeTimerId_) { + this.resizeTimerId_ = + setInterval(this.adjustDimensions.bind(this), 250); + } + }, + + /** + * Adjust game space dimensions on resize. + */ + adjustDimensions: function () { + clearInterval(this.resizeTimerId_); + this.resizeTimerId_ = null; + + var boxStyles = window.getComputedStyle(this.outerContainerEl); + var padding = Number(boxStyles.paddingLeft.substr(0, + boxStyles.paddingLeft.length - 2)); + + this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2; + + // Redraw the elements back onto the canvas. + if (this.canvas) { + this.canvas.width = this.dimensions.WIDTH; + this.canvas.height = this.dimensions.HEIGHT; + + Runner.updateCanvasScaling(this.canvas); + + this.distanceMeter.calcXPos(this.dimensions.WIDTH); + this.clearCanvas(); + this.horizon.update(0, 0, true); + this.tRex.update(0); + + // Outer container and distance meter. + if (this.playing || this.crashed || this.paused) { + this.containerEl.style.width = this.dimensions.WIDTH + 'px'; + this.containerEl.style.height = this.dimensions.HEIGHT + 'px'; + this.distanceMeter.update(0, Math.ceil(this.distanceRan)); + this.stop(); + } else { + this.tRex.draw(0, 0); + } + + // Game over panel. + if (this.crashed && this.gameOverPanel) { + this.gameOverPanel.updateDimensions(this.dimensions.WIDTH); + this.gameOverPanel.draw(); + } + } + }, + + /** + * Play the game intro. + * Canvas container width expands out to the full width. + */ + playIntro: function () { + if (!this.activated && !this.crashed) { + this.playingIntro = true; + this.tRex.playingIntro = true; + + // CSS animation definition. + var keyframes = '@-webkit-keyframes intro { ' + + 'from { width:' + Trex.config.WIDTH + 'px }' + + 'to { width: ' + this.dimensions.WIDTH + 'px }' + + '}'; + + // create a style sheet to put the keyframe rule in + // and then place the style sheet in the html head + var sheet = document.createElement('style'); + sheet.innerHTML = keyframes; + document.head.appendChild(sheet); + + this.containerEl.addEventListener(Runner.events.ANIM_END, + this.startGame.bind(this)); + + this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both'; + this.containerEl.style.width = this.dimensions.WIDTH + 'px'; + + // if (this.touchController) { + // this.outerContainerEl.appendChild(this.touchController); + // } + this.playing = true; + this.activated = true; + } else if (this.crashed) { + this.restart(); + } + }, + + + /** + * Update the game status to started. + */ + startGame: function () { + this.runningTime = 0; + this.playingIntro = false; + this.tRex.playingIntro = false; + this.containerEl.style.webkitAnimation = ''; + this.playCount++; + + // Handle tabbing off the page. Pause the current game. + document.addEventListener(Runner.events.VISIBILITY, + this.onVisibilityChange.bind(this)); + + window.addEventListener(Runner.events.BLUR, + this.onVisibilityChange.bind(this)); + + window.addEventListener(Runner.events.FOCUS, + this.onVisibilityChange.bind(this)); + }, + + clearCanvas: function () { + this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH, + this.dimensions.HEIGHT); + }, + + /** + * Update the game frame and schedules the next one. + */ + update: function () { + this.updatePending = false; + + var now = getTimeStamp(); + var deltaTime = now - (this.time || now); + this.time = now; + + if (this.playing) { + this.clearCanvas(); + + if (this.tRex.jumping) { + this.tRex.updateJump(deltaTime); + } + + this.runningTime += deltaTime; + var hasObstacles = this.runningTime > this.config.CLEAR_TIME; + + // First jump triggers the intro. + if (this.tRex.jumpCount == 1 && !this.playingIntro) { + this.playIntro(); + } + + // The horizon doesn't move until the intro is over. + if (this.playingIntro) { + this.horizon.update(0, this.currentSpeed, hasObstacles); + } else { + deltaTime = !this.activated ? 0 : deltaTime; + this.horizon.update(deltaTime, this.currentSpeed, hasObstacles, + this.inverted); + } + + // Check for collisions. + var collision = hasObstacles && + checkForCollision(this.horizon.obstacles[0], this.tRex); + + if (!collision) { + this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame; + + if (!this.highScoreReached && this.distanceMeter.getActualDistance(this.distanceRan) >= 1000) { + this.highScoreReached = true; + window.dispatchEvent(new CustomEvent('reachedHighScore', { + detail: { score: this.distanceRan } + })); + } + + if (this.currentSpeed < this.config.MAX_SPEED) { + this.currentSpeed += this.config.ACCELERATION; + } + } else { + this.gameOver(); + } + + this.distanceMeter.update(deltaTime, + Math.ceil(this.distanceRan)); + + // Night mode. + if (this.invertTimer > this.config.INVERT_FADE_DURATION) { + this.invertTimer = 0; + this.invertTrigger = false; + this.invert(); + } else if (this.invertTimer) { + this.invertTimer += deltaTime; + } else { + var actualDistance = + this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan)); + + if (actualDistance > 0) { + this.invertTrigger = !(actualDistance % + this.config.INVERT_DISTANCE); + + if (this.invertTrigger && this.invertTimer === 0) { + this.invertTimer += deltaTime; + this.invert(); + } + } + } + } + + if (this.playing || !this.activated) { + this.tRex.update(deltaTime); + this.scheduleNextUpdate(); + } + }, + + /** + * Event handler. + */ + handleEvent: function (e) { + return (function (evtType, events) { + switch (evtType) { + case events.KEYDOWN: + case events.TOUCHSTART: + case events.MOUSEDOWN: + this.onKeyDown(e); + break; + case events.KEYUP: + case events.TOUCHEND: + case events.MOUSEUP: + this.onKeyUp(e); + break; + } + }.bind(this))(e.type, Runner.events); + }, + + /** + * Bind relevant key / mouse / touch listeners. + */ + startListening: function () { + // Keys. + document.addEventListener(Runner.events.KEYDOWN, this); + document.addEventListener(Runner.events.KEYUP, this); + + if (IS_MOBILE) { + // Mobile only touch devices. + this.touchController.addEventListener(Runner.events.TOUCHSTART, this); + this.touchController.addEventListener(Runner.events.TOUCHEND, this); + this.containerEl.addEventListener(Runner.events.TOUCHSTART, this); + } else { + // Mouse. + document.addEventListener(Runner.events.MOUSEDOWN, this); + document.addEventListener(Runner.events.MOUSEUP, this); + } + }, + + /** + * Remove all listeners. + */ + stopListening: function () { + document.removeEventListener(Runner.events.KEYDOWN, this); + document.removeEventListener(Runner.events.KEYUP, this); + + if (IS_MOBILE) { + this.touchController.removeEventListener(Runner.events.TOUCHSTART, this); + this.touchController.removeEventListener(Runner.events.TOUCHEND, this); + this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this); + } else { + document.removeEventListener(Runner.events.MOUSEDOWN, this); + document.removeEventListener(Runner.events.MOUSEUP, this); + } + }, + + /** + * Process keydown. + * @param {Event} e + */ + onKeyDown: function (e) { + // Prevent native page scrolling whilst tapping on mobile. + if (this.playing) { + e.preventDefault(); + } + + if (e.target != this.detailsButton) { + if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] || + e.type == Runner.events.TOUCHSTART)) { + if (!this.playing) { + // this.loadSounds(); + this.playing = true; + this.update(); + if (window.errorPageController) { + errorPageController.trackEasterEgg(); + } + } + // Play sound effect and jump on starting the game for the first time. + if (!this.tRex.jumping && !this.tRex.ducking) { + // this.playSound(this.soundFx.BUTTON_PRESS); + this.tRex.startJump(this.currentSpeed); + } + } + + if (this.crashed && e.type == Runner.events.TOUCHSTART && + e.currentTarget == this.containerEl) { + this.restart(); + } + } + + if (this.playing && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) { + e.preventDefault(); + if (this.tRex.jumping) { + // Speed drop, activated only when jump key is not pressed. + this.tRex.setSpeedDrop(); + } + } + }, + + + /** + * Process key up. + * @param {Event} e + */ + onKeyUp: function (e) { + var keyCode = String(e.keyCode); + var isjumpKey = Runner.keycodes.JUMP[keyCode] || + e.type == Runner.events.TOUCHEND || + e.type == Runner.events.MOUSEDOWN; + + if (this.isRunning() && isjumpKey) { + this.tRex.endJump(); + } else if (Runner.keycodes.DUCK[keyCode]) { + this.tRex.speedDrop = false; + } else if (this.crashed) { + // Check that enough time has elapsed before allowing jump key to restart. + var deltaTime = getTimeStamp() - this.time; + + if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) || + (deltaTime >= this.config.GAMEOVER_CLEAR_TIME && + Runner.keycodes.JUMP[keyCode])) { + this.restart(); + } + } else if (this.paused && isjumpKey) { + // Reset the jump state + this.tRex.reset(); + this.play(); + } + }, + + /** + * Returns whether the event was a left click on canvas. + * On Windows right click is registered as a click. + * @param {Event} e + * @return {boolean} + */ + isLeftClickOnCanvas: function (e) { + return e.button != null && e.button < 2 && + e.type == Runner.events.MOUSEUP && e.target == this.canvas; + }, + + /** + * RequestAnimationFrame wrapper. + */ + scheduleNextUpdate: function () { + if (!this.updatePending) { + this.updatePending = true; + this.raqId = requestAnimationFrame(this.update.bind(this)); + } + }, + + /** + * Whether the game is running. + * @return {boolean} + */ + isRunning: function () { + return !!this.raqId; + }, + + /** + * Game over state. + */ + gameOver: function () { + // this.playSound(this.soundFx.HIT); + vibrate(200); + + this.stop(); + this.crashed = true; + this.distanceMeter.acheivement = false; + + this.tRex.update(100, Trex.status.CRASHED); + + // Game over panel. + if (!this.gameOverPanel) { + this.gameOverPanel = new GameOverPanel(this.canvas, + this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART, + this.dimensions); + } else { + this.gameOverPanel.draw(); + } + + // Update the high score. + if (this.distanceRan > this.highestScore) { + this.highestScore = Math.ceil(this.distanceRan); + this.distanceMeter.setHighScore(this.highestScore); + } + + // Reset the time clock. + this.time = getTimeStamp(); + }, + + stop: function () { + this.playing = false; + this.paused = true; + cancelAnimationFrame(this.raqId); + this.raqId = 0; + }, + + play: function () { + if (!this.crashed) { + this.playing = true; + this.paused = false; + this.tRex.update(0, Trex.status.RUNNING); + this.time = getTimeStamp(); + this.update(); + } + }, + + restart: function () { + if (!this.raqId) { + this.playCount++; + this.runningTime = 0; + this.playing = true; + this.crashed = false; + this.distanceRan = 0; + this.setSpeed(this.config.SPEED); + this.time = getTimeStamp(); + this.containerEl.classList.remove(Runner.classes.CRASHED); + this.clearCanvas(); + this.distanceMeter.reset(this.highestScore); + this.horizon.reset(); + this.tRex.reset(); + // this.playSound(this.soundFx.BUTTON_PRESS); + this.invert(true); + this.update(); + } + }, + + /** + * Pause the game if the tab is not in focus. + */ + onVisibilityChange: function (e) { + if (document.hidden || document.webkitHidden || e.type == 'blur' || + document.visibilityState != 'visible') { + this.stop(); + } else if (!this.crashed) { + this.tRex.reset(); + this.play(); + } + }, + + // /** + // * Play a sound. + // * @param {SoundBuffer} soundBuffer + // */ + // playSound: function (soundBuffer) { + // if (soundBuffer) { + // var sourceNode = this.audioContext.createBufferSource(); + // sourceNode.buffer = soundBuffer; + // sourceNode.connect(this.audioContext.destination); + // sourceNode.start(0); + // } + // }, + + /** + * Inverts the current page / canvas colors. + * @param {boolean} Whether to reset colors. + */ + invert: function (reset) { + if (reset) { + document.body.classList.toggle(Runner.classes.INVERTED, false); + this.invertTimer = 0; + this.inverted = false; + } else { + this.inverted = document.body.classList.toggle(Runner.classes.INVERTED, + this.invertTrigger); + } + } + }; + + + /** + * Updates the canvas size taking into + * account the backing store pixel ratio and + * the device pixel ratio. + * + * See article by Paul Lewis: + * http://www.html5rocks.com/en/tutorials/canvas/hidpi/ + * + * @param {HTMLCanvasElement} canvas + * @param {number} opt_width + * @param {number} opt_height + * @return {boolean} Whether the canvas was scaled. + */ + Runner.updateCanvasScaling = function (canvas, opt_width, opt_height) { + var context = canvas.getContext('2d'); + + // Query the various pixel ratios + var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1; + var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1; + var ratio = devicePixelRatio / backingStoreRatio; + + // Upscale the canvas if the two ratios don't match + if (devicePixelRatio !== backingStoreRatio) { + var oldWidth = opt_width || canvas.width; + var oldHeight = opt_height || canvas.height; + + canvas.width = oldWidth * ratio; + canvas.height = oldHeight * ratio; + + canvas.style.width = oldWidth + 'px'; + canvas.style.height = oldHeight + 'px'; + + // Scale the context to counter the fact that we've manually scaled + // our canvas element. + context.scale(ratio, ratio); + return true; + } else if (devicePixelRatio == 1) { + // Reset the canvas width / height. Fixes scaling bug when the page is + // zoomed and the devicePixelRatio changes accordingly. + canvas.style.width = canvas.width + 'px'; + canvas.style.height = canvas.height + 'px'; + } + return false; + }; + + + /** + * Get random number. + * @param {number} min + * @param {number} max + * @param {number} + */ + function getRandomNum(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + + /** + * Vibrate on mobile devices. + * @param {number} duration Duration of the vibration in milliseconds. + */ + function vibrate(duration) { + if (IS_MOBILE && window.navigator.vibrate) { + window.navigator.vibrate(duration); + } + } + + + /** + * Create canvas element. + * @param {HTMLElement} container Element to append canvas to. + * @param {number} width + * @param {number} height + * @param {string} opt_classname + * @return {HTMLCanvasElement} + */ + function createCanvas(container, width, height, opt_classname) { + var canvas = document.createElement('canvas'); + canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' + + opt_classname : Runner.classes.CANVAS; + canvas.width = width; + canvas.height = height; + container.appendChild(canvas); + + return canvas; + } + + + /** + * Decodes the base 64 audio to ArrayBuffer used by Web Audio. + * @param {string} base64String + */ + function decodeBase64ToArrayBuffer(base64String) { + var len = (base64String.length / 4) * 3; + var str = atob(base64String); + var arrayBuffer = new ArrayBuffer(len); + var bytes = new Uint8Array(arrayBuffer); + + for (var i = 0; i < len; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; + } + + + /** + * Return the current timestamp. + * @return {number} + */ + function getTimeStamp() { + return IS_IOS ? new Date().getTime() : performance.now(); + } + + + //****************************************************************************** + + + /** + * Game over panel. + * @param {!HTMLCanvasElement} canvas + * @param {Object} textImgPos + * @param {Object} restartImgPos + * @param {!Object} dimensions Canvas dimensions. + * @constructor + */ + function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.canvasDimensions = dimensions; + this.textImgPos = textImgPos; + this.restartImgPos = restartImgPos; + this.draw(); + }; + + + /** + * Dimensions used in the panel. + * @enum {number} + */ + GameOverPanel.dimensions = { + TEXT_X: 0, + TEXT_Y: 13, + TEXT_WIDTH: 191, + TEXT_HEIGHT: 11, + RESTART_WIDTH: 36, + RESTART_HEIGHT: 32 + }; + + + GameOverPanel.prototype = { + /** + * Update the panel dimensions. + * @param {number} width New canvas width. + * @param {number} opt_height Optional new canvas height. + */ + updateDimensions: function (width, opt_height) { + this.canvasDimensions.WIDTH = width; + if (opt_height) { + this.canvasDimensions.HEIGHT = opt_height; + } + }, + + /** + * Draw the panel. + */ + draw: function () { + var dimensions = GameOverPanel.dimensions; + + var centerX = this.canvasDimensions.WIDTH / 2; + + // Game over text. + var textSourceX = dimensions.TEXT_X; + var textSourceY = dimensions.TEXT_Y; + var textSourceWidth = dimensions.TEXT_WIDTH; + var textSourceHeight = dimensions.TEXT_HEIGHT; + + var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2)); + var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3); + var textTargetWidth = dimensions.TEXT_WIDTH; + var textTargetHeight = dimensions.TEXT_HEIGHT; + + var restartSourceWidth = dimensions.RESTART_WIDTH; + var restartSourceHeight = dimensions.RESTART_HEIGHT; + var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2); + var restartTargetY = this.canvasDimensions.HEIGHT / 2; + + if (IS_HIDPI) { + textSourceY *= 2; + textSourceX *= 2; + textSourceWidth *= 2; + textSourceHeight *= 2; + restartSourceWidth *= 2; + restartSourceHeight *= 2; + } + + textSourceX += this.textImgPos.x; + textSourceY += this.textImgPos.y; + + // Game over text from sprite. + this.canvasCtx.drawImage(Runner.imageSprite, + textSourceX, textSourceY, textSourceWidth, textSourceHeight, + textTargetX, textTargetY, textTargetWidth, textTargetHeight); + + // Restart button. + this.canvasCtx.drawImage(Runner.imageSprite, + this.restartImgPos.x, this.restartImgPos.y, + restartSourceWidth, restartSourceHeight, + restartTargetX, restartTargetY, dimensions.RESTART_WIDTH, + dimensions.RESTART_HEIGHT); + } + }; + + + //****************************************************************************** + + /** + * Check for a collision. + * @param {!Obstacle} obstacle + * @param {!Trex} tRex T-rex object. + * @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing + * collision boxes. + * @return {Array} + */ + function checkForCollision(obstacle, tRex, opt_canvasCtx) { + var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos; + + // Adjustments are made to the bounding box as there is a 1 pixel white + // border around the t-rex and obstacles. + var tRexBox = new CollisionBox( + tRex.xPos + 1, + tRex.yPos + 1, + tRex.config.WIDTH - 2, + tRex.config.HEIGHT - 2); + + var obstacleBox = new CollisionBox( + obstacle.xPos + 1, + obstacle.yPos + 1, + obstacle.typeConfig.width * obstacle.size - 2, + obstacle.typeConfig.height - 2); + + // Debug outer box + if (opt_canvasCtx) { + drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox); + } + + // Simple outer bounds check. + if (boxCompare(tRexBox, obstacleBox)) { + var collisionBoxes = obstacle.collisionBoxes; + var tRexCollisionBoxes = Trex.collisionBoxes.RUNNING; + + // Detailed axis aligned box check. + for (var t = 0; t < tRexCollisionBoxes.length; t++) { + for (var i = 0; i < collisionBoxes.length; i++) { + // Adjust the box to actual positions. + var adjTrexBox = + createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox); + var adjObstacleBox = + createAdjustedCollisionBox(collisionBoxes[i], obstacleBox); + var crashed = boxCompare(adjTrexBox, adjObstacleBox); + + // Draw boxes for debug. + if (opt_canvasCtx) { + drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox); + } + + if (crashed) { + return [adjTrexBox, adjObstacleBox]; + } + } + } + } + return false; + }; + + + /** + * Adjust the collision box. + * @param {!CollisionBox} box The original box. + * @param {!CollisionBox} adjustment Adjustment box. + * @return {CollisionBox} The adjusted collision box object. + */ + function createAdjustedCollisionBox(box, adjustment) { + return new CollisionBox( + box.x + adjustment.x, + box.y + adjustment.y, + box.width, + box.height); + }; + + + /** + * Draw the collision boxes for debug. + */ + function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) { + canvasCtx.save(); + canvasCtx.strokeStyle = '#f00'; + canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height); + + canvasCtx.strokeStyle = '#0f0'; + canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y, + obstacleBox.width, obstacleBox.height); + canvasCtx.restore(); + }; + + + /** + * Compare two collision boxes for a collision. + * @param {CollisionBox} tRexBox + * @param {CollisionBox} obstacleBox + * @return {boolean} Whether the boxes intersected. + */ + function boxCompare(tRexBox, obstacleBox) { + var crashed = false; + var tRexBoxX = tRexBox.x; + var tRexBoxY = tRexBox.y; + + var obstacleBoxX = obstacleBox.x; + var obstacleBoxY = obstacleBox.y; + + // Axis-Aligned Bounding Box method. + if (tRexBox.x < obstacleBoxX + obstacleBox.width && + tRexBox.x + tRexBox.width > obstacleBoxX && + tRexBox.y < obstacleBox.y + obstacleBox.height && + tRexBox.height + tRexBox.y > obstacleBox.y) { + crashed = true; + } + + return crashed; + }; + + + //****************************************************************************** + + /** + * Collision box object. + * @param {number} x X position. + * @param {number} y Y Position. + * @param {number} w Width. + * @param {number} h Height. + */ + function CollisionBox(x, y, w, h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + }; + + + //****************************************************************************** + + /** + * Obstacle. + * @param {HTMLCanvasCtx} canvasCtx + * @param {Obstacle.type} type + * @param {Object} spritePos Obstacle position in sprite. + * @param {Object} dimensions + * @param {number} gapCoefficient Mutipler in determining the gap. + * @param {number} speed + * @param {number} opt_xOffset + */ + function Obstacle(canvasCtx, type, spriteImgPos, dimensions, + gapCoefficient, speed, opt_xOffset) { + + this.canvasCtx = canvasCtx; + this.spritePos = spriteImgPos; + this.typeConfig = type; + this.gapCoefficient = gapCoefficient; + this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH); + this.dimensions = dimensions; + this.remove = false; + this.xPos = dimensions.WIDTH + (opt_xOffset || 0); + this.yPos = 0; + this.width = 0; + this.collisionBoxes = []; + this.gap = 0; + this.speedOffset = 0; + + // For animated obstacles. + this.currentFrame = 0; + this.timer = 0; + + this.init(speed); + }; + + /** + * Coefficient for calculating the maximum gap. + * @const + */ + Obstacle.MAX_GAP_COEFFICIENT = 1.5; + + /** + * Maximum obstacle grouping count. + * @const + */ + Obstacle.MAX_OBSTACLE_LENGTH = 3, + + + Obstacle.prototype = { + /** + * Initialise the DOM for the obstacle. + * @param {number} speed + */ + init: function (speed) { + this.cloneCollisionBoxes(); + + // Only allow sizing if we're at the right speed. + if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { + this.size = 1; + } + + this.width = this.typeConfig.width * this.size; + + // Check if obstacle can be positioned at various heights. + if (Array.isArray(this.typeConfig.yPos)) { + var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile : + this.typeConfig.yPos; + this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; + } else { + this.yPos = this.typeConfig.yPos; + } + + this.draw(); + + // Make collision box adjustments, + // Central box is adjusted to the size as one box. + // ____ ______ ________ + // _| |-| _| |-| _| |-| + // | |<->| | | |<--->| | | |<----->| | + // | | 1 | | | | 2 | | | | 3 | | + // |_|___|_| |_|_____|_| |_|_______|_| + // + if (this.size > 1) { + this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width - + this.collisionBoxes[2].width; + this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width; + } + + // For obstacles that go at a different speed from the horizon. + if (this.typeConfig.speedOffset) { + this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset : + -this.typeConfig.speedOffset; + } + + this.gap = this.getGap(this.gapCoefficient, speed); + }, + + /** + * Draw and crop based on size. + */ + draw: function () { + var sourceWidth = this.typeConfig.width; + var sourceHeight = this.typeConfig.height; + + if (IS_HIDPI) { + sourceWidth = sourceWidth * 2; + sourceHeight = sourceHeight * 2; + } + + // X position in sprite. + var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) + + this.spritePos.x; + + // Animation frames. + if (this.currentFrame > 0) { + sourceX += sourceWidth * this.currentFrame; + } + + this.canvasCtx.drawImage(Runner.imageSprite, + sourceX, this.spritePos.y, + sourceWidth * this.size, sourceHeight, + this.xPos, this.yPos, + this.typeConfig.width * this.size, this.typeConfig.height); + }, + + /** + * Obstacle frame update. + * @param {number} deltaTime + * @param {number} speed + */ + update: function (deltaTime, speed) { + if (!this.remove) { + if (this.typeConfig.speedOffset) { + speed += this.speedOffset; + } + this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime); + + // Update frame + if (this.typeConfig.numFrames) { + this.timer += deltaTime; + if (this.timer >= this.typeConfig.frameRate) { + this.currentFrame = + this.currentFrame == this.typeConfig.numFrames - 1 ? + 0 : this.currentFrame + 1; + this.timer = 0; + } + } + this.draw(); + + if (!this.isVisible()) { + this.remove = true; + } + } + }, + + /** + * Calculate a random gap size. + * - Minimum gap gets wider as speed increses + * @param {number} gapCoefficient + * @param {number} speed + * @return {number} The gap size. + */ + getGap: function (gapCoefficient, speed) { + var minGap = Math.round(this.width * speed + + this.typeConfig.minGap * gapCoefficient); + var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); + return getRandomNum(minGap, maxGap); + }, + + /** + * Check if obstacle is visible. + * @return {boolean} Whether the obstacle is in the game area. + */ + isVisible: function () { + return this.xPos + this.width > 0; + }, + + /** + * Make a copy of the collision boxes, since these will change based on + * obstacle type and size. + */ + cloneCollisionBoxes: function () { + var collisionBoxes = this.typeConfig.collisionBoxes; + + for (var i = collisionBoxes.length - 1; i >= 0; i--) { + this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x, + collisionBoxes[i].y, collisionBoxes[i].width, + collisionBoxes[i].height); + } + } + }; + + + /** + * Obstacle definitions. + * minGap: minimum pixel space betweeen obstacles. + * multipleSpeed: Speed at which multiples are allowed. + * speedOffset: speed faster / slower than the horizon. + * minSpeed: Minimum speed which the obstacle can make an appearance. + */ + Obstacle.types = [ + { + type: 'CACTUS_SMALL', + width: 17, + height: 35, + yPos: 105, + multipleSpeed: 4, + minGap: 120, + minSpeed: 0, + collisionBoxes: [ + new CollisionBox(0, 7, 5, 27), + new CollisionBox(4, 0, 6, 34), + new CollisionBox(10, 4, 7, 14) + ] + }, + { + type: 'CACTUS_LARGE', + width: 25, + height: 50, + yPos: 90, + multipleSpeed: 7, + minGap: 120, + minSpeed: 0, + collisionBoxes: [ + new CollisionBox(0, 12, 7, 38), + new CollisionBox(8, 0, 7, 49), + new CollisionBox(13, 10, 10, 38) + ] + }, + ]; + + + //****************************************************************************** + /** + * T-rex game character. + * @param {HTMLCanvas} canvas + * @param {Object} spritePos Positioning within image sprite. + * @constructor + */ + function Trex(canvas, spritePos) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.spritePos = spritePos; + this.xPos = 0; + this.yPos = 0; + // Position when on the ground. + this.groundYPos = 0; + this.currentFrame = 0; + this.currentAnimFrames = []; + this.blinkDelay = 0; + this.blinkCount = 0; + this.animStartTime = 0; + this.timer = 0; + this.msPerFrame = 1000 / FPS; + this.config = Trex.config; + // Current status. + this.status = Trex.status.WAITING; + + this.jumping = false; + this.jumpVelocity = 0; + this.reachedMinHeight = false; + this.speedDrop = false; + this.jumpCount = 0; + this.jumpspotX = 0; + + this.init(); + }; + + + /** + * T-rex player config. + * @enum {number} + */ + Trex.config = { + DROP_VELOCITY: -5, + GRAVITY: 0.6, + HEIGHT: 47, + INIITAL_JUMP_VELOCITY: -10, + INTRO_DURATION: 1500, + MAX_JUMP_HEIGHT: 30, + MIN_JUMP_HEIGHT: 30, + SPEED_DROP_COEFFICIENT: 3, + SPRITE_WIDTH: 262, + START_X_POS: 50, + WIDTH: 42, + WIDTH_RUNNING: 50 + }; + + + /** + * Used in collision detection. + * @type {Array} + */ + Trex.collisionBoxes = { + RUNNING: [ + new CollisionBox(22, 0, 17, 16), + new CollisionBox(1, 18, 30, 9), + new CollisionBox(10, 35, 14, 8), + new CollisionBox(1, 24, 29, 5), + new CollisionBox(5, 30, 21, 4), + new CollisionBox(9, 34, 15, 4) + ] + }; + + + /** + * Animation states. + * @enum {string} + */ + Trex.status = { + CRASHED: 'CRASHED', + JUMPING: 'JUMPING', + RUNNING: 'RUNNING', + WAITING: 'WAITING' + }; + + /** + * Blinking coefficient. + * @const + */ + Trex.BLINK_TIMING = 500; + + + /** + * Animation config for different states. + * @enum {Object} + */ + Trex.animFrames = { + WAITING: { + frames: [43.5, 0], + msPerFrame: 1000 / 3 + }, + RUNNING: { + frames: [88, 140], + msPerFrame: 1000 / 12 + }, + CRASHED: { + frames: [190, 234], + msPerFrame: 1000 / 60 + }, + JUMPING: { + frames: [88], + msPerFrame: 1000 / 60 + }, + }; + + + Trex.prototype = { + /** + * T-rex player initaliser. + * Sets the t-rex to blink at random intervals. + */ + init: function () { + this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT - + Runner.config.BOTTOM_PAD; + this.yPos = this.groundYPos; + this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; + + this.draw(0, 0); + this.update(0, Trex.status.WAITING); + }, + + /** + * Setter for the jump velocity. + * The approriate drop velocity is also set. + */ + setJumpVelocity: function (setting) { + this.config.INIITAL_JUMP_VELOCITY = -setting; + this.config.DROP_VELOCITY = -setting / 2; + }, + + /** + * Set the animation status. + * @param {!number} deltaTime + * @param {Trex.status} status Optional status to switch to. + */ + update: function (deltaTime, opt_status) { + this.timer += deltaTime; + + // Update the status. + if (opt_status) { + this.status = opt_status; + this.currentFrame = 0; + this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; + this.currentAnimFrames = Trex.animFrames[opt_status].frames; + + if (opt_status == Trex.status.WAITING) { + this.animStartTime = getTimeStamp(); + this.setBlinkDelay(); + } + } + + // Game intro animation, T-rex moves in from the left. + if (this.playingIntro && this.xPos < this.config.START_X_POS) { + this.xPos += Math.round((this.config.START_X_POS / + this.config.INTRO_DURATION) * deltaTime); + } + + if (this.status == Trex.status.WAITING) { + this.blink(getTimeStamp()); + } else { + this.draw(this.currentAnimFrames[this.currentFrame], 0); + } + + // Update the frame position. + if (this.timer >= this.msPerFrame) { + this.currentFrame = this.currentFrame == + this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; + this.timer = 0; + } + + // Speed drop becomes duck if the down key is still being pressed. + if (this.speedDrop && this.yPos == this.groundYPos) { + this.speedDrop = false; + } + }, + + /** + * Draw the t-rex to a particular position. + * @param {number} x + * @param {number} y + */ + draw: function (x, y) { + var sourceX = x; + var sourceY = y; + var sourceWidth = this.status === Trex.status.RUNNING || this.status === Trex.status.JUMPING ? + this.config.WIDTH_RUNNING : this.config.WIDTH; + var sourceHeight = this.config.HEIGHT; + + if (IS_HIDPI) { + sourceX *= 2; + sourceY *= 2; + sourceWidth *= 2; + sourceHeight *= 2; + } + + // Adjustments for sprite sheet position. + sourceX += this.spritePos.x; + sourceY += this.spritePos.y; + + // Standing / running + this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + this.config.WIDTH, this.config.HEIGHT); + }, + + /** + * Sets a random time for the blink to happen. + */ + setBlinkDelay: function () { + this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING); + }, + + /** + * Make t-rex blink at random intervals. + * @param {number} time Current time in milliseconds. + */ + blink: function (time) { + var deltaTime = time - this.animStartTime; + + if (deltaTime >= this.blinkDelay) { + this.draw(this.currentAnimFrames[this.currentFrame], 0); + + if (this.currentFrame == 1) { + // Set new random delay to blink. + this.setBlinkDelay(); + this.animStartTime = time; + this.blinkCount++; + } + } + }, + + /** + * Initialise a jump. + * @param {number} speed + */ + startJump: function (speed) { + if (!this.jumping) { + this.update(0, Trex.status.JUMPING); + // Tweak the jump velocity based on the speed. + this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10); + this.jumping = true; + this.reachedMinHeight = false; + this.speedDrop = false; + } + }, + + /** + * Jump is complete, falling down. + */ + endJump: function () { + if (this.reachedMinHeight && + this.jumpVelocity < this.config.DROP_VELOCITY) { + this.jumpVelocity = this.config.DROP_VELOCITY; + } + }, + + /** + * Update frame for a jump. + * @param {number} deltaTime + * @param {number} speed + */ + updateJump: function (deltaTime, speed) { + var msPerFrame = Trex.animFrames[this.status].msPerFrame; + var framesElapsed = deltaTime / msPerFrame; + + // Speed drop makes Trex fall faster. + if (this.speedDrop) { + this.yPos += Math.round(this.jumpVelocity * + this.config.SPEED_DROP_COEFFICIENT * framesElapsed); + } else { + this.yPos += Math.round(this.jumpVelocity * framesElapsed); + } + + this.jumpVelocity += this.config.GRAVITY * framesElapsed; + + // Minimum height has been reached. + if (this.yPos < this.minJumpHeight || this.speedDrop) { + this.reachedMinHeight = true; + } + + // Reached max height + if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) { + this.endJump(); + } + + // Back down at ground level. Jump completed. + if (this.yPos > this.groundYPos) { + this.reset(); + this.jumpCount++; + } + + this.update(deltaTime); + }, + + /** + * Set the speed drop. Immediately cancels the current jump. + */ + setSpeedDrop: function () { + this.speedDrop = true; + this.jumpVelocity = 1; + }, + + /** + * @param {boolean} isDucking. + */ + setDuck: function (isDucking) { + if (isDucking && this.status != Trex.status.DUCKING) { + this.update(0, Trex.status.DUCKING); + this.ducking = true; + } else if (this.status == Trex.status.DUCKING) { + this.update(0, Trex.status.RUNNING); + this.ducking = false; + } + }, + + /** + * Reset the t-rex to running at start of game. + */ + reset: function () { + this.yPos = this.groundYPos; + this.jumpVelocity = 0; + this.jumping = false; + this.ducking = false; + this.update(0, Trex.status.RUNNING); + this.midair = false; + this.speedDrop = false; + this.jumpCount = 0; + this.highScoreReached = false; + } + }; + + + //****************************************************************************** + + /** + * Handles displaying the distance meter. + * @param {!HTMLCanvasElement} canvas + * @param {Object} spritePos Image position in sprite. + * @param {number} canvasWidth + * @constructor + */ + function DistanceMeter(canvas, spritePos, canvasWidth) { + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.image = Runner.imageSprite; + this.spritePos = spritePos; + this.x = 0; + this.y = 5; + + this.currentDistance = 0; + this.maxScore = 0; + this.highScore = 0; + this.container = null; + + this.digits = []; + this.acheivement = false; + this.defaultString = ''; + this.flashTimer = 0; + this.flashIterations = 0; + this.invertTrigger = false; + + this.config = DistanceMeter.config; + this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; + this.init(canvasWidth); + }; + + + /** + * @enum {number} + */ + DistanceMeter.dimensions = { + WIDTH: 10, + HEIGHT: 12, + DEST_WIDTH: 11 + }; + + + /** + * Y positioning of the digits in the sprite sheet. + * X position is always 0. + * @type {Array} + */ + DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120]; + + + /** + * Distance meter config. + * @enum {number} + */ + DistanceMeter.config = { + // Number of digits. + MAX_DISTANCE_UNITS: 5, + + // Distance that causes achievement animation. + ACHIEVEMENT_DISTANCE: 100, + + // Used for conversion from pixel distance to a scaled unit. + COEFFICIENT: 0.025, + + // Flash duration in milliseconds. + FLASH_DURATION: 1000 / 4, + + // Flash iterations for achievement animation. + FLASH_ITERATIONS: 3 + }; + + + DistanceMeter.prototype = { + /** + * Initialise the distance meter to '00000'. + * @param {number} width Canvas width in px. + */ + init: function (width) { + var maxDistanceStr = ''; + + this.calcXPos(width); + this.maxScore = this.maxScoreUnits; + for (var i = 0; i < this.maxScoreUnits; i++) { + this.draw(i, 0); + this.defaultString += '0'; + maxDistanceStr += '9'; + } + + this.maxScore = parseInt(maxDistanceStr); + }, + + /** + * Calculate the xPos in the canvas. + * @param {number} canvasWidth + */ + calcXPos: function (canvasWidth) { + this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH * + (this.maxScoreUnits + 1)); + }, + + /** + * Draw a digit to canvas. + * @param {number} digitPos Position of the digit. + * @param {number} value Digit value 0-9. + * @param {boolean} opt_highScore Whether drawing the high score. + */ + draw: function (digitPos, value, opt_highScore) { + var sourceWidth = DistanceMeter.dimensions.WIDTH; + var sourceHeight = DistanceMeter.dimensions.HEIGHT; + var sourceX = DistanceMeter.dimensions.WIDTH * value; + var sourceY = 0; + + var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH; + var targetY = this.y; + var targetWidth = DistanceMeter.dimensions.WIDTH; + var targetHeight = DistanceMeter.dimensions.HEIGHT; + + // For high DPI we 2x source values. + if (IS_HIDPI) { + sourceWidth *= 2; + sourceHeight *= 2; + sourceX *= 2; + } + + sourceX += this.spritePos.x; + sourceY += this.spritePos.y; + + this.canvasCtx.save(); + + if (opt_highScore) { + // Left of the current score. + var highScoreX = this.x - (this.maxScoreUnits * 2) * + DistanceMeter.dimensions.WIDTH; + this.canvasCtx.translate(highScoreX, this.y); + } else { + this.canvasCtx.translate(this.x, this.y); + } + + this.canvasCtx.drawImage(this.image, sourceX, sourceY, + sourceWidth, sourceHeight, + targetX, targetY, + targetWidth, targetHeight + ); + + this.canvasCtx.restore(); + }, + + /** + * Covert pixel distance to a 'real' distance. + * @param {number} distance Pixel distance ran. + * @return {number} The 'real' distance ran. + */ + getActualDistance: function (distance) { + return distance ? Math.round(distance * this.config.COEFFICIENT) : 0; + }, + + /** + * Update the distance meter. + * @param {number} distance + * @param {number} deltaTime + * @return {boolean} Whether the acheivement sound fx should be played. + */ + update: function (deltaTime, distance) { + var paint = true; + + if (!this.acheivement) { + distance = this.getActualDistance(distance); + // Score has gone beyond the initial digit count. + if (distance > this.maxScore && this.maxScoreUnits == + this.config.MAX_DISTANCE_UNITS) { + this.maxScoreUnits++; + this.maxScore = parseInt(this.maxScore + '9'); + } else { + this.distance = 0; + } + + if (distance > 0) { + // Acheivement unlocked + if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) { + // Flash score and play sound. + this.acheivement = true; + this.flashTimer = 0; + // playSound = true; + } + + // Create a string representation of the distance with leading 0. + var distanceStr = (this.defaultString + + distance).substr(-this.maxScoreUnits); + this.digits = distanceStr.split(''); + } else { + this.digits = this.defaultString.split(''); + } + } else { + // Control flashing of the score on reaching acheivement. + if (this.flashIterations <= this.config.FLASH_ITERATIONS) { + this.flashTimer += deltaTime; + + if (this.flashTimer < this.config.FLASH_DURATION) { + paint = false; + } else if (this.flashTimer > + this.config.FLASH_DURATION * 2) { + this.flashTimer = 0; + this.flashIterations++; + } + } else { + this.acheivement = false; + this.flashIterations = 0; + this.flashTimer = 0; + } + } + + // Draw the digits if not flashing. + if (paint) { + for (var i = this.digits.length - 1; i >= 0; i--) { + this.draw(i, parseInt(this.digits[i])); + } + } + + this.drawHighScore(); + return false; + }, + + /** + * Draw the high score. + */ + drawHighScore: function () { + this.canvasCtx.save(); + this.canvasCtx.globalAlpha = .8; + for (var i = this.highScore.length - 1; i >= 0; i--) { + this.draw(i, parseInt(this.highScore[i], 10), true); + } + this.canvasCtx.restore(); + }, + + /** + * Set the highscore as a array string. + * Position of char in the sprite: H - 10, I - 11. + * @param {number} distance Distance ran in pixels. + */ + setHighScore: function (distance) { + distance = this.getActualDistance(distance); + var highScoreStr = (this.defaultString + + distance).substr(-this.maxScoreUnits); + + this.highScore = ['10', '11', ''].concat(highScoreStr.split('')); + }, + + /** + * Reset the distance meter back to '00000'. + */ + reset: function () { + this.update(0); + this.acheivement = false; + } + }; + + + //****************************************************************************** + + /** + * Cloud background item. + * Similar to an obstacle object but without collision boxes. + * @param {HTMLCanvasElement} canvas Canvas element. + * @param {Object} spritePos Position of image in sprite. + * @param {number} containerWidth + */ + function Cloud(canvas, spritePos, containerWidth) { + this.canvas = canvas; + this.canvasCtx = this.canvas.getContext('2d'); + this.spritePos = spritePos; + this.containerWidth = containerWidth; + this.xPos = containerWidth; + this.yPos = 0; + this.remove = false; + this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP, + Cloud.config.MAX_CLOUD_GAP); + + this.init(); + }; + + + /** + * Cloud object config. + * @enum {number} + */ + Cloud.config = { + HEIGHT: 14, + MAX_CLOUD_GAP: 400, + MAX_SKY_LEVEL: 30, + MIN_CLOUD_GAP: 100, + MIN_SKY_LEVEL: 71, + WIDTH: 46 + }; + + + Cloud.prototype = { + /** + * Initialise the cloud. Sets the Cloud height. + */ + init: function () { + this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL, + Cloud.config.MIN_SKY_LEVEL); + this.draw(); + }, + + /** + * Draw the cloud. + */ + draw: function () { + this.canvasCtx.save(); + var sourceWidth = Cloud.config.WIDTH; + var sourceHeight = Cloud.config.HEIGHT; + + if (IS_HIDPI) { + sourceWidth = sourceWidth * 2; + sourceHeight = sourceHeight * 2; + } + + this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x, + this.spritePos.y, + sourceWidth, sourceHeight, + this.xPos, this.yPos, + Cloud.config.WIDTH, Cloud.config.HEIGHT); + + this.canvasCtx.restore(); + }, + + /** + * Update the cloud position. + * @param {number} speed + */ + update: function (speed) { + if (!this.remove) { + this.xPos -= Math.ceil(speed); + this.draw(); + + // Mark as removeable if no longer in the canvas. + if (!this.isVisible()) { + this.remove = true; + } + } + }, + + /** + * Check if the cloud is visible on the stage. + * @return {boolean} + */ + isVisible: function () { + return this.xPos + Cloud.config.WIDTH > 0; + } + }; + + + //****************************************************************************** + + /** + * Nightmode shows a moon and stars on the horizon. + */ + function NightMode(canvas, spritePos, containerWidth) { + this.spritePos = spritePos; + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.xPos = containerWidth - 50; + this.yPos = 30; + this.currentPhase = 0; + this.opacity = 0; + this.containerWidth = containerWidth; + this.stars = []; + this.drawStars = false; + this.placeStars(); + }; + + /** + * @enum {number} + */ + NightMode.config = { + FADE_SPEED: 0.035, + HEIGHT: 40, + MOON_SPEED: 0.25, + NUM_STARS: 2, + STAR_SIZE: 9, + STAR_SPEED: 0.3, + STAR_MAX_Y: 70, + WIDTH: 20 + }; + + NightMode.phases = [140, 120, 100, 60, 40, 20, 0]; + + NightMode.prototype = { + /** + * Update moving moon, changing phases. + * @param {boolean} activated Whether night mode is activated. + * @param {number} delta + */ + update: function (activated, delta) { + // Moon phase. + if (activated && this.opacity == 0) { + this.currentPhase++; + + if (this.currentPhase >= NightMode.phases.length) { + this.currentPhase = 0; + } + } + + // Fade in / out. + if (activated && (this.opacity < 1 || this.opacity == 0)) { + this.opacity += NightMode.config.FADE_SPEED; + } else if (this.opacity > 0) { + this.opacity -= NightMode.config.FADE_SPEED; + } + + // Set moon positioning. + if (this.opacity > 0) { + this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED); + + // Update stars. + if (this.drawStars) { + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i].x = this.updateXPos(this.stars[i].x, + NightMode.config.STAR_SPEED); + } + } + this.draw(); + } else { + this.opacity = 0; + this.placeStars(); + } + this.drawStars = true; + }, + + updateXPos: function (currentPos, speed) { + if (currentPos < -NightMode.config.WIDTH) { + currentPos = this.containerWidth; + } else { + currentPos -= speed; + } + return currentPos; + }, + + draw: function () { + var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 : + NightMode.config.WIDTH; + var moonSourceHeight = NightMode.config.HEIGHT; + var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase]; + var moonOutputWidth = moonSourceWidth; + var starSize = NightMode.config.STAR_SIZE; + var starSourceX = Runner.spriteDefinition.LDPI.STAR.x; + + if (IS_HIDPI) { + moonSourceWidth *= 2; + moonSourceHeight *= 2; + moonSourceX = this.spritePos.x + + (NightMode.phases[this.currentPhase] * 2); + starSize *= 2; + starSourceX = Runner.spriteDefinition.HDPI.STAR.x; + } + + this.canvasCtx.save(); + this.canvasCtx.globalAlpha = this.opacity; + + // Stars. + if (this.drawStars) { + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.canvasCtx.drawImage(Runner.imageSprite, + starSourceX, this.stars[i].sourceY, starSize, starSize, + Math.round(this.stars[i].x), this.stars[i].y, + NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE); + } + } + + // Moon. + this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX, + this.spritePos.y, moonSourceWidth, moonSourceHeight, + Math.round(this.xPos), this.yPos, + moonOutputWidth, NightMode.config.HEIGHT); + + this.canvasCtx.globalAlpha = 1; + this.canvasCtx.restore(); + }, + + // Do star placement. + placeStars: function () { + var segmentSize = Math.round(this.containerWidth / + NightMode.config.NUM_STARS); + + for (var i = 0; i < NightMode.config.NUM_STARS; i++) { + this.stars[i] = {}; + this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1)); + this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y); + + if (IS_HIDPI) { + this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y + + NightMode.config.STAR_SIZE * 2 * i; + } else { + this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y + + NightMode.config.STAR_SIZE * i; + } + } + }, + + reset: function () { + this.currentPhase = 0; + this.opacity = 0; + this.update(false); + } + + }; + + + //****************************************************************************** + + /** + * Horizon Line. + * Consists of two connecting lines. Randomly assigns a flat / bumpy horizon. + * @param {HTMLCanvasElement} canvas + * @param {Object} spritePos Horizon position in sprite. + * @constructor + */ + function HorizonLine(canvas, spritePos) { + this.spritePos = spritePos; + this.canvas = canvas; + this.canvasCtx = canvas.getContext('2d'); + this.sourceDimensions = {}; + this.dimensions = HorizonLine.dimensions; + this.sourceXPos = [this.spritePos.x, this.spritePos.x + + this.dimensions.WIDTH]; + this.xPos = []; + this.yPos = 0; + this.bumpThreshold = 0.5; + + this.setSourceDimensions(); + this.draw(); + }; + + + /** + * Horizon line dimensions. + * @enum {number} + */ + HorizonLine.dimensions = { + WIDTH: 600, + HEIGHT: 12, + YPOS: 127 + }; + + + HorizonLine.prototype = { + /** + * Set the source dimensions of the horizon line. + */ + setSourceDimensions: function () { + + for (var dimension in HorizonLine.dimensions) { + if (IS_HIDPI) { + if (dimension != 'YPOS') { + this.sourceDimensions[dimension] = + HorizonLine.dimensions[dimension] * 2; + } + } else { + this.sourceDimensions[dimension] = + HorizonLine.dimensions[dimension]; + } + this.dimensions[dimension] = HorizonLine.dimensions[dimension]; + } + + this.xPos = [0, HorizonLine.dimensions.WIDTH]; + this.yPos = HorizonLine.dimensions.YPOS; + }, + + /** + * Return the crop x position of a type. + */ + getRandomType: function () { + return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0; + }, + + /** + * Draw the horizon line. + */ + draw: function () { + this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0], + this.spritePos.y, + this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, + this.xPos[0], this.yPos, + this.dimensions.WIDTH, this.dimensions.HEIGHT); + + this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1], + this.spritePos.y, + this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT, + this.xPos[1], this.yPos, + this.dimensions.WIDTH, this.dimensions.HEIGHT); + }, + + /** + * Update the x position of an indivdual piece of the line. + * @param {number} pos Line position. + * @param {number} increment + */ + updateXPos: function (pos, increment) { + var line1 = pos; + var line2 = pos == 0 ? 1 : 0; + + this.xPos[line1] -= increment; + this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; + + if (this.xPos[line1] <= -this.dimensions.WIDTH) { + this.xPos[line1] += this.dimensions.WIDTH * 2; + this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; + this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; + } + }, + + /** + * Update the horizon line. + * @param {number} deltaTime + * @param {number} speed + */ + update: function (deltaTime, speed) { + var increment = Math.floor(speed * (FPS / 1000) * deltaTime); + + if (this.xPos[0] <= 0) { + this.updateXPos(0, increment); + } else { + this.updateXPos(1, increment); + } + this.draw(); + }, + + /** + * Reset horizon to the starting position. + */ + reset: function () { + this.xPos[0] = 0; + this.xPos[1] = HorizonLine.dimensions.WIDTH; + } + }; + + + //****************************************************************************** + + /** + * Horizon background class. + * @param {HTMLCanvasElement} canvas + * @param {Object} spritePos Sprite positioning. + * @param {Object} dimensions Canvas dimensions. + * @param {number} gapCoefficient + * @constructor + */ + function Horizon(canvas, spritePos, dimensions, gapCoefficient) { + this.canvas = canvas; + this.canvasCtx = this.canvas.getContext('2d'); + this.config = Horizon.config; + this.dimensions = dimensions; + this.gapCoefficient = gapCoefficient; + this.obstacles = []; + this.obstacleHistory = []; + this.horizonOffsets = [0, 0]; + this.cloudFrequency = this.config.CLOUD_FREQUENCY; + this.spritePos = spritePos; + this.nightMode = null; + + // Cloud + this.clouds = []; + this.cloudSpeed = this.config.BG_CLOUD_SPEED; + + // Horizon + this.horizonLine = null; + this.init(); + }; + + + /** + * Horizon config. + * @enum {number} + */ + Horizon.config = { + BG_CLOUD_SPEED: 0.2, + BUMPY_THRESHOLD: .3, + CLOUD_FREQUENCY: .5, + HORIZON_HEIGHT: 16, + MAX_CLOUDS: 6 + }; + + + Horizon.prototype = { + /** + * Initialise the horizon. Just add the line and a cloud. No obstacles. + */ + init: function () { + this.addCloud(); + this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON); + this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, + this.dimensions.WIDTH); + }, + + /** + * @param {number} deltaTime + * @param {number} currentSpeed + * @param {boolean} updateObstacles Used as an override to prevent + * the obstacles from being updated / added. This happens in the + * ease in section. + * @param {boolean} showNightMode Night mode activated. + */ + update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) { + this.runningTime += deltaTime; + this.horizonLine.update(deltaTime, currentSpeed); + this.nightMode.update(showNightMode); + this.updateClouds(deltaTime, currentSpeed); + + if (updateObstacles) { + this.updateObstacles(deltaTime, currentSpeed); + } + }, + + /** + * Update the cloud positions. + * @param {number} deltaTime + * @param {number} currentSpeed + */ + updateClouds: function (deltaTime, speed) { + var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed; + var numClouds = this.clouds.length; + + if (numClouds) { + for (var i = numClouds - 1; i >= 0; i--) { + this.clouds[i].update(cloudSpeed); + } + + var lastCloud = this.clouds[numClouds - 1]; + + // Check for adding a new cloud. + if (numClouds < this.config.MAX_CLOUDS && + (this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap && + this.cloudFrequency > Math.random()) { + this.addCloud(); + } + + // Remove expired clouds. + this.clouds = this.clouds.filter(function (obj) { + return !obj.remove; + }); + } else { + this.addCloud(); + } + }, + + /** + * Update the obstacle positions. + * @param {number} deltaTime + * @param {number} currentSpeed + */ + updateObstacles: function (deltaTime, currentSpeed) { + // Obstacles, move to Horizon layer. + var updatedObstacles = this.obstacles.slice(0); + + for (var i = 0; i < this.obstacles.length; i++) { + var obstacle = this.obstacles[i]; + obstacle.update(deltaTime, currentSpeed); + + // Clean up existing obstacles. + if (obstacle.remove) { + updatedObstacles.shift(); + } + } + this.obstacles = updatedObstacles; + + if (this.obstacles.length > 0) { + var lastObstacle = this.obstacles[this.obstacles.length - 1]; + + if (lastObstacle && !lastObstacle.followingObstacleCreated && + lastObstacle.isVisible() && + (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) < + this.dimensions.WIDTH) { + this.addNewObstacle(currentSpeed); + lastObstacle.followingObstacleCreated = true; + } + } else { + // Create new obstacles. + this.addNewObstacle(currentSpeed); + } + }, + + removeFirstObstacle: function () { + this.obstacles.shift(); + }, + + /** + * Add a new obstacle. + * @param {number} currentSpeed + */ + addNewObstacle: function (currentSpeed) { + var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1); + var obstacleType = Obstacle.types[obstacleTypeIndex]; + + // Check for multiples of the same type of obstacle. + // Also check obstacle is available at current speed. + if (this.duplicateObstacleCheck(obstacleType.type) || + currentSpeed < obstacleType.minSpeed) { + this.addNewObstacle(currentSpeed); + } else { + var obstacleSpritePos = this.spritePos[obstacleType.type]; + + this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType, + obstacleSpritePos, this.dimensions, + this.gapCoefficient, currentSpeed, obstacleType.width)); + + this.obstacleHistory.unshift(obstacleType.type); + + if (this.obstacleHistory.length > 1) { + this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION); + } + } + }, + + /** + * Returns whether the previous two obstacles are the same as the next one. + * Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION. + * @return {boolean} + */ + duplicateObstacleCheck: function (nextObstacleType) { + var duplicateCount = 0; + + for (var i = 0; i < this.obstacleHistory.length; i++) { + duplicateCount = this.obstacleHistory[i] == nextObstacleType ? + duplicateCount + 1 : 0; + } + return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION; + }, + + /** + * Reset the horizon layer. + * Remove existing obstacles and reposition the horizon line. + */ + reset: function () { + this.obstacles = []; + this.horizonLine.reset(); + this.nightMode.reset(); + }, + + /** + * Update the canvas width and scaling. + * @param {number} width Canvas width. + * @param {number} height Canvas height. + */ + resize: function (width, height) { + this.canvas.width = width; + this.canvas.height = height; + }, + + /** + * Add a new cloud to the horizon. + */ + addCloud: function () { + this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD, + this.dimensions.WIDTH)); + } + }; +})(); + +new Runner('.interstitial-wrapper'); \ No newline at end of file diff --git a/explorer/frontend/public/static/contract_star.png b/explorer/frontend/public/static/contract_star.png new file mode 100644 index 000000000..32d635ef6 Binary files /dev/null and b/explorer/frontend/public/static/contract_star.png differ diff --git a/explorer/frontend/public/static/gas_hawk_logo.svg b/explorer/frontend/public/static/gas_hawk_logo.svg new file mode 100644 index 000000000..ffa6acdde --- /dev/null +++ b/explorer/frontend/public/static/gas_hawk_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/gecko_terminal.png b/explorer/frontend/public/static/gecko_terminal.png new file mode 100644 index 000000000..e64f92c43 Binary files /dev/null and b/explorer/frontend/public/static/gecko_terminal.png differ diff --git a/explorer/frontend/public/static/google_calendar.svg b/explorer/frontend/public/static/google_calendar.svg new file mode 100644 index 000000000..7bd127e92 --- /dev/null +++ b/explorer/frontend/public/static/google_calendar.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/identicon_logos/blockies.png b/explorer/frontend/public/static/identicon_logos/blockies.png new file mode 100644 index 000000000..d9ccead35 Binary files /dev/null and b/explorer/frontend/public/static/identicon_logos/blockies.png differ diff --git a/explorer/frontend/public/static/identicon_logos/github.png b/explorer/frontend/public/static/identicon_logos/github.png new file mode 100644 index 000000000..346f9d212 Binary files /dev/null and b/explorer/frontend/public/static/identicon_logos/github.png differ diff --git a/explorer/frontend/public/static/identicon_logos/gradient_avatar.png b/explorer/frontend/public/static/identicon_logos/gradient_avatar.png new file mode 100644 index 000000000..396ee2784 Binary files /dev/null and b/explorer/frontend/public/static/identicon_logos/gradient_avatar.png differ diff --git a/explorer/frontend/public/static/identicon_logos/jazzicon.png b/explorer/frontend/public/static/identicon_logos/jazzicon.png new file mode 100644 index 000000000..c06bc880c Binary files /dev/null and b/explorer/frontend/public/static/identicon_logos/jazzicon.png differ diff --git a/explorer/frontend/public/static/identicon_logos/nouns.svg b/explorer/frontend/public/static/identicon_logos/nouns.svg new file mode 100644 index 000000000..8f14010f9 --- /dev/null +++ b/explorer/frontend/public/static/identicon_logos/nouns.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/merits/activity_pass.svg b/explorer/frontend/public/static/merits/activity_pass.svg new file mode 100644 index 000000000..5a07fce84 --- /dev/null +++ b/explorer/frontend/public/static/merits/activity_pass.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/merits/badges.svg b/explorer/frontend/public/static/merits/badges.svg new file mode 100644 index 000000000..d2d93f457 --- /dev/null +++ b/explorer/frontend/public/static/merits/badges.svg @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/merits/campaigns.svg b/explorer/frontend/public/static/merits/campaigns.svg new file mode 100644 index 000000000..efbd2064c --- /dev/null +++ b/explorer/frontend/public/static/merits/campaigns.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/merits/cells.svg b/explorer/frontend/public/static/merits/cells.svg new file mode 100644 index 000000000..85adb070c --- /dev/null +++ b/explorer/frontend/public/static/merits/cells.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/merits/cells_dark.svg b/explorer/frontend/public/static/merits/cells_dark.svg new file mode 100644 index 000000000..35c24ec26 --- /dev/null +++ b/explorer/frontend/public/static/merits/cells_dark.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/merits/merits_program.png b/explorer/frontend/public/static/merits/merits_program.png new file mode 100644 index 000000000..8e5dee055 Binary files /dev/null and b/explorer/frontend/public/static/merits/merits_program.png differ diff --git a/explorer/frontend/public/static/merits/offers.svg b/explorer/frontend/public/static/merits/offers.svg new file mode 100644 index 000000000..7f7f21845 --- /dev/null +++ b/explorer/frontend/public/static/merits/offers.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/noves-logo-dark.svg b/explorer/frontend/public/static/noves-logo-dark.svg new file mode 100644 index 000000000..a35d7754e --- /dev/null +++ b/explorer/frontend/public/static/noves-logo-dark.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/noves-logo.svg b/explorer/frontend/public/static/noves-logo.svg new file mode 100644 index 000000000..f91f0fa6f --- /dev/null +++ b/explorer/frontend/public/static/noves-logo.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer/frontend/public/static/og_placeholder.png b/explorer/frontend/public/static/og_placeholder.png new file mode 100644 index 000000000..1babd9e52 Binary files /dev/null and b/explorer/frontend/public/static/og_placeholder.png differ diff --git a/explorer/frontend/public/static/resizer.png b/explorer/frontend/public/static/resizer.png new file mode 100644 index 000000000..4a894f2e2 Binary files /dev/null and b/explorer/frontend/public/static/resizer.png differ diff --git a/explorer/frontend/public/static/resizer_dark.png b/explorer/frontend/public/static/resizer_dark.png new file mode 100644 index 000000000..b8ab11d65 Binary files /dev/null and b/explorer/frontend/public/static/resizer_dark.png differ diff --git a/explorer/frontend/public/static/resizer_light.png b/explorer/frontend/public/static/resizer_light.png new file mode 100644 index 000000000..d6bce7954 Binary files /dev/null and b/explorer/frontend/public/static/resizer_light.png differ diff --git a/explorer/frontend/reset.d.ts b/explorer/frontend/reset.d.ts new file mode 100644 index 000000000..12bd3edc9 --- /dev/null +++ b/explorer/frontend/reset.d.ts @@ -0,0 +1 @@ +import '@total-typescript/ts-reset'; diff --git a/explorer/frontend/stubs/ENS.ts b/explorer/frontend/stubs/ENS.ts new file mode 100644 index 000000000..a35773f07 --- /dev/null +++ b/explorer/frontend/stubs/ENS.ts @@ -0,0 +1,34 @@ +import * as bens from '@blockscout/bens-types'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ENS_DOMAIN: bens.DetailedDomain = { + id: '0x126d74db13895f8d3a1d362410212731d1e1d9be8add83e388385f93d84c8c84', + name: 'kitty.cat.eth', + tokens: [ + { + id: '973523146267017920308', + contract_hash: ADDRESS_HASH, + type: bens.TokenType.NATIVE_DOMAIN_TOKEN, + }, + ], + owner: ADDRESS_PARAMS, + resolved_address: ADDRESS_PARAMS, + registrant: ADDRESS_PARAMS, + registration_date: '2023-12-20T01:29:12.000Z', + expiry_date: '2099-01-02T01:29:12.000Z', + other_addresses: { + ETH: ADDRESS_HASH, + }, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; + +export const ENS_DOMAIN_EVENT: bens.DomainEvent = { + transaction_hash: TX_HASH, + timestamp: '2022-06-06T08:43:15.000000Z', + from_address: ADDRESS_PARAMS, + action: '0xf7a16963', +}; diff --git a/explorer/frontend/stubs/L2.ts b/explorer/frontend/stubs/L2.ts new file mode 100644 index 000000000..4784ed7e0 --- /dev/null +++ b/explorer/frontend/stubs/L2.ts @@ -0,0 +1,74 @@ +import type { + OptimismL2TxnBatch, + OptimisticL2DepositsItem, + OptimisticL2DisputeGamesItem, + OptimisticL2OutputRootsItem, + OptimisticL2TxnBatchesItem, + OptimisticL2WithdrawalsItem, +} from 'types/api/optimisticL2'; + +import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const L2_DEPOSIT_ITEM: OptimisticL2DepositsItem = { + l1_block_number: 9045233, + l1_block_timestamp: '2023-05-22T18:00:36.000000Z', + l1_transaction_hash: TX_HASH, + l1_transaction_origin: ADDRESS_HASH, + l2_transaction_gas_limit: '100000', + l2_transaction_hash: TX_HASH, +}; + +export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = { + challenge_period_end: null, + from: ADDRESS_PARAMS, + l1_transaction_hash: TX_HASH, + l2_timestamp: '2023-06-01T13:44:56.000000Z', + l2_transaction_hash: TX_HASH, + msg_nonce: 2393, + msg_nonce_version: 1, + status: 'Ready to prove', +}; + +export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = { + number: 260991, + batch_data_container: 'in_blob4844', + l1_timestamp: '2023-06-01T14:46:48.000000Z', + l1_transaction_hashes: [ + TX_HASH, + ], + l2_start_block_number: 5218590, + l2_end_block_number: 5218777, + transactions_count: 9, +}; + +export const L2_TXN_BATCH: OptimismL2TxnBatch = { + ...L2_TXN_BATCHES_ITEM, + batch_data_container: 'in_blob4844', + blobs: [ + { + hash: '0x01fb41e1ae9f827e13abb0ee94be2ee574a23ac31426cea630ddd18af854bc85', + l1_timestamp: '2024-09-03T13:26:23.000000Z', + l1_transaction_hash: '0xd25ee571f1701690615099b208a9431d8611d0130dc342bead6d9edc291f04b9', + }, + ], +}; + +export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = { + l1_block_number: 9103684, + l1_timestamp: '2023-06-01T15:26:12.000000Z', + l1_transaction_hash: TX_HASH, + l2_block_number: 10102468, + l2_output_index: 50655, + output_root: TX_HASH, +}; + +export const L2_DISPUTE_GAMES_ITEM: OptimisticL2DisputeGamesItem = { + contract_address_hash: ADDRESS_HASH, + created_at: '2023-06-01T15:26:12.000000Z', + game_type: 0, + index: 6594, + l2_block_number: 50655, + resolved_at: null, + status: 'In progress', +}; diff --git a/explorer/frontend/stubs/RPC.ts b/explorer/frontend/stubs/RPC.ts new file mode 100644 index 000000000..0da0947c7 --- /dev/null +++ b/explorer/frontend/stubs/RPC.ts @@ -0,0 +1,94 @@ +import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt, Withdrawal } from 'viem'; + +import { ADDRESS_HASH } from './addressParams'; +import { BLOCK_HASH } from './block'; +import { TX_HASH } from './tx'; + +export const WITHDRAWAL: Withdrawal = { + index: '0x1af95d9', + validatorIndex: '0x7d748', + address: '0x9b52b9033ecbb6635f1c31a646d5691b282878aa', + amount: '0x29e16b', +}; + +export const GET_TRANSACTION: GetTransactionReturnType = { + blockHash: BLOCK_HASH, + blockNumber: BigInt(10361367), + from: ADDRESS_HASH, + gas: BigInt(800000), + maxPriorityFeePerGas: BigInt(2), + maxFeePerGas: BigInt(14), + hash: TX_HASH, + input: '0x7898e0', + nonce: 117694, + to: ADDRESS_HASH, + transactionIndex: 60, + value: BigInt(42), + type: 'eip1559', + accessList: [], + chainId: 5, + v: BigInt(0), + r: '0x2c5022ff7f78a22f1a99afbd568f75cb52812189ed8c264c8310e0b8dba2c8a8', + s: '0x50938f87c92b9eeb9777507ca8f7397840232d00d1dbac3edac6c115b4656763', + yParity: 1, + typeHex: '0x2', +}; + +export const GET_TRANSACTION_RECEIPT: TransactionReceipt = { + blockHash: BLOCK_HASH, + blockNumber: BigInt(10361367), + contractAddress: null, + cumulativeGasUsed: BigInt(39109), + effectiveGasPrice: BigInt(13), + from: ADDRESS_HASH, + gasUsed: BigInt(39109), + logs: [], + logsBloom: '0x0', + status: 'success', + to: ADDRESS_HASH, + transactionHash: TX_HASH, + transactionIndex: 60, + type: '0x2', +}; + +export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420); + +export const GET_BALANCE = BigInt(42_000_000_000_000); + +export const GET_TRANSACTIONS_COUNT = 42; + +export const GET_BLOCK: GetBlockReturnType = { + baseFeePerGas: BigInt(11), + difficulty: BigInt(111), + extraData: '0xd8830', + gasLimit: BigInt(800000), + gasUsed: BigInt(42000), + hash: BLOCK_HASH, + logsBloom: '0x008000', + miner: ADDRESS_HASH, + mixHash: BLOCK_HASH, + nonce: '0x0000000000000000', + number: BigInt(10361367), + parentHash: BLOCK_HASH, + receiptsRoot: BLOCK_HASH, + sha3Uncles: BLOCK_HASH, + size: BigInt(88), + stateRoot: BLOCK_HASH, + timestamp: BigInt(1628580000), + totalDifficulty: BigInt(10361367), + transactions: [ + TX_HASH, + ], + transactionsRoot: TX_HASH, + uncles: [], + withdrawals: Array(10).fill(WITHDRAWAL), + withdrawalsRoot: TX_HASH, + sealFields: [ '0x00' ], + blobGasUsed: BigInt(0), + excessBlobGas: BigInt(0), +}; + +export const GET_BLOCK_WITH_TRANSACTIONS: GetBlockReturnType = { + ...GET_BLOCK, + transactions: Array(50).fill(GET_TRANSACTION), +}; diff --git a/explorer/frontend/stubs/account.ts b/explorer/frontend/stubs/account.ts new file mode 100644 index 000000000..f46a3a064 --- /dev/null +++ b/explorer/frontend/stubs/account.ts @@ -0,0 +1,98 @@ +import type { AddressTag, TransactionTag, ApiKey, CustomAbi, VerifiedAddress, TokenInfoApplication, WatchlistAddress } from 'types/api/account'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const PRIVATE_TAG_ADDRESS: AddressTag = { + address: ADDRESS_PARAMS, + address_hash: ADDRESS_HASH, + id: 4, + name: 'placeholder', +}; + +export const PRIVATE_TAG_TX: TransactionTag = { + id: 1, + name: 'placeholder', + transaction_hash: TX_HASH, +}; + +export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = { + address: ADDRESS_PARAMS, + address_balance: '7072643779453701031672', + address_hash: ADDRESS_HASH, + exchange_rate: '0.00099052', + id: 18, + name: 'placeholder', + notification_methods: { + email: false, + }, + notification_settings: { + 'ERC-20': { + incoming: true, + outcoming: true, + }, + 'ERC-721': { + incoming: true, + outcoming: true, + }, + 'ERC-404': { + incoming: true, + outcoming: true, + }, + 'native': { + incoming: true, + outcoming: true, + }, + }, + tokens_count: 42, + tokens_fiat_value: '12345', + tokens_overflow: false, +}; + +export const API_KEY: ApiKey = { + api_key: '9c3ecf44-a1ca-4ff1-b28e-329e8b65f652', + name: 'placeholder', +}; + +export const CUSTOM_ABI: CustomAbi = { + abi: [ + { + constant: false, + payable: false, + inputs: [ { name: 'target', type: 'address' } ], + name: 'unknownWriteMethod', + outputs: [ { name: 'result', type: 'address' } ], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + contract_address: ADDRESS_PARAMS, + contract_address_hash: ADDRESS_HASH, + id: 1, + name: 'placeholder', +}; + +export const VERIFIED_ADDRESS: VerifiedAddress = { + userId: 'john.doe@gmail.com', + chainId: '5', + contractAddress: ADDRESS_HASH, + verifiedDate: '2022-11-11', + metadata: { + tokenName: 'Placeholder Token', + tokenSymbol: 'PLC', + }, +}; + +export const TOKEN_INFO_APPLICATION: TokenInfoApplication = { + id: '1', + tokenAddress: ADDRESS_HASH, + status: 'IN_PROCESS', + updatedAt: '2022-11-11 13:49:48.031453Z', + requesterName: 'John Doe', + requesterEmail: 'john.doe@gmail.com', + projectWebsite: 'http://example.com', + projectEmail: 'info@example.com', + iconUrl: 'https://example.com/100/100', + projectDescription: 'Hello!', + projectSector: 'DeFi', +}; diff --git a/explorer/frontend/stubs/address.ts b/explorer/frontend/stubs/address.ts new file mode 100644 index 000000000..07c8ce273 --- /dev/null +++ b/explorer/frontend/stubs/address.ts @@ -0,0 +1,130 @@ +import type { + Address, + AddressCoinBalanceHistoryItem, + AddressCollection, + AddressCounters, + AddressEpochRewardsItem, + AddressMudTableItem, + AddressNFT, + AddressTabsCounters, + AddressTokenBalance, +} from 'types/api/address'; +import type { AddressesItem } from 'types/api/addresses'; + +import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams'; +import { MUD_SCHEMA, MUD_TABLE } from './mud'; +import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token'; +import { TX_HASH } from './tx'; + +export const ADDRESS_INFO: Address = { + block_number_balance_updated_at: 8774377, + coin_balance: '810941268802273085757', + creation_transaction_hash: null, + creator_address_hash: ADDRESS_HASH, + exchange_rate: null, + has_logs: true, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: false, + hash: ADDRESS_HASH, + implementations: [ { address_hash: ADDRESS_HASH, name: 'Transparent Upgradable Proxy' } ], + is_contract: true, + is_verified: true, + name: 'ChainLink Token (goerli)', + token: TOKEN_INFO_ERC_20, + private_tags: [], + public_tags: [], + watchlist_names: [], + watchlist_address_id: null, + ens_domain_name: null, +}; + +export const ADDRESS_COUNTERS: AddressCounters = { + gas_usage_count: '8028907522', + token_transfers_count: '420', + transactions_count: '119020', + validations_count: '0', +}; + +export const ADDRESS_TABS_COUNTERS: AddressTabsCounters = { + internal_transactions_count: 10, + logs_count: 10, + token_balances_count: 10, + token_transfers_count: 10, + transactions_count: 10, + validations_count: 10, + withdrawals_count: 10, +}; + +export const TOP_ADDRESS: AddressesItem = { + coin_balance: '11886682377162664596540805', + transactions_count: '1835', + hash: '0x4f7A67464B5976d7547c860109e4432d50AfB38e', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [ ], + watchlist_names: [], + ens_domain_name: null, +}; + +export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = { + block_number: 9004413, + block_timestamp: '2023-05-15T13:16:24Z', + delta: '1000000000000000000', + transaction_hash: TX_HASH, + value: '953427250000000000000000', +}; + +export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = { + token: TOKEN_INFO_ERC_20, + token_id: null, + token_instance: null, + value: '1000000000000000000000000', +}; + +export const ADDRESS_NFT_721: AddressNFT = { + token_type: 'ERC-721', + token: TOKEN_INFO_ERC_721, + value: '1', + ...TOKEN_INSTANCE, +}; + +export const ADDRESS_NFT_1155: AddressNFT = { + token_type: 'ERC-1155', + token: TOKEN_INFO_ERC_1155, + value: '10', + ...TOKEN_INSTANCE, +}; + +export const ADDRESS_NFT_404: AddressNFT = { + token_type: 'ERC-404', + token: TOKEN_INFO_ERC_404, + value: '10', + ...TOKEN_INSTANCE, +}; + +export const ADDRESS_COLLECTION: AddressCollection = { + token: TOKEN_INFO_ERC_1155, + amount: '4', + token_instances: Array(4).fill(TOKEN_INSTANCE), +}; + +export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = { + schema: MUD_SCHEMA, + table: MUD_TABLE, +}; + +export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = { + amount: '136609473658452408568', + block_number: 10355938, + block_timestamp: '2022-05-15T13:16:24Z', + type: 'voter', + token: TOKEN_INFO_ERC_20, + block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6', + account: ADDRESS_PARAMS, + epoch_number: 1234, + associated_account: ADDRESS_PARAMS, +}; diff --git a/explorer/frontend/stubs/addressParams.ts b/explorer/frontend/stubs/addressParams.ts new file mode 100644 index 000000000..8548abca9 --- /dev/null +++ b/explorer/frontend/stubs/addressParams.ts @@ -0,0 +1,15 @@ +import type { AddressParam } from 'types/api/addressParams'; + +export const ADDRESS_HASH = '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a'; + +export const ADDRESS_PARAMS: AddressParam = { + hash: ADDRESS_HASH, + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, +}; diff --git a/explorer/frontend/stubs/advancedFilter.ts b/explorer/frontend/stubs/advancedFilter.ts new file mode 100644 index 000000000..341280220 --- /dev/null +++ b/explorer/frontend/stubs/advancedFilter.ts @@ -0,0 +1,17 @@ +import type { AdvancedFilterResponseItem } from 'types/api/advancedFilter'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ADVANCED_FILTER_ITEM: AdvancedFilterResponseItem = { + fee: '215504444616317', + from: ADDRESS_PARAMS, + hash: TX_HASH, + method: 'approve', + timestamp: '2022-11-11T11:11:11.000000Z', + to: ADDRESS_PARAMS, + token: null, + total: null, + type: 'coin_transfer', + value: '42000420000000000000', +}; diff --git a/explorer/frontend/stubs/arbitrumL2.ts b/explorer/frontend/stubs/arbitrumL2.ts new file mode 100644 index 000000000..f612838e5 --- /dev/null +++ b/explorer/frontend/stubs/arbitrumL2.ts @@ -0,0 +1,52 @@ +import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatch, ArbitrumL2MessagesItem, ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2'; + +import { ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ARBITRUM_MESSAGES_ITEM: ArbitrumL2MessagesItem = { + completion_transaction_hash: TX_HASH, + id: 181920, + origination_address_hash: ADDRESS_HASH, + origination_transaction_block_number: 123456, + origination_transaction_hash: TX_HASH, + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'relayed', +}; + +export const ARBITRUM_L2_TXN_BATCHES_ITEM: ArbitrumL2TxnBatchesItem = { + number: 12345, + blocks_count: 12345, + transactions_count: 10000, + commitment_transaction: { + block_number: 12345, + timestamp: '2024-04-17T08:51:58.000000Z', + hash: TX_HASH, + status: 'finalized', + }, + batch_data_container: 'in_blob4844', +}; + +export const ARBITRUM_L2_TXN_BATCH: ArbitrumL2TxnBatch = { + ...ARBITRUM_L2_TXN_BATCHES_ITEM, + after_acc_hash: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb', + before_acc_hash: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc', + start_block_number: 1245209, + end_block_number: 1245490, + data_availability: { + batch_data_container: 'in_blob4844', + }, +}; + +export const ARBITRUM_L2_TXN_WITHDRAWALS_ITEM: ArbitrumL2TxnWithdrawalsItem = { + arb_block_number: 70889261, + caller_address_hash: '0x507f55d716340fc836ba52c1a8daebcfeedeef1a', + completion_transaction_hash: null, + callvalue: '100000000000000', + data: '0x', + destination_address_hash: '0x507f55d716340fc836ba52c1a8daebcfeedeef1a', + eth_block_number: 6494128, + id: 43685, + l2_timestamp: 1723578569, + status: 'relayed', + token: null, +}; diff --git a/explorer/frontend/stubs/blobs.ts b/explorer/frontend/stubs/blobs.ts new file mode 100644 index 000000000..89ca5c4cb --- /dev/null +++ b/explorer/frontend/stubs/blobs.ts @@ -0,0 +1,20 @@ +import type { Blob, TxBlob } from 'types/api/blobs'; + +import { TX_HASH } from './tx'; + +const BLOB_HASH = '0x0137cd898a9aaa92bbe94999d2a98241f5eabc829d9354160061789963f85995'; +const BLOB_PROOF = '0x82683d5d6e58a76f2a607b8712cad113621d46cb86a6bcfcffb1e274a70c7308b3243c6075ee22d904fecf8d4c147c6f'; + +export const TX_BLOB: TxBlob = { + blob_data: '0x010203040506070809101112', + hash: BLOB_HASH, + kzg_commitment: BLOB_PROOF, + kzg_proof: BLOB_PROOF, +}; + +export const BLOB: Blob = { + ...TX_BLOB, + transaction_hashes: [ + { block_consensus: true, transaction_hash: TX_HASH }, + ], +}; diff --git a/explorer/frontend/stubs/block.ts b/explorer/frontend/stubs/block.ts new file mode 100644 index 000000000..fcff21937 --- /dev/null +++ b/explorer/frontend/stubs/block.ts @@ -0,0 +1,59 @@ +import type { Block, BlockEpochElectionReward, BlockEpoch } from 'types/api/block'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TOKEN_INFO_ERC_20, TOKEN_TRANSFER_ERC_20 } from './token'; + +export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70'; + +export const BLOCK: Block = { + base_fee_per_gas: '10000000000', + burnt_fees: '92834504000000000', + burnt_fees_percentage: 42.2, + difficulty: '340282366920938463463374607431768211451', + extra_data: 'TODO', + gas_limit: '30000000', + gas_target_percentage: 55.79, + gas_used: '6631036', + gas_used_percentage: 22.10, + hash: BLOCK_HASH, + height: 8988736, + miner: ADDRESS_PARAMS, + nonce: '0x0000000000000000', + parent_hash: BLOCK_HASH, + priority_fee: '19241635454943109', + rewards: [ + { + reward: '19241635454943109', + type: 'Validator Reward', + }, + ], + size: 46406, + state_root: 'TODO', + timestamp: '2023-05-12T19:29:12.000000Z', + total_difficulty: '10837812015930321201107455268036056402048391639', + transactions_count: 142, + transaction_fees: '19241635547777613', + type: 'block', + uncles_hashes: [], +}; + +const BLOCK_EPOCH_REWARD: BlockEpochElectionReward = { + count: 10, + total: '157705500305820107521', + token: TOKEN_INFO_ERC_20, +}; + +export const BLOCK_EPOCH: BlockEpoch = { + number: 1486, + aggregated_election_rewards: { + group: BLOCK_EPOCH_REWARD, + validator: BLOCK_EPOCH_REWARD, + voter: BLOCK_EPOCH_REWARD, + delegated_payment: BLOCK_EPOCH_REWARD, + }, + distribution: { + carbon_offsetting_transfer: TOKEN_TRANSFER_ERC_20, + community_transfer: TOKEN_TRANSFER_ERC_20, + reserve_bolster_transfer: TOKEN_TRANSFER_ERC_20, + }, +}; diff --git a/explorer/frontend/stubs/contract.ts b/explorer/frontend/stubs/contract.ts new file mode 100644 index 000000000..90b3429d2 --- /dev/null +++ b/explorer/frontend/stubs/contract.ts @@ -0,0 +1,119 @@ +import type * as stats from '@blockscout/stats-types'; +import type { SmartContract, SmartContractMudSystemsResponse } from 'types/api/contract'; +import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; + +import type { SolidityScanReport } from 'lib/solidityScan/schema'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { STATS_COUNTER } from './stats'; + +export const CONTRACT_CODE_UNVERIFIED = { + creation_bytecode: '0x60806040526e', + deployed_bytecode: '0x608060405233', + is_self_destructed: false, +} as SmartContract; + +export const CONTRACT_CODE_VERIFIED = { + abi: [ + { + inputs: [], + name: 'symbol', + outputs: [ { internalType: 'string', name: '', type: 'string' } ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ { internalType: 'address', name: 'newOwner', type: 'address' } ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + additional_sources: [], + can_be_visualized_via_sol2uml: true, + compiler_settings: { + compilationTarget: { + 'contracts/StubContract.sol': 'StubContract', + }, + evmVersion: 'london', + libraries: {}, + metadata: { + bytecodeHash: 'ipfs', + }, + optimizer: { + enabled: false, + runs: 200, + }, + remappings: [], + }, + compiler_version: 'v0.8.7+commit.e28d00a7', + constructor_args: '0000000000000000000000005c7bcd6e7de5423a257d81b442095a1a6ced35c5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + creation_bytecode: '0x6080604052348', + deployed_bytecode: '0x60806040', + evm_version: 'london', + external_libraries: [], + file_path: 'contracts/StubContract.sol', + is_verified: true, + name: 'StubContract', + optimization_enabled: false, + optimization_runs: 200, + source_code: 'source_code', + verified_at: '2023-02-21T14:39:16.906760Z', + license_type: 'mit', +} as unknown as SmartContract; + +export const VERIFIED_CONTRACT_INFO: VerifiedContract = { + address: { ...ADDRESS_PARAMS, name: 'StubContract' }, + coin_balance: '30319033612988277', + compiler_version: 'v0.8.17+commit.8df45f5f', + has_constructor_args: true, + language: 'solidity', + market_cap: null, + optimization_enabled: false, + transactions_count: 565058, + verified_at: '2023-04-10T13:16:33.884921Z', + license_type: 'mit', +}; + +export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '1234', +}; + +export const VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE: stats.ContractsPageStats = { + total_contracts: STATS_COUNTER, + new_contracts_24h: STATS_COUNTER, + total_verified_contracts: STATS_COUNTER, + new_verified_contracts_24h: STATS_COUNTER, +}; + +export const SOLIDITY_SCAN_REPORT: SolidityScanReport = { + scan_report: { + contractname: 'BullRunners', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 0, + low: 2, + medium: 0, + }, + score_v2: '72.22', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; + +export const MUD_SYSTEMS: SmartContractMudSystemsResponse = { + items: [ + { + name: 'sy.AccessManagement', + address_hash: ADDRESS_HASH, + }, + ], +}; diff --git a/explorer/frontend/stubs/internalTx.ts b/explorer/frontend/stubs/internalTx.ts new file mode 100644 index 000000000..f0e24bd1f --- /dev/null +++ b/explorer/frontend/stubs/internalTx.ts @@ -0,0 +1,19 @@ +import type { InternalTransaction } from 'types/api/internalTransaction'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const INTERNAL_TX: InternalTransaction = { + block_number: 9006105, + created_contract: null, + error: null, + from: ADDRESS_PARAMS, + gas_limit: '754278', + index: 1, + success: true, + timestamp: '2023-05-15T20:14:00.000000Z', + to: ADDRESS_PARAMS, + transaction_hash: TX_HASH, + type: 'staticcall', + value: '22324344900000000', +}; diff --git a/explorer/frontend/stubs/interop.ts b/explorer/frontend/stubs/interop.ts new file mode 100644 index 000000000..a5dfb525a --- /dev/null +++ b/explorer/frontend/stubs/interop.ts @@ -0,0 +1,21 @@ +import type { InteropMessage } from 'types/api/interop'; + +import { ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const INTEROP_MESSAGE: InteropMessage = { + init_transaction_hash: TX_HASH, + nonce: 52, + payload: '0x4f0edcc90000000000000000000000007da521cbbe62e89cd75e0993c78b8c68c25f696b', + relay_chain: { + chain_id: 420120000, + chain_name: 'Optimism Testnet', + chain_logo: null, + instance_url: 'https://optimism-interop-alpha-0.blockscout.com/', + }, + relay_transaction_hash: TX_HASH, + sender: ADDRESS_HASH, + status: 'Relayed', + target: ADDRESS_HASH, + timestamp: '2025-02-20T01:05:14.000000Z', +}; diff --git a/explorer/frontend/stubs/log.ts b/explorer/frontend/stubs/log.ts new file mode 100644 index 000000000..3e34dbcee --- /dev/null +++ b/explorer/frontend/stubs/log.ts @@ -0,0 +1,35 @@ +import type { Log } from 'types/api/log'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const LOG: Log = { + address: ADDRESS_PARAMS, + data: '0x000000000000000000000000000000000000000000000000000000d75e4be200', + decoded: { + method_call: 'CreditSpended(uint256 indexed _type, uint256 _quantity)', + method_id: '58cdf94a', + parameters: [ + { + indexed: true, + name: '_type', + type: 'uint256', + value: 'placeholder', + }, + { + indexed: false, + name: '_quantity', + type: 'uint256', + value: 'placeholder', + }, + ], + }, + index: 42, + topics: [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x000000000000000000000000c52ea157a7fb3e25a069d47df0428ac70cd656b1', + '0x000000000000000000000000302fd86163cb9ad5533b3952dafa3b633a82bc51', + null, + ], + transaction_hash: TX_HASH, +}; diff --git a/explorer/frontend/stubs/marketplace.ts b/explorer/frontend/stubs/marketplace.ts new file mode 100644 index 000000000..067136893 --- /dev/null +++ b/explorer/frontend/stubs/marketplace.ts @@ -0,0 +1,19 @@ +/* eslint-disable max-len */ +import type { MarketplaceAppOverview } from 'types/client/marketplace'; + +export const MARKETPLACE_APP: MarketplaceAppOverview = { + author: 'StubApp Inc.', + id: 'stub-app', + title: 'My cool app name', + logo: '', + categories: [ + 'Bridge', + ], + shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + site: 'https://example.com', + description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + external: true, + url: 'https://example.com', +}; + +export const CATEGORIES: Array = Array(9).fill('Bridge').map((c, i) => c + i); diff --git a/explorer/frontend/stubs/mud.ts b/explorer/frontend/stubs/mud.ts new file mode 100644 index 000000000..dbbb51c89 --- /dev/null +++ b/explorer/frontend/stubs/mud.ts @@ -0,0 +1,24 @@ +import type { MudWorldItem, MudWorldSchema, MudWorldTable } from 'types/api/mudWorlds'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const MUD_TABLE: MudWorldTable = { + table_full_name: 'ot.Match', + table_id: '0x6f7400000000000000000000000000004d617463680000000000000000000000', + table_name: 'Match', + table_namespace: '', + table_type: 'offchain', +}; + +export const MUD_SCHEMA: MudWorldSchema = { + key_names: [ 'matchEntityKey', 'entity' ], + key_types: [ 'bytes32', 'bytes32' ], + value_names: [ 'matchEntity' ], + value_types: [ 'bytes32' ], +}; + +export const MUD_WORLD: MudWorldItem = { + address: ADDRESS_PARAMS, + coin_balance: '7072643779453701031672', + transactions_count: 442, +}; diff --git a/explorer/frontend/stubs/noves/NovesTranslate.ts b/explorer/frontend/stubs/noves/NovesTranslate.ts new file mode 100644 index 000000000..848ed6dab --- /dev/null +++ b/explorer/frontend/stubs/noves/NovesTranslate.ts @@ -0,0 +1,43 @@ +import type { NovesResponseData, NovesClassificationData, NovesRawTransactionData } from 'types/api/noves'; + +const NOVES_TRANSLATE_CLASSIFIED: NovesClassificationData = { + description: 'Sent 0.04 ETH', + received: [ { + action: 'Sent Token', + actionFormatted: 'Sent Token', + amount: '45', + from: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + to: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + token: { + address: '', + name: 'ETH', + symbol: 'ETH', + decimals: 18, + }, + } ], + sent: [], + source: { + type: '', + }, + type: '0x2', + typeFormatted: 'Send NFT', +}; + +const NOVES_TRANSLATE_RAW: NovesRawTransactionData = { + blockNumber: 1, + fromAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + gas: 2, + gasPrice: 3, + timestamp: 20000, + toAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + transactionFee: 2, + transactionHash: '0x128b79937a0eDE33258992c9668455f997f1aF24', +}; + +export const NOVES_TRANSLATE: NovesResponseData = { + accountAddress: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + chain: 'base', + txTypeVersion: 2, + rawTransactionData: NOVES_TRANSLATE_RAW, + classificationData: NOVES_TRANSLATE_CLASSIFIED, +}; diff --git a/explorer/frontend/stubs/pools.ts b/explorer/frontend/stubs/pools.ts new file mode 100644 index 000000000..d3111cc5d --- /dev/null +++ b/explorer/frontend/stubs/pools.ts @@ -0,0 +1,17 @@ +export const POOL = { + contract_address: '0x6a1041865b76d1dc33da0257582591227c57832c', + chain_id: '1', + base_token_address: '0xf63e309818e4ea13782678ce6c31c1234fa61809', + base_token_symbol: 'JANET', + base_token_icon_url: null, + quote_token_address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + quote_token_symbol: 'WETH', + quote_token_icon_url: 'https://coin-images.coingecko.com/coins/images/2518/small/weth.png?1696503332', + base_token_fully_diluted_valuation_usd: '15211385', + base_token_market_cap_usd: '15211385', + quote_token_fully_diluted_valuation_usd: '15211385', + quote_token_market_cap_usd: '15211385', + liquidity: '394101.2428', + dex: { id: 'uniswap_v2', name: 'Uniswap V2' }, + coin_gecko_terminal_url: 'https://www.geckoterminal.com/eth/pools/0x6a1041865b76d1dc33da0257582591227c57832c', +}; diff --git a/explorer/frontend/stubs/rewards.ts b/explorer/frontend/stubs/rewards.ts new file mode 100644 index 000000000..a03d06dbd --- /dev/null +++ b/explorer/frontend/stubs/rewards.ts @@ -0,0 +1,56 @@ +import type { GetActivityRewardsResponse } from '@blockscout/points-types'; + +export const USER_ACTIVITY: GetActivityRewardsResponse = { + items: [ + { + date: '2025-03-10', + end_date: '2025-03-16', + activity: 'sent_transactions', + amount: '60', + percentile: 0.5, + is_pending: true, + }, + { + date: '2025-03-10', + end_date: '2025-03-16', + activity: 'verified_contracts', + amount: '40', + percentile: 0.3, + is_pending: true, + }, + { + date: '2025-03-10', + end_date: '2025-03-16', + activity: 'blockscout_usage', + amount: '80', + percentile: 0.8, + is_pending: true, + }, + ], + last_week: [ + { + date: '2025-03-03', + end_date: '2025-03-09', + activity: 'sent_transactions', + amount: '40', + percentile: 0.25, + is_pending: false, + }, + { + date: '2025-03-03', + end_date: '2025-03-09', + activity: 'verified_contracts', + amount: '60', + percentile: 0.6, + is_pending: false, + }, + { + date: '2025-03-03', + end_date: '2025-03-09', + activity: 'blockscout_usage', + amount: '100', + percentile: 0.95, + is_pending: false, + }, + ], +}; diff --git a/explorer/frontend/stubs/scrollL2.ts b/explorer/frontend/stubs/scrollL2.ts new file mode 100644 index 000000000..df919e3e3 --- /dev/null +++ b/explorer/frontend/stubs/scrollL2.ts @@ -0,0 +1,32 @@ +import type { ScrollL2MessageItem, ScrollL2TxnBatch } from 'types/api/scrollL2'; + +import { TX_HASH } from './tx'; + +export const SCROLL_L2_TXN_BATCH: ScrollL2TxnBatch = { + commitment_transaction: { + block_number: 4053979, + hash: '0xd04d626495ef69abd37ae3ea585ed03319a3d3b50cf10874f7f36741c7b45a18', + timestamp: '2023-08-09T08:09:12.000000Z', + }, + confirmation_transaction: { + block_number: null, + hash: null, + timestamp: null, + }, + end_block_number: 1711, + number: 273, + start_block_number: 1697, + transactions_count: 15, + data_availability: { + batch_data_container: 'in_blob4844', + }, +}; + +export const SCROLL_L2_MESSAGE_ITEM: ScrollL2MessageItem = { + id: 930795, + origination_transaction_block_number: 20639178, + origination_transaction_hash: TX_HASH, + origination_timestamp: '2024-08-30T05:03:23.000000Z', + completion_transaction_hash: 'TX_HASH', + value: '5084131319054877748', +}; diff --git a/explorer/frontend/stubs/search.ts b/explorer/frontend/stubs/search.ts new file mode 100644 index 000000000..77020dd8f --- /dev/null +++ b/explorer/frontend/stubs/search.ts @@ -0,0 +1,30 @@ +import type { SearchResult, SearchResultItem } from 'types/api/search'; + +import { ADDRESS_HASH } from './addressParams'; + +export const SEARCH_RESULT_ITEM: SearchResultItem = { + address_hash: ADDRESS_HASH, + address_url: '/address/0x3714A8C7824B22271550894f7555f0a672f97809', + name: 'USDC', + symbol: 'USDC', + token_url: '/token/0x3714A8C7824B22271550894f7555f0a672f97809', + type: 'token', + icon_url: null, + is_verified_via_admin_panel: false, + is_smart_contract_verified: false, + exchange_rate: '1.11', + total_supply: null, + token_type: 'ERC-20', +}; + +export const SEARCH_RESULT_NEXT_PAGE_PARAMS: SearchResult['next_page_params'] = { + address_hash: ADDRESS_HASH, + block_hash: null, + holders_count: 11, + inserted_at: '2023-05-19T17:21:19.203681Z', + item_type: 'token', + items_count: 50, + name: 'USDCTest', + q: 'usd', + transaction_hash: null, +}; diff --git a/explorer/frontend/stubs/shibarium.ts b/explorer/frontend/stubs/shibarium.ts new file mode 100644 index 000000000..3003be711 --- /dev/null +++ b/explorer/frontend/stubs/shibarium.ts @@ -0,0 +1,20 @@ +import type { ShibariumDepositsItem, ShibariumWithdrawalsItem } from 'types/api/shibarium'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const SHIBARIUM_DEPOSIT_ITEM: ShibariumDepositsItem = { + l1_block_number: 9045233, + l1_transaction_hash: TX_HASH, + l2_transaction_hash: TX_HASH, + timestamp: '2023-05-22T18:00:36.000000Z', + user: ADDRESS_PARAMS, +}; + +export const SHIBARIUM_WITHDRAWAL_ITEM: ShibariumWithdrawalsItem = { + l2_block_number: 9045233, + l1_transaction_hash: TX_HASH, + l2_transaction_hash: TX_HASH, + timestamp: '2023-05-22T18:00:36.000000Z', + user: ADDRESS_PARAMS, +}; diff --git a/explorer/frontend/stubs/stats.ts b/explorer/frontend/stubs/stats.ts new file mode 100644 index 000000000..976debc27 --- /dev/null +++ b/explorer/frontend/stubs/stats.ts @@ -0,0 +1,110 @@ +import type * as stats from '@blockscout/stats-types'; +import type { HomeStats } from 'types/api/stats'; + +export const HOMEPAGE_STATS: HomeStats = { + average_block_time: 14346, + coin_price: '1807.68', + coin_price_change_percentage: 42, + gas_prices: { + average: { + fiat_price: '1.01', + price: 20.41, + time: 12283, + base_fee: 2.22222, + priority_fee: 12.424242, + }, + fast: { + fiat_price: '1.26', + price: 25.47, + time: 9321, + base_fee: 4.44444, + priority_fee: 22.242424, + }, + slow: { + fiat_price: '0.97', + price: 19.55, + time: 24543, + base_fee: 1.11111, + priority_fee: 7.8909, + }, + }, + gas_price_updated_at: '2022-11-11T11:09:49.051171Z', + gas_prices_update_in: 300000, + gas_used_today: '0', + market_cap: '0', + network_utilization_percentage: 22.56, + static_gas_price: null, + total_addresses: '28634064', + total_blocks: '8940150', + total_gas_used: '0', + total_transactions: '193823272', + transactions_today: '0', + tvl: '1767425.102766552', +}; + +const STATS_CHART_INFO: stats.LineChartInfo = { + id: 'chart_0', + title: 'Average transaction fee', + description: 'The average amount in ETH spent per transaction', + units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], +}; + +export const STATS_CHARTS_SECTION: stats.LineChartSection = { + id: 'placeholder', + title: 'Placeholder', + charts: [ + STATS_CHART_INFO, + { + id: 'chart_1', + title: 'Transactions fees', + description: 'Amount of tokens paid as fees', + units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'chart_2', + title: 'New transactions', + description: 'New transactions number', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + { + id: 'chart_3', + title: 'Transactions growth', + description: 'Cumulative transactions number', + units: undefined, + resolutions: [ 'DAY', 'MONTH' ], + }, + ], +}; + +export const STATS_CHARTS = { + sections: [ STATS_CHARTS_SECTION ], +}; + +export const STATS_COUNTER: stats.Counter = { + id: 'stub', + value: '9074405', + title: 'Placeholder Counter', + description: 'Placeholder description', + units: '', +}; + +export const HOMEPAGE_STATS_MICROSERVICE: stats.MainPageStats = { + average_block_time: STATS_COUNTER, + total_addresses: STATS_COUNTER, + total_blocks: STATS_COUNTER, + total_transactions: STATS_COUNTER, + yesterday_transactions: STATS_COUNTER, + total_operational_transactions: STATS_COUNTER, + yesterday_operational_transactions: STATS_COUNTER, + daily_new_transactions: { + chart: [], + info: STATS_CHART_INFO, + }, + daily_new_operational_transactions: { + chart: [], + info: STATS_CHART_INFO, + }, +}; diff --git a/explorer/frontend/stubs/token.ts b/explorer/frontend/stubs/token.ts new file mode 100644 index 000000000..357fb77b3 --- /dev/null +++ b/explorer/frontend/stubs/token.ts @@ -0,0 +1,182 @@ +import type { + TokenCounters, + TokenHolder, + TokenHolders, + TokenHoldersPagination, + TokenInfo, + TokenInstance, + TokenType, +} from 'types/api/token'; +import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } from 'types/api/tokens'; +import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; +import { generateListStub } from './utils'; + +export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70'; + +export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = { + address_hash: ADDRESS_HASH, + circulating_market_cap: '117629601.61913824', + decimals: '18', + exchange_rate: '0.999997', + holders_count: '16026', + name: 'Stub Token (goerli)', + symbol: 'STUB', + total_supply: '60000000000000000000000', + type: 'ERC-20', + icon_url: null, +}; + +export const TOKEN_INFO_ERC_721: TokenInfo<'ERC-721'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-721', +}; + +export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-1155', +}; + +export const TOKEN_INFO_ERC_404: TokenInfo<'ERC-404'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-404', +}; + +export const TOKEN_COUNTERS: TokenCounters = { + token_holders_count: '123456', + transfers_count: '123456', +}; + +export const TOKEN_HOLDER_ERC_20: TokenHolder = { + address: ADDRESS_PARAMS, + value: '1021378038331138520', +}; + +export const TOKEN_HOLDER_ERC_1155: TokenHolder = { + address: ADDRESS_PARAMS, + token_id: '12345', + value: '1021378038331138520', +}; + +export const getTokenHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => { + switch (type) { + case 'ERC-721': + return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination }); + default: + return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination }); + } +}; + +export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => { + switch (type) { + case 'ERC-721': + return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination }); + default: + return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination }); + } +}; + +export const TOKEN_TRANSFER_ERC_20: TokenTransfer = { + block_hash: BLOCK_HASH, + block_number: '123456', + from: ADDRESS_PARAMS, + log_index: '4', + method: 'addLiquidity', + timestamp: '2022-06-24T10:22:11.000000Z', + to: ADDRESS_PARAMS, + token: TOKEN_INFO_ERC_20, + total: { + decimals: '18', + value: '9851351626684503', + }, + transaction_hash: TX_HASH, + type: 'token_minting', +}; + +export const TOKEN_TRANSFER_ERC_721: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + token_instance: null, + }, + token: TOKEN_INFO_ERC_721, +}; + +export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + value: '123', + decimals: '18', + token_instance: null, + }, + token: TOKEN_INFO_ERC_1155, +}; + +export const TOKEN_TRANSFER_ERC_404: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + value: '123', + decimals: '18', + token_instance: null, + }, + token: TOKEN_INFO_ERC_404, +}; + +export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => { + switch (type) { + case 'ERC-721': + return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_404, 50, { next_page_params: pagination }); + default: + return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination }); + } +}; + +export const getTokenInstanceTransfersStub = (type?: TokenType, pagination: TokenInstanceTransferPagination | null = null): TokenInstanceTransferResponse => { + switch (type) { + case 'ERC-721': + return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_721, 10, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_1155, 10, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_404, 10, { next_page_params: pagination }); + default: + return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_20, 10, { next_page_params: pagination }); + } +}; + +export const TOKEN_INSTANCE: TokenInstance = { + animation_url: null, + external_app_url: 'https://vipsland.com/nft/collections/genesis/188882', + id: '188882', + image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif', + is_unique: true, + metadata: { + attributes: Array(3).fill({ trait_type: 'skin tone', value: 'very light skin tone' }), + description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*', + external_url: 'https://vipsland.com/nft/collections/genesis/188882', + image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif', + name: 'GENESIS #188882, 8a77ca1bcaa4036f', + }, + owner: ADDRESS_PARAMS, + holder_address_hash: ADDRESS_HASH, + thumbnails: null, +}; diff --git a/explorer/frontend/stubs/tx.ts b/explorer/frontend/stubs/tx.ts new file mode 100644 index 000000000..a1c7f728e --- /dev/null +++ b/explorer/frontend/stubs/tx.ts @@ -0,0 +1,78 @@ +import type * as stats from '@blockscout/stats-types'; +import type { RawTracesResponse } from 'types/api/rawTrace'; +import type { Transaction, TransactionsStats } from 'types/api/transaction'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { STATS_COUNTER } from './stats'; + +export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967'; + +export const TX: Transaction = { + timestamp: '2022-11-11T11:11:11.000000Z', + fee: { + type: 'actual', + value: '2100000000000000', + }, + gas_limit: '21000', + block_number: 9004925, + status: 'ok', + method: 'placeholder', + confirmations: 71, + type: 0, + exchange_rate: '1828.71', + to: ADDRESS_PARAMS, + transaction_burnt_fee: null, + max_fee_per_gas: null, + result: 'success', + hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + gas_price: '100000000000', + priority_fee: null, + base_fee_per_gas: '24', + from: ADDRESS_PARAMS, + token_transfers: null, + transaction_types: [ + 'coin_transfer', + ], + gas_used: '21000', + created_contract: null, + position: 0, + nonce: 295929, + has_error_in_internal_transactions: false, + actions: [], + decoded_input: null, + token_transfers_overflow: false, + raw_input: '0x', + value: '42000420000000000000', + max_priority_fee_per_gas: null, + revert_reason: null, + confirmation_duration: [ + 0, + 14545, + ], + transaction_tag: null, +}; + +export const TX_ZKEVM_L2: Transaction = { + ...TX, + zkevm_batch_number: 12345, + zkevm_sequence_hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + zkevm_status: 'Confirmed by Sequencer', + zkevm_verify_hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', +}; + +export const TX_RAW_TRACE: RawTracesResponse = []; + +export const TXS_STATS: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; + +export const TXS_STATS_MICROSERVICE: stats.TransactionsPageStats = { + pending_transactions_30m: STATS_COUNTER, + transactions_24h: STATS_COUNTER, + operational_transactions_24h: STATS_COUNTER, + transactions_fee_24h: STATS_COUNTER, + average_transactions_fee_24h: STATS_COUNTER, +}; diff --git a/explorer/frontend/stubs/txInterpretation.ts b/explorer/frontend/stubs/txInterpretation.ts new file mode 100644 index 000000000..e54c2cdda --- /dev/null +++ b/explorer/frontend/stubs/txInterpretation.ts @@ -0,0 +1,34 @@ +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; + +import { TOKEN_INFO_ERC_20 } from './token'; + +export const TX_INTERPRETATION: TxInterpretationResponse = { + data: { + summaries: [ + { + summary_template: '{action_type} {source_amount} Ether into {destination_amount} {destination_token}', + summary_template_variables: { + action_type: { type: 'string', value: 'Wrap' }, + source_amount: { type: 'currency', value: '0.7' }, + destination_amount: { type: 'currency', value: '0.7' }, + destination_token: { + type: 'token', + value: TOKEN_INFO_ERC_20, + }, + }, + }, + { + summary_template: '{action_type} {source_amount} Ether into {destination_amount} {destination_token}', + summary_template_variables: { + action_type: { type: 'string', value: 'Wrap' }, + source_amount: { type: 'currency', value: '0.7' }, + destination_amount: { type: 'currency', value: '0.7' }, + destination_token: { + type: 'token', + value: TOKEN_INFO_ERC_20, + }, + }, + }, + ], + }, +}; diff --git a/explorer/frontend/stubs/txStateChanges.ts b/explorer/frontend/stubs/txStateChanges.ts new file mode 100644 index 000000000..f843c8c3a --- /dev/null +++ b/explorer/frontend/stubs/txStateChanges.ts @@ -0,0 +1,48 @@ +import type { TxStateChange } from 'types/api/txStateChanges'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TOKEN_INFO_ERC_721 } from './token'; + +export const STATE_CHANGE_MINER: TxStateChange = { + address: ADDRESS_PARAMS, + balance_after: '124280364215547113', + balance_before: '123405277440098758', + change: '875086775448355', + is_miner: true, + token: null, + type: 'coin', +}; + +export const STATE_CHANGE_COIN: TxStateChange = { + address: ADDRESS_PARAMS, + balance_after: '61659392141463351540', + balance_before: '61660292436225994690', + change: '-900294762600000', + is_miner: false, + token: null, + type: 'coin', +}; + +export const STATE_CHANGE_TOKEN: TxStateChange = { + address: ADDRESS_PARAMS, + balance_after: '43', + balance_before: '42', + change: [ + { + direction: 'to', + total: { + token_id: '1621395', + token_instance: null, + }, + }, + ], + is_miner: false, + token: TOKEN_INFO_ERC_721, + type: 'token', +}; + +export const TX_STATE_CHANGES: Array = [ + STATE_CHANGE_MINER, + STATE_CHANGE_COIN, + STATE_CHANGE_TOKEN, +]; diff --git a/explorer/frontend/stubs/userOps.ts b/explorer/frontend/stubs/userOps.ts new file mode 100644 index 000000000..283e43c4f --- /dev/null +++ b/explorer/frontend/stubs/userOps.ts @@ -0,0 +1,69 @@ +import type { UserOpsItem, UserOp, UserOpsAccount } from 'types/api/userOps'; + +import { ADDRESS_HASH } from './addressParams'; +import { BLOCK_HASH } from './block'; +import { TX_HASH } from './tx'; + +const USER_OP_HASH = '0xb94fab8f31f83001a23e20b2ce3cdcfb284c57a64b9a073e0e09c018bc701978'; + +export const USER_OPS_ITEM: UserOpsItem = { + hash: USER_OP_HASH, + block_number: '10356381', + transaction_hash: TX_HASH, + address: ADDRESS_HASH, + timestamp: '2023-12-18T10:48:49.000000Z', + status: true, + fee: '48285720012071430', +}; + +export const USER_OP: UserOp = { + hash: USER_OP_HASH, + sender: ADDRESS_HASH, + nonce: '0x00b', + call_data: '0x123', + execute_call_data: null, + decoded_call_data: null, + decoded_execute_call_data: null, + call_gas_limit: '71316', + verification_gas_limit: '91551', + pre_verification_gas: '53627', + max_fee_per_gas: '100000020', + max_priority_fee_per_gas: '100000000', + signature: '0x000', + aggregator: null, + aggregator_signature: null, + entry_point: ADDRESS_HASH, + transaction_hash: TX_HASH, + block_number: '10358181', + block_hash: BLOCK_HASH, + bundler: ADDRESS_HASH, + factory: null, + paymaster: ADDRESS_HASH, + status: true, + revert_reason: null, + gas: '399596', + gas_price: '1575000898', + gas_used: '118810', + sponsor_type: 'paymaster_sponsor', + fee: '17927001792700', + timestamp: '2023-12-18T10:48:49.000000Z', + user_logs_count: 1, + user_logs_start_index: 2, + raw: { + sender: ADDRESS_HASH, + nonce: '1', + init_code: '0x', + call_data: '0x345', + call_gas_limit: '29491', + verification_gas_limit: '80734', + pre_verification_gas: '3276112', + max_fee_per_gas: '309847206', + max_priority_fee_per_gas: '100000000', + paymaster_and_data: '0x', + signature: '0x000', + }, +}; + +export const USER_OPS_ACCOUNT: UserOpsAccount = { + total_ops: 1, +}; diff --git a/explorer/frontend/stubs/utils.ts b/explorer/frontend/stubs/utils.ts new file mode 100644 index 000000000..929a41d72 --- /dev/null +++ b/explorer/frontend/stubs/utils.ts @@ -0,0 +1,14 @@ +import type { ArrayElement } from 'types/utils'; + +import type { PaginatedResourceName, PaginatedResourceResponse, PaginatedResourceResponseItems } from 'lib/api/resources'; + +export function generateListStub( + stub: ArrayElement>, + num = 50, + rest: Omit, 'items'>, +) { + return { + items: Array(num).fill(stub), + ...rest, + }; +} diff --git a/explorer/frontend/stubs/validators.ts b/explorer/frontend/stubs/validators.ts new file mode 100644 index 000000000..bbcc21768 --- /dev/null +++ b/explorer/frontend/stubs/validators.ts @@ -0,0 +1,54 @@ +import type { + ValidatorStability, + ValidatorsStabilityCountersResponse, + ValidatorBlackfort, + ValidatorsBlackfortCountersResponse, + ValidatorsZilliqaItem, + ValidatorZilliqa, +} from 'types/api/validators'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const VALIDATOR_STABILITY: ValidatorStability = { + address: ADDRESS_PARAMS, + blocks_validated_count: 25987, + state: 'active', +}; + +export const VALIDATORS_STABILITY_COUNTERS: ValidatorsStabilityCountersResponse = { + active_validators_count: '42', + active_validators_percentage: 7.14, + new_validators_count_24h: '11', + validators_count: '140', +}; + +export const VALIDATOR_BLACKFORT: ValidatorBlackfort = { + address: ADDRESS_PARAMS, + name: 'testnet-1', + commission: 10, + delegated_amount: '0', + self_bonded_amount: '10000', +}; + +export const VALIDATORS_BLACKFORT_COUNTERS: ValidatorsBlackfortCountersResponse = { + new_validators_count_24h: '11', + validators_count: '140', +}; + +export const VALIDATORS_ZILLIQA_ITEM: ValidatorsZilliqaItem = { + index: 420, + bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100', + balance: '1000000000000000000', +}; + +export const VALIDATOR_ZILLIQA: ValidatorZilliqa = { + index: 420, + bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100', + balance: '1000000000000000000', + added_at_block_number: 1234567890, + control_address: ADDRESS_PARAMS, + peer_id: '1234567890', + reward_address: ADDRESS_PARAMS, + signing_address: ADDRESS_PARAMS, + stake_updated_at_block_number: 1234567890, +}; diff --git a/explorer/frontend/stubs/withdrawals.ts b/explorer/frontend/stubs/withdrawals.ts new file mode 100644 index 000000000..25fbb4a82 --- /dev/null +++ b/explorer/frontend/stubs/withdrawals.ts @@ -0,0 +1,12 @@ +import type { WithdrawalsItem } from 'types/api/withdrawals'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const WITHDRAWAL: WithdrawalsItem = { + amount: '12565723', + index: 3810697, + receiver: ADDRESS_PARAMS, + validator_index: 25987, + block_number: 9005713, + timestamp: '2023-05-12T19:29:12.000000Z', +}; diff --git a/explorer/frontend/stubs/zkEvmL2.ts b/explorer/frontend/stubs/zkEvmL2.ts new file mode 100644 index 000000000..85f7f3e31 --- /dev/null +++ b/explorer/frontend/stubs/zkEvmL2.ts @@ -0,0 +1,44 @@ +import type { ZkEvmL2DepositsItem, ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2'; + +import { TX_HASH } from './tx'; + +export const ZKEVM_DEPOSITS_ITEM: ZkEvmL2DepositsItem = { + block_number: 19674901, + index: 181920, + l1_transaction_hash: '0xa74edfa5824a07a5f95ca1145140ed589df7f05bb17796bf18090b14c4566b5d', + l2_transaction_hash: '0x436d1c7ada270466ca0facdb96ecc22934d68d13b8a08f541b8df11b222967b5', + symbol: 'ETH', + timestamp: '2023-06-01T14:46:48.000000Z', + value: '0.13040262', +}; + +export const ZKEVM_WITHDRAWALS_ITEM: ZkEvmL2WithdrawalsItem = { + block_number: 11692968, + index: 47003, + l1_transaction_hash: '0x230cf46dabea287ac7d0ba83b8ea120bb83c1de58a81d34f44788f0459096c52', + l2_transaction_hash: '0x519d9f025ec47f08a48d708964d177189d2246ddf988686c481f5debcf097e34', + symbol: 'ETH', + timestamp: '2024-04-17T08:51:58.000000Z', + value: '110.35', +}; + +export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Finalized', + verify_transaction_hash: TX_HASH, + sequence_transaction_hash: TX_HASH, + number: 5218590, + transactions_count: 9, +}; + +export const ZKEVM_L2_TXN_BATCH: ZkEvmL2TxnBatch = { + acc_input_hash: '0xb815fe2832977f1324ad0124a019b938f189f7b470292f40a21284f15774b3b3', + global_exit_root: '0x0000000000000000000000000000000000000000000000000000000000000000', + number: 1, + sequence_transaction_hash: '0x57b9b95db5f94f125710bdc8fbb3fabaac10125b44b0cb61dbc69daddf06d0cd', + state_root: '0xb9a589d6b3ae44d3b250a9993caa5e3721568197f56e4743989ecb2285d80ec4', + status: 'Finalized', + timestamp: '2023-09-15T06:22:48.000000Z', + transactions: [ '0xff99dd67646b8f3d657cc6f19eb33abc346de2dbaccd03e45e7726cc28e3e186' ], + verify_transaction_hash: '0x093276fa65c67d7b12dd96f4fefafba9d9ad2f1c23c6e53f96583971ce75352d', +}; diff --git a/explorer/frontend/stubs/zkSyncL2.ts b/explorer/frontend/stubs/zkSyncL2.ts new file mode 100644 index 000000000..041bf29e3 --- /dev/null +++ b/explorer/frontend/stubs/zkSyncL2.ts @@ -0,0 +1,27 @@ +import type { ZkSyncBatch, ZkSyncBatchesItem } from 'types/api/zkSyncL2'; + +import { TX_HASH } from './tx'; + +export const ZKSYNC_L2_TXN_BATCHES_ITEM: ZkSyncBatchesItem = { + commit_transaction_hash: TX_HASH, + commit_transaction_timestamp: '2022-03-17T19:33:04.519145Z', + execute_transaction_hash: TX_HASH, + execute_transaction_timestamp: '2022-03-17T20:49:48.856345Z', + number: 8002, + prove_transaction_hash: TX_HASH, + prove_transaction_timestamp: '2022-03-17T20:49:48.772442Z', + status: 'Executed on L1', + timestamp: '2022-03-17T17:00:11.000000Z', + transactions_count: 1215, +}; + +export const ZKSYNC_L2_TXN_BATCH: ZkSyncBatch = { + ...ZKSYNC_L2_TXN_BATCHES_ITEM, + start_block_number: 1245209, + end_block_number: 1245490, + l1_gas_price: '4173068062', + l1_transactions_count: 0, + l2_fair_gas_price: '100000000', + l2_transactions_count: 287, + root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792', +}; diff --git a/explorer/frontend/svgo.config.js b/explorer/frontend/svgo.config.js new file mode 100644 index 000000000..1c3d7c355 --- /dev/null +++ b/explorer/frontend/svgo.config.js @@ -0,0 +1,18 @@ +module.exports = { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + removeHiddenElems: false, + }, + }, + }, + 'removeDimensions', + ], + js2svg: { + indent: 2, + pretty: true, + }, +}; diff --git a/explorer/frontend/toolkit/chakra/accordion.tsx b/explorer/frontend/toolkit/chakra/accordion.tsx new file mode 100644 index 000000000..333aa6ee6 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/accordion.tsx @@ -0,0 +1,91 @@ +import { Accordion, Icon } from '@chakra-ui/react'; +import * as React from 'react'; + +import IndicatorIcon from 'icons/arrows/east-mini.svg'; + +interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps { + indicatorPlacement?: 'start' | 'end'; + noIndicator?: boolean; + variant?: Accordion.RootProps['variant']; +} + +export const AccordionItemTrigger = React.forwardRef< + HTMLButtonElement, + AccordionItemTriggerProps +>(function AccordionItemTrigger(props, ref) { + const { children, indicatorPlacement: indicatorPlacementProp, variant, noIndicator, ...rest } = props; + + const indicatorPlacement = variant === 'faq' ? 'start' : (indicatorPlacementProp ?? 'end'); + + const indicator = variant === 'faq' ? ( + +

+ + ) : ( + + + + ); + + return ( + + { indicatorPlacement === 'start' && !noIndicator && indicator } + { children } + { indicatorPlacement === 'end' && !noIndicator && indicator } + + ); +}); + +export interface AccordionItemContentProps extends Accordion.ItemContentProps {} + +export const AccordionItemContent = React.forwardRef< + HTMLDivElement, + AccordionItemContentProps +>(function AccordionItemContent(props, ref) { + return ( + + + + ); +}); + +export const AccordionRoot = (props: Accordion.RootProps) => { + const { multiple = true, ...rest } = props; + return ; +}; + +export const AccordionItem = Accordion.Item; diff --git a/explorer/frontend/toolkit/chakra/alert.tsx b/explorer/frontend/toolkit/chakra/alert.tsx new file mode 100644 index 000000000..86b2f139d --- /dev/null +++ b/explorer/frontend/toolkit/chakra/alert.tsx @@ -0,0 +1,88 @@ +import type { AlertDescriptionProps } from '@chakra-ui/react'; +import { Alert as ChakraAlert, Icon } from '@chakra-ui/react'; +import * as React from 'react'; + +import IndicatorIcon from 'icons/info_filled.svg'; + +import { CloseButton } from './close-button'; +import { Skeleton } from './skeleton'; + +export interface AlertProps extends Omit { + startElement?: React.ReactNode; + endElement?: React.ReactNode; + descriptionProps?: AlertDescriptionProps; + title?: React.ReactNode; + icon?: React.ReactElement; + closable?: boolean; + onClose?: () => void; + loading?: boolean; + showIcon?: boolean; +} + +export const Alert = React.forwardRef( + function Alert(props, ref) { + const { + title, + children, + icon, + closable, + onClose, + startElement, + endElement, + loading, + showIcon = false, + descriptionProps, + ...rest + } = props; + + const [ isOpen, setIsOpen ] = React.useState(true); + + const defaultIcon = ; + + const iconElement = (() => { + if (startElement !== undefined) { + return startElement; + } + + if (!showIcon && icon === undefined) { + return null; + } + + return { icon || defaultIcon }; + })(); + + const handleClose = React.useCallback(() => { + setIsOpen(false); + onClose?.(); + }, [ onClose ]); + + if (closable && !isOpen) { + return null; + } + + return ( + + + { iconElement } + { children ? ( + + { title && { title } } + { children } + + ) : ( + { title } + ) } + { endElement } + { closable && ( + + ) } + + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/avatar.tsx b/explorer/frontend/toolkit/chakra/avatar.tsx new file mode 100644 index 000000000..9d323b2e8 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/avatar.tsx @@ -0,0 +1,74 @@ +'use client'; + +import type { GroupProps, SlotRecipeProps } from '@chakra-ui/react'; +import { Avatar as ChakraAvatar, Group } from '@chakra-ui/react'; +import * as React from 'react'; + +type ImageProps = React.ImgHTMLAttributes; + +export interface AvatarProps extends ChakraAvatar.RootProps { + name?: string; + src?: string; + srcSet?: string; + loading?: ImageProps['loading']; + icon?: React.ReactElement; + fallback?: React.ReactNode; +} + +export const Avatar = React.forwardRef( + function Avatar(props, ref) { + const { name, src, srcSet, loading, icon, fallback, children, ...rest } = + props; + return ( + + + { fallback } + + + { children } + + ); + }, +); + +interface AvatarFallbackProps extends ChakraAvatar.FallbackProps { + name?: string; + icon?: React.ReactElement; +} + +const AvatarFallback = React.forwardRef( + function AvatarFallback(props, ref) { + const { name, icon, children, ...rest } = props; + return ( + + { children } + { name != null && children == null && <>{ getInitials(name) } } + { name == null && children == null && ( + { icon } + ) } + + ); + }, +); + +function getInitials(name: string) { + const names = name.trim().split(' '); + const firstName = names[0] != null ? names[0] : ''; + const lastName = names.length > 1 ? names[names.length - 1] : ''; + return firstName && lastName ? + `${ firstName.charAt(0) }${ lastName.charAt(0) }` : + firstName.charAt(0); +} + +interface AvatarGroupProps extends GroupProps, SlotRecipeProps<'avatar'> {} + +export const AvatarGroup = React.forwardRef( + function AvatarGroup(props, ref) { + const { size, variant, borderless, ...rest } = props; + return ( + + + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/badge.tsx b/explorer/frontend/toolkit/chakra/badge.tsx new file mode 100644 index 000000000..d186295f0 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/badge.tsx @@ -0,0 +1,36 @@ +import type { BadgeProps as ChakraBadgeProps } from '@chakra-ui/react'; +import { chakra, Badge as ChakraBadge } from '@chakra-ui/react'; +import React from 'react'; + +import { TruncatedTextTooltip } from '../components/truncation/TruncatedTextTooltip'; +import { Skeleton } from './skeleton'; + +export interface BadgeProps extends Omit { + loading?: boolean; + startElement?: React.ReactNode; + endElement?: React.ReactNode; + truncated?: boolean; +} + +export const Badge = React.forwardRef( + function Badge(props, ref) { + const { loading, startElement, children, asChild = true, truncated = false, endElement, ...rest } = props; + + const child = { children }; + + const childrenElement = truncated ? ( + + { child } + + ) : child; + + return ( + + + { startElement } + { childrenElement } + { endElement } + + + ); + }); diff --git a/explorer/frontend/toolkit/chakra/button.tsx b/explorer/frontend/toolkit/chakra/button.tsx new file mode 100644 index 000000000..91cb0dc63 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/button.tsx @@ -0,0 +1,134 @@ +import type { ButtonProps as ChakraButtonProps, ButtonGroupProps as ChakraButtonGroupProps } from '@chakra-ui/react'; +import { + AbsoluteCenter, + Button as ChakraButton, + ButtonGroup as ChakraButtonGroup, + Span, + Spinner, +} from '@chakra-ui/react'; +import * as React from 'react'; + +import { Skeleton } from './skeleton'; + +interface ButtonLoadingProps { + loading?: boolean; + loadingText?: React.ReactNode; + loadingSkeleton?: boolean; +} + +export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps { + expanded?: boolean; + selected?: boolean; + highlighted?: boolean; +} + +export const Button = React.forwardRef( + function Button(props, ref) { + const { loading, disabled, loadingText, children, expanded, selected, highlighted, loadingSkeleton = false, ...rest } = props; + + const content = (() => { + if (loading && !loadingText) { + return ( + <> + + + + { children } + + ); + } + + if (loading && loadingText) { + return ( + <> + + { loadingText } + + ); + } + + return children; + })(); + + return ( + }> + + { content } + + + ); + }, +); + +export interface ButtonGroupProps extends ChakraButtonGroupProps {} + +export const ButtonGroup = React.forwardRef( + function ButtonGroup(props, ref) { + const { ...rest } = props; + + return ( + + ); + }, +); + +export interface ButtonGroupRadioProps extends Omit { + children: Array>; + onChange?: (value: string) => void; + defaultValue?: string; + loading?: boolean; + equalWidth?: boolean; +} + +export const ButtonGroupRadio = React.forwardRef( + function ButtonGroupRadio(props, ref) { + const { children, onChange, variant = 'segmented', defaultValue, loading = false, equalWidth = false, ...rest } = props; + + const firstChildValue = React.useMemo(() => { + const firstChild = Array.isArray(children) ? children[0] : undefined; + return typeof firstChild?.props.value === 'string' ? firstChild.props.value : undefined; + }, [ children ]); + + const [ value, setValue ] = React.useState(defaultValue ?? firstChildValue); + + const handleItemClick = React.useCallback((event: React.MouseEvent) => { + const value = event.currentTarget.value; + setValue(value); + onChange?.(value); + }, [ onChange ]); + + const clonedChildren = React.Children.map(children, (child: React.ReactElement) => { + return React.cloneElement(child, { + onClick: handleItemClick, + selected: value === child.props.value, + variant, + }); + }); + + const childrenLength = React.Children.count(children); + + return ( + + + { clonedChildren } + + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/checkbox.tsx b/explorer/frontend/toolkit/chakra/checkbox.tsx new file mode 100644 index 000000000..ed01adff1 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/checkbox.tsx @@ -0,0 +1,49 @@ +import type { Checkbox as ArkCheckbox } from '@ark-ui/react/checkbox'; +import type { HTMLChakraProps } from '@chakra-ui/react'; +import { Checkbox as ChakraCheckbox, CheckboxGroup as ChakraCheckboxGroup } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface CheckboxProps extends ChakraCheckbox.RootProps { + icon?: React.ReactNode; + inputProps?: React.InputHTMLAttributes; + rootRef?: React.Ref; +} + +export const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + const { icon, children, inputProps, rootRef, ...rest } = props; + return ( + + + + { icon || } + + { children != null && ( + { children } + ) } + + ); + }, +); + +export interface CheckboxGroupProps extends HTMLChakraProps<'div', ArkCheckbox.GroupProps> { + orientation?: 'vertical' | 'horizontal'; +} + +export const CheckboxGroup = React.forwardRef( + function CheckboxGroup(props, ref) { + const { children, orientation = 'vertical', ...rest } = props; + return ( + + { children } + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/close-button.tsx b/explorer/frontend/toolkit/chakra/close-button.tsx new file mode 100644 index 000000000..ef488ee9d --- /dev/null +++ b/explorer/frontend/toolkit/chakra/close-button.tsx @@ -0,0 +1,27 @@ +import type { ButtonProps } from '@chakra-ui/react'; +import { Icon, useRecipe } from '@chakra-ui/react'; +import * as React from 'react'; + +import CloseIcon from 'icons/close.svg'; + +import { recipe as closeButtonRecipe } from '../theme/recipes/close-button.recipe'; +import { IconButton } from './icon-button'; +export interface CloseButtonProps extends Omit { + variant?: 'plain'; + size?: 'md'; +} + +export const CloseButton = React.forwardRef< + HTMLButtonElement, + CloseButtonProps +>(function CloseButton(props, ref) { + const recipe = useRecipe({ recipe: closeButtonRecipe }); + const [ recipeProps, restProps ] = recipe.splitVariantProps(props); + const styles = recipe(recipeProps); + + return ( + + { props.children ?? } + + ); +}); diff --git a/explorer/frontend/toolkit/chakra/collapsible.tsx b/explorer/frontend/toolkit/chakra/collapsible.tsx new file mode 100644 index 000000000..058a905c8 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/collapsible.tsx @@ -0,0 +1,99 @@ +import { Flex, type FlexProps } from '@chakra-ui/react'; +import React from 'react'; +import { scroller, Element } from 'react-scroll'; + +import { useUpdateEffect } from '../hooks/useUpdateEffect'; +import type { LinkProps } from './link'; +import { Link } from './link'; + +interface CollapsibleDetailsProps extends LinkProps { + children: React.ReactNode; + id?: string; + isExpanded?: boolean; + text?: [string, string]; + noScroll?: boolean; +} + +const SCROLL_CONFIG = { + duration: 500, + smooth: true, +}; + +const CUT_ID = 'CollapsibleDetails'; + +export const CollapsibleDetails = (props: CollapsibleDetailsProps) => { + + const { children, id = CUT_ID, onClick, isExpanded: isExpandedProp = false, text: textProp, loading, noScroll, ...rest } = props; + + const [ isExpanded, setIsExpanded ] = React.useState(isExpandedProp); + + const handleClick = React.useCallback((event: React.MouseEvent) => { + setIsExpanded((flag) => !flag); + if (!noScroll) { + scroller.scrollTo(id, SCROLL_CONFIG); + } + onClick?.(event); + }, [ id, noScroll, onClick ]); + + useUpdateEffect(() => { + setIsExpanded(isExpandedProp); + isExpandedProp && !noScroll && scroller.scrollTo(id, SCROLL_CONFIG); + }, [ isExpandedProp, id, noScroll ]); + + const text = isExpanded ? (textProp?.[1] ?? 'Hide details') : (textProp?.[0] ?? 'View details'); + + return ( + <> + + { text } + + { isExpanded && children } + + ); +}; + +interface CollapsibleListProps extends FlexProps { + items: Array; + renderItem: (item: T, index: number) => React.ReactNode; + triggerProps?: LinkProps; + cutLength?: number; +} + +export const CollapsibleList = (props: CollapsibleListProps) => { + const CUT_LENGTH = 3; + + const { items, renderItem, triggerProps, cutLength = CUT_LENGTH, ...rest } = props; + + const [ isExpanded, setIsExpanded ] = React.useState(false); + + const handleToggle = React.useCallback(() => { + setIsExpanded((flag) => !flag); + }, []); + + return ( + + { items.slice(0, isExpanded ? undefined : cutLength).map(renderItem) } + { items.length > cutLength && ( + + { isExpanded ? 'Hide' : 'Show all' } + + ) } + + ); +}; diff --git a/explorer/frontend/toolkit/chakra/color-mode.tsx b/explorer/frontend/toolkit/chakra/color-mode.tsx new file mode 100644 index 000000000..ea39a3afb --- /dev/null +++ b/explorer/frontend/toolkit/chakra/color-mode.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { ThemeProvider, useTheme } from 'next-themes'; +import type { ThemeProviderProps } from 'next-themes'; +import * as React from 'react'; + +import config from 'configs/app'; + +export interface ColorModeProviderProps extends ThemeProviderProps {} + +export type ColorMode = 'light' | 'dark'; + +export function ColorModeProvider(props: ColorModeProviderProps) { + return ( + + ); +} + +export function useColorMode() { + const { resolvedTheme, setTheme } = useTheme(); + const toggleColorMode = () => { + setTheme(resolvedTheme === 'light' ? 'dark' : 'light'); + }; + return { + colorMode: resolvedTheme as ColorMode, + setColorMode: setTheme, + toggleColorMode, + }; +} + +export function useColorModeValue(light: T, dark: T) { + const { colorMode } = useColorMode(); + return colorMode === 'light' ? light : dark; +} diff --git a/explorer/frontend/toolkit/chakra/dialog.tsx b/explorer/frontend/toolkit/chakra/dialog.tsx new file mode 100644 index 000000000..6794c8c95 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/dialog.tsx @@ -0,0 +1,82 @@ +import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react'; +import * as React from 'react'; + +import { BackToButton } from '../components/buttons/BackToButton'; +import { CloseButton } from './close-button'; + +interface DialogContentProps extends ChakraDialog.ContentProps { + portalled?: boolean; + portalRef?: React.RefObject; + backdrop?: boolean; +} + +export const DialogContent = React.forwardRef< + HTMLDivElement, + DialogContentProps +>(function DialogContent(props, ref) { + const { + children, + portalled = true, + portalRef, + backdrop = true, + ...rest + } = props; + + return ( + + { backdrop && } + + + { children } + + + + ); +}); + +export const DialogCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDialog.CloseTriggerProps +>(function DialogCloseTrigger(props, ref) { + return ( + + + { props.children } + + + ); +}); + +export interface DialogHeaderProps extends ChakraDialog.HeaderProps { + startElement?: React.ReactNode; + onBackToClick?: () => void; +} + +export const DialogHeader = React.forwardRef< + HTMLDivElement, + DialogHeaderProps +>(function DialogHeader(props, ref) { + const { startElement: startElementProp, onBackToClick, ...rest } = props; + + const startElement = startElementProp ?? (onBackToClick && ); + + return ( + + { startElement } + { props.children } + + + ); +}); + +export const DialogRoot = ChakraDialog.Root; +export const DialogFooter = ChakraDialog.Footer; +export const DialogBody = ChakraDialog.Body; +export const DialogBackdrop = ChakraDialog.Backdrop; +export const DialogTitle = ChakraDialog.Title; +export const DialogDescription = ChakraDialog.Description; +export const DialogTrigger = ChakraDialog.Trigger; +export const DialogActionTrigger = ChakraDialog.ActionTrigger; diff --git a/explorer/frontend/toolkit/chakra/drawer.tsx b/explorer/frontend/toolkit/chakra/drawer.tsx new file mode 100644 index 000000000..75081fa3d --- /dev/null +++ b/explorer/frontend/toolkit/chakra/drawer.tsx @@ -0,0 +1,64 @@ +import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react'; +import * as React from 'react'; + +import { CloseButton } from './close-button'; + +interface DrawerContentProps extends ChakraDrawer.ContentProps { + portalled?: boolean; + portalRef?: React.RefObject; + offset?: ChakraDrawer.ContentProps['padding']; + backdrop?: boolean; +} + +export const DrawerContent = React.forwardRef< + HTMLDivElement, + DrawerContentProps +>(function DrawerContent(props, ref) { + const { children, portalled = true, portalRef, offset, backdrop = true, ...rest } = props; + return ( + + { backdrop && } + + + { children } + + + + ); +}); + +export const DrawerCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDrawer.CloseTriggerProps +>(function DrawerCloseTrigger(props, ref) { + return ( + + + + ); +}); + +const EMPTY_ELEMENT = () => null; + +export const DrawerRoot = (props: ChakraDrawer.RootProps) => { + const { initialFocusEl = EMPTY_ELEMENT, lazyMount = true, unmountOnExit = true, ...rest } = props; + return ; +}; + +export const DrawerTrigger = (props: ChakraDrawer.TriggerProps) => { + const { asChild = true, ...rest } = props; + return ; +}; + +export const DrawerFooter = ChakraDrawer.Footer; +export const DrawerHeader = ChakraDrawer.Header; +export const DrawerBody = ChakraDrawer.Body; +export const DrawerDescription = ChakraDrawer.Description; +export const DrawerTitle = ChakraDrawer.Title; +export const DrawerActionTrigger = ChakraDrawer.ActionTrigger; diff --git a/explorer/frontend/toolkit/chakra/field.tsx b/explorer/frontend/toolkit/chakra/field.tsx new file mode 100644 index 000000000..758ba89ce --- /dev/null +++ b/explorer/frontend/toolkit/chakra/field.tsx @@ -0,0 +1,108 @@ +import { Field as ChakraField } from '@chakra-ui/react'; +import * as React from 'react'; + +import { space } from 'toolkit/utils/htmlEntities'; + +import getComponentDisplayName from '../utils/getComponentDisplayName'; +import type { InputProps } from './input'; +import type { InputGroupProps } from './input-group'; + +export interface FieldProps extends Omit { + label?: React.ReactNode; + helperText?: React.ReactNode; + errorText?: React.ReactNode; + optionalText?: React.ReactNode; + children: React.ReactElement | React.ReactElement; + size?: 'sm' | 'md' | 'lg' | '2xl'; +} + +export const Field = React.forwardRef( + function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = props; + + // A floating field cannot be without a label. + if (rest.floating && label) { + const injectedProps = { + className: 'peer', + placeholder: ' ', + size: rest.size, + floating: rest.floating, + bgColor: rest.bgColor, + disabled: rest.disabled, + readOnly: rest.readOnly, + }; + + const labelElement = ( + + { label } + + { errorText && ( + -{ space }{ errorText } + ) } + + ); + + const helperTextElement = helperText && ( + { helperText } + ); + + const child = React.Children.only>(children); + const isInputGroup = getComponentDisplayName(child.type) === 'InputGroup'; + + if (isInputGroup) { + const inputElement = React.cloneElement( + React.Children.only>(child.props.children as React.ReactElement), + injectedProps, + ); + + const groupInputElement = React.cloneElement(child, + {}, + inputElement, + labelElement, + ); + + return ( + + { groupInputElement } + { helperTextElement } + + ); + } + + const inputElement = React.cloneElement(child, injectedProps); + + return ( + + { inputElement } + { labelElement } + { helperTextElement } + + ); + } + + // Pass size value to the input component + const injectedProps = { + size: rest.size, + }; + const child = React.Children.only>(children); + const clonedChild = React.cloneElement(child, injectedProps); + + return ( + + { label && ( + + { label } + + + ) } + { clonedChild } + { helperText && ( + { helperText } + ) } + { errorText && ( + { errorText } + ) } + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/heading.tsx b/explorer/frontend/toolkit/chakra/heading.tsx new file mode 100644 index 000000000..98700c9d9 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/heading.tsx @@ -0,0 +1,36 @@ +import type { HeadingProps as ChakraHeadingProps } from '@chakra-ui/react'; +import { Heading as ChakraHeading } from '@chakra-ui/react'; +import React from 'react'; + +export interface HeadingProps extends ChakraHeadingProps { + level?: '1' | '2' | '3'; +} + +export const Heading = React.forwardRef( + function Heading(props, ref) { + const { level, ...rest } = props; + + const textStyle = (() => { + switch (level) { + case '1': + return { base: 'heading.md', lg: 'heading.xl' }; + case '2': + return { base: 'heading.sm', lg: 'heading.lg' }; + case '3': + return { base: 'heading.xs', lg: 'heading.md' }; + } + })(); + + const as = (() => { + switch (level) { + case '1': + return 'h1'; + case '2': + return 'h2'; + case '3': + return 'h3'; + } + })(); + + return ; + }); diff --git a/explorer/frontend/toolkit/chakra/icon-button.tsx b/explorer/frontend/toolkit/chakra/icon-button.tsx new file mode 100644 index 000000000..a521692f7 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/icon-button.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +import { Button, type ButtonProps } from './button'; + +export interface IconButtonProps extends Omit { + size?: '2xs' | 'md'; +} + +export const IconButton = React.forwardRef( + function IconButton(props, ref) { + const { size, variant = 'plain', children, ...rest } = props; + + // FIXME: I have to clone the children instead of using _icon props because of style overrides + // in some pw tests for some reason the _icon style will be applied before the style of child (IconSvg component) + const child = React.Children.only(children as React.ReactElement); + const clonedChildren = size ? React.cloneElement(child, { boxSize: 5 }) : child; + + const sizeStyle = (() => { + switch (size) { + case '2xs': { + return { + _icon: { boxSize: 5 }, + boxSize: 5, + borderRadius: 'sm', + }; + } + case 'md': { + return { + _icon: { boxSize: 5 }, + boxSize: 8, + }; + } + default: + return {}; + } + })(); + + return ( + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/image.tsx b/explorer/frontend/toolkit/chakra/image.tsx new file mode 100644 index 000000000..119b8e575 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/image.tsx @@ -0,0 +1,51 @@ +import type { BoxProps, ImageProps as ChakraImageProps } from '@chakra-ui/react'; +import { Image as ChakraImage } from '@chakra-ui/react'; +import React from 'react'; + +import { Skeleton } from './skeleton'; + +export interface ImageProps extends ChakraImageProps { + fallback?: React.ReactNode; +} + +export const Image = React.forwardRef( + function Image(props, ref) { + const { fallback, src, onLoad, onError, ...rest } = props; + + const [ loading, setLoading ] = React.useState(true); + const [ error, setError ] = React.useState(false); + + const handleLoadError = React.useCallback((event: React.SyntheticEvent) => { + setError(true); + setLoading(false); + onError?.(event); + }, [ onError ]); + + const handleLoadSuccess = React.useCallback((event: React.SyntheticEvent) => { + setLoading(false); + onLoad?.(event); + }, [ onLoad ]); + + if (!src && fallback) { + return fallback; + } + + if (error) { + return fallback; + } + + return ( + <> + { loading && } + + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/input-group.tsx b/explorer/frontend/toolkit/chakra/input-group.tsx new file mode 100644 index 000000000..7f493ade9 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/input-group.tsx @@ -0,0 +1,88 @@ +import type { BoxProps, InputElementProps } from '@chakra-ui/react'; +import { Group, InputElement } from '@chakra-ui/react'; +import { debounce } from 'es-toolkit'; +import * as React from 'react'; + +import getComponentDisplayName from '../utils/getComponentDisplayName'; +import type { InputProps } from './input'; + +export interface InputGroupProps extends BoxProps { + startElementProps?: InputElementProps; + endElementProps?: InputElementProps; + startElement?: React.ReactNode; + endElement?: React.ReactNode; + children: React.ReactElement; + startOffset?: InputElementProps['paddingStart']; + endOffset?: InputElementProps['paddingEnd']; +} + +export const InputGroup = React.forwardRef( + function InputGroup(props, ref) { + const { + startElement, + startElementProps, + endElement, + endElementProps, + children, + startOffset, + endOffset, + ...rest + } = props; + + const startElementRef = React.useRef(null); + const endElementRef = React.useRef(null); + + const [ inlinePaddings, setInlinePaddings ] = React.useState<{ start?: number; end?: number }>(); + + const calculateInlinePaddings = React.useCallback(() => { + const { width: endWidth } = endElementRef?.current?.getBoundingClientRect() ?? {}; + const { width: startWidth } = startElementRef?.current?.getBoundingClientRect() ?? {}; + + setInlinePaddings({ + start: startWidth ?? 0, + end: endWidth ?? 0, + }); + }, []); + + React.useEffect(() => { + calculateInlinePaddings(); + + const resizeHandler = debounce(calculateInlinePaddings, 300); + const resizeObserver = new ResizeObserver(resizeHandler); + resizeObserver.observe(window.document.body); + + return function cleanup() { + resizeObserver.unobserve(window.document.body); + }; + }, [ calculateInlinePaddings ]); + + return ( + + { startElement && ( + + { startElement } + + ) } + { React.Children.map(children, (child: React.ReactElement) => { + if (getComponentDisplayName(child.type) !== 'FieldInput') { + return child; + } + return React.cloneElement(child, { + ...(startElement && { ps: startOffset ?? (inlinePaddings?.start ? `${ inlinePaddings.start }px` : undefined) }), + ...(endElement && { pe: endOffset ?? (inlinePaddings?.end ? `${ inlinePaddings.end }px` : undefined) }), + // hide input value and placeholder for the first render + value: inlinePaddings ? child.props.value : undefined, + placeholder: inlinePaddings ? child.props.placeholder : undefined, + }); + }) } + { endElement && ( + + { endElement } + + ) } + + ); + }, +); + +InputGroup.displayName = 'InputGroup'; diff --git a/explorer/frontend/toolkit/chakra/input.tsx b/explorer/frontend/toolkit/chakra/input.tsx new file mode 100644 index 000000000..5c7dad5b3 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/input.tsx @@ -0,0 +1,8 @@ +import type { InputProps as ChakraInputProps } from '@chakra-ui/react'; +import { Input as ChakraInput } from '@chakra-ui/react'; + +export interface InputProps extends Omit { + size?: 'sm' | 'md' | 'lg' | '2xl'; +} + +export const Input = ChakraInput; diff --git a/explorer/frontend/toolkit/chakra/link.tsx b/explorer/frontend/toolkit/chakra/link.tsx new file mode 100644 index 000000000..9972130b9 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/link.tsx @@ -0,0 +1,115 @@ +import type { LinkProps as ChakraLinkProps } from '@chakra-ui/react'; +import { Link as ChakraLink, LinkBox as ChakraLinkBox, LinkOverlay as ChakraLinkOverlay, Icon } from '@chakra-ui/react'; +import NextLink from 'next/link'; +import type { LinkProps as NextLinkProps } from 'next/link'; +import React from 'react'; + +import ArrowIcon from 'icons/link_external.svg'; + +import { Skeleton } from './skeleton'; + +export const LinkExternalIcon = ({ color }: { color?: ChakraLinkProps['color'] }) => ( + + + +); + +interface LinkPropsChakra extends ChakraLinkProps { + loading?: boolean; + external?: boolean; + iconColor?: ChakraLinkProps['color']; + noIcon?: boolean; + disabled?: boolean; +} + +interface LinkPropsNext extends Pick {} + +export interface LinkProps extends LinkPropsChakra, LinkPropsNext {} + +const splitProps = (props: LinkProps): { chakra: LinkPropsChakra; next: NextLinkProps } => { + const { scroll = true, shallow = false, prefetch = false, ...rest } = props; + + return { + chakra: rest, + next: { + href: rest.href as NextLinkProps['href'], + scroll, + shallow, + prefetch, + }, + }; +}; + +export const Link = React.forwardRef( + function Link(props, ref) { + const { chakra, next } = splitProps(props); + const { external, loading, href, children, disabled, noIcon, iconColor, ...rest } = chakra; + + if (external) { + return ( + } asChild> + + { children } + { !noIcon && } + + + ); + } + + return ( + } asChild> + + { next.href ? ( + + { children } + + ) : + { children } + } + + + ); + }, +); + +export const LinkBox = ChakraLinkBox; + +export const LinkOverlay = React.forwardRef( + function LinkOverlay(props, ref) { + const { chakra, next } = splitProps(props); + const { children, external, ...rest } = chakra; + + if (external) { + return ( + + { children } + + ); + } + + return ( + + { next.href ? { children } : children } + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/menu.tsx b/explorer/frontend/toolkit/chakra/menu.tsx new file mode 100644 index 000000000..358dca612 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/menu.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { AbsoluteCenter, Menu as ChakraMenu, Portal } from '@chakra-ui/react'; +import * as React from 'react'; +import { LuCheck, LuChevronRight } from 'react-icons/lu'; + +interface MenuContentProps extends ChakraMenu.ContentProps { + portalled?: boolean; + portalRef?: React.RefObject; +} + +export const MenuContent = React.forwardRef( + function MenuContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props; + return ( + + + + + + ); + }, +); + +export const MenuArrow = React.forwardRef< + HTMLDivElement, + ChakraMenu.ArrowProps +>(function MenuArrow(props, ref) { + return ( + + + + ); +}); + +export const MenuCheckboxItem = React.forwardRef< + HTMLDivElement, + ChakraMenu.CheckboxItemProps +>(function MenuCheckboxItem(props, ref) { + return ( + + + + + + + { props.children } + + ); +}); + +export const MenuRadioItem = React.forwardRef< + HTMLDivElement, + ChakraMenu.RadioItemProps +>(function MenuRadioItem(props, ref) { + const { children, ...rest } = props; + return ( + + + + + + + { children } + + ); +}); + +export const MenuItemGroup = React.forwardRef< + HTMLDivElement, + ChakraMenu.ItemGroupProps +>(function MenuItemGroup(props, ref) { + const { title, children, ...rest } = props; + return ( + + { title && ( + + { title } + + ) } + { children } + + ); +}); + +export interface MenuTriggerItemProps extends ChakraMenu.ItemProps { + startIcon?: React.ReactNode; +} + +export const MenuTriggerItem = React.forwardRef< + HTMLDivElement, + MenuTriggerItemProps +>(function MenuTriggerItem(props, ref) { + const { startIcon, children, ...rest } = props; + return ( + + { startIcon } + { children } + + + ); +}); + +export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup; +export const MenuContextTrigger = ChakraMenu.ContextTrigger; +export const MenuRoot = (props: ChakraMenu.RootProps) => { + const { lazyMount = true, unmountOnExit = true, ...rest } = props; + const positioning = { + placement: 'bottom-start' as const, + ...props.positioning, + offset: { + mainAxis: 4, + ...props.positioning?.offset, + }, + }; + + return ; +}; +export const MenuSeparator = ChakraMenu.Separator; + +export const MenuItem = ChakraMenu.Item; +export const MenuItemText = ChakraMenu.ItemText; +export const MenuItemCommand = ChakraMenu.ItemCommand; +export const MenuTrigger = ChakraMenu.Trigger; diff --git a/explorer/frontend/toolkit/chakra/pin-input.tsx b/explorer/frontend/toolkit/chakra/pin-input.tsx new file mode 100644 index 000000000..8f5f92393 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/pin-input.tsx @@ -0,0 +1,27 @@ +import { PinInput as ChakraPinInput, Group } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface PinInputProps extends ChakraPinInput.RootProps { + rootRef?: React.Ref; + count?: number; + inputProps?: React.InputHTMLAttributes; + attached?: boolean; +} + +export const PinInput = React.forwardRef( + function PinInput(props, ref) { + const { count = 6, inputProps, rootRef, attached, placeholder = ' ', bgColor, ...rest } = props; + return ( + + + + + { Array.from({ length: count }).map((_, index) => ( + + )) } + + + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/popover.tsx b/explorer/frontend/toolkit/chakra/popover.tsx new file mode 100644 index 000000000..e3d9dab3d --- /dev/null +++ b/explorer/frontend/toolkit/chakra/popover.tsx @@ -0,0 +1,108 @@ +import { Popover as ChakraPopover, Portal } from '@chakra-ui/react'; +import * as React from 'react'; + +import { CloseButton } from './close-button'; + +export interface PopoverContentProps extends ChakraPopover.ContentProps { + portalled?: boolean; + portalRef?: React.RefObject; +} + +export const PopoverContent = React.forwardRef< + HTMLDivElement, + PopoverContentProps +>(function PopoverContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props; + return ( + + + + + + ); +}); + +export const PopoverArrow = React.forwardRef< + HTMLDivElement, + ChakraPopover.ArrowProps +>(function PopoverArrow(props, ref) { + return ( + + + + ); +}); + +export const PopoverCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPopover.CloseTriggerProps +>(function PopoverCloseTrigger(props, ref) { + return ( + + + + ); +}); + +export const PopoverCloseTriggerWrapper = React.forwardRef< + HTMLButtonElement, + ChakraPopover.CloseTriggerProps +>(function PopoverCloseTriggerWrapper(props, ref) { + const { disabled, ...rest } = props; + + if (disabled) { + return props.children; + } + + return ( + + ); +}); + +export const PopoverRoot = (props: ChakraPopover.RootProps) => { + const positioning = { + placement: 'bottom-start' as const, + overflowPadding: 4, + ...props.positioning, + offset: { + mainAxis: 4, + ...props.positioning?.offset, + }, + }; + const { lazyMount = true, unmountOnExit = true, ...rest } = props; + + return ( + + ); +}; + +export const PopoverTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPopover.TriggerProps +>(function PopoverTrigger(props, ref) { + const { asChild = true, ...rest } = props; + return ; +}); + +export const PopoverTitle = ChakraPopover.Title; +export const PopoverDescription = ChakraPopover.Description; +export const PopoverFooter = ChakraPopover.Footer; +export const PopoverHeader = ChakraPopover.Header; +export const PopoverBody = ChakraPopover.Body; diff --git a/explorer/frontend/toolkit/chakra/progress-circle.tsx b/explorer/frontend/toolkit/chakra/progress-circle.tsx new file mode 100644 index 000000000..1f6b31fd4 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/progress-circle.tsx @@ -0,0 +1,38 @@ +import type { SystemStyleObject } from '@chakra-ui/react'; +import { + AbsoluteCenter, + ProgressCircle as ChakraProgressCircle, +} from '@chakra-ui/react'; +import * as React from 'react'; + +export interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps { + trackColor?: SystemStyleObject['stroke']; + cap?: SystemStyleObject['strokeLinecap']; +} + +export const ProgressCircleRing = React.forwardRef< + SVGSVGElement, + ProgressCircleRingProps +>(function ProgressCircleRing(props, ref) { + const { trackColor, cap, color, ...rest } = props; + return ( + + + + + ); +}); + +export const ProgressCircleValueText = React.forwardRef< + HTMLDivElement, + ChakraProgressCircle.ValueTextProps +>(function ProgressCircleValueText(props, ref) { + return ( + + + + ); +}); + +export interface ProgressCircleRootProps extends ChakraProgressCircle.RootProps {} +export const ProgressCircleRoot = ChakraProgressCircle.Root; diff --git a/explorer/frontend/toolkit/chakra/provider.tsx b/explorer/frontend/toolkit/chakra/provider.tsx new file mode 100644 index 000000000..461c3070b --- /dev/null +++ b/explorer/frontend/toolkit/chakra/provider.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { ChakraProvider } from '@chakra-ui/react'; +import React from 'react'; + +import theme from '../theme/theme'; +import { + ColorModeProvider, + type ColorModeProviderProps, +} from './color-mode'; + +export function Provider(props: ColorModeProviderProps) { + return ( + + + + ); +} diff --git a/explorer/frontend/toolkit/chakra/radio.tsx b/explorer/frontend/toolkit/chakra/radio.tsx new file mode 100644 index 000000000..832f75f9f --- /dev/null +++ b/explorer/frontend/toolkit/chakra/radio.tsx @@ -0,0 +1,26 @@ +import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface RadioProps extends ChakraRadioGroup.ItemProps { + rootRef?: React.Ref; + inputProps?: React.InputHTMLAttributes; +} + +export const Radio = React.forwardRef( + function Radio(props, ref) { + const { children, inputProps, rootRef, ...rest } = props; + return ( + + + + { children && ( + { children } + ) } + + ); + }, +); + +export interface RadioGroupProps extends ChakraRadioGroup.RootProps {} + +export const RadioGroup = ChakraRadioGroup.Root; diff --git a/explorer/frontend/toolkit/chakra/rating.tsx b/explorer/frontend/toolkit/chakra/rating.tsx new file mode 100644 index 000000000..71d12e3c1 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/rating.tsx @@ -0,0 +1,43 @@ +import { Icon, RatingGroup, useRatingGroup } from '@chakra-ui/react'; +import * as React from 'react'; + +import StarFilledIcon from 'icons/star_filled.svg'; +import StarOutlineIcon from 'icons/star_outline.svg'; + +export interface RatingProps extends Omit { + count?: number; + label?: string | Array; + defaultValue?: number; + onValueChange?: ({ value }: { value: number }) => void; + readOnly?: boolean; +} + +export const Rating = React.forwardRef( + function Rating(props, ref) { + const { count = 5, label: labelProp, defaultValue, onValueChange, readOnly, ...rest } = props; + const store = useRatingGroup({ count, defaultValue, onValueChange, readOnly }); + + const highlightedIndex = store.hovering && !readOnly ? store.hoveredValue : store.value; + const label = Array.isArray(labelProp) ? labelProp[highlightedIndex - 1] : labelProp; + + return ( + + + + { Array.from({ length: count }).map((_, index) => { + const icon = index < highlightedIndex ? + : + ; + + return ( + + + + ); + }) } + + { label && { label } } + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/select.tsx b/explorer/frontend/toolkit/chakra/select.tsx new file mode 100644 index 000000000..4361a253c --- /dev/null +++ b/explorer/frontend/toolkit/chakra/select.tsx @@ -0,0 +1,332 @@ +'use client'; + +import type { ListCollection } from '@chakra-ui/react'; +import { Box, Select as ChakraSelect, createListCollection, Flex, Portal, Icon, useSelectContext } from '@chakra-ui/react'; +import { useDebounce } from '@uidotdev/usehooks'; +import * as React from 'react'; + +import ArrowIcon from 'icons/arrows/east-mini.svg'; +import CheckIcon from 'icons/check.svg'; + +import { FilterInput } from '../components/filters/FilterInput'; +import { CloseButton } from './close-button'; +import { Skeleton } from './skeleton'; + +export interface SelectOption { + value: Value; + label: string; + icon?: React.ReactNode; +} + +export interface SelectControlProps extends ChakraSelect.ControlProps { + noIndicator?: boolean; + triggerProps?: ChakraSelect.TriggerProps; + loading?: boolean; +} + +export const SelectControl = React.forwardRef< + HTMLButtonElement, + SelectControlProps +>(function SelectControl(props, ref) { + // NOTE: here defaultValue means the "default" option of the select, not its initial value + const { children, noIndicator, triggerProps, loading, defaultValue, ...rest } = props; + + const context = useSelectContext(); + const isDefaultValue = Array.isArray(defaultValue) ? context.value.every((item) => defaultValue.includes(item)) : context.value === defaultValue; + + return ( + + + { children } + { (!noIndicator) && ( + + { !noIndicator && ( + + + + ) } + + ) } + + + ); +}); + +export const SelectClearTrigger = React.forwardRef< + HTMLButtonElement, + ChakraSelect.ClearTriggerProps +>(function SelectClearTrigger(props, ref) { + return ( + + + + ); +}); + +interface SelectContentProps extends ChakraSelect.ContentProps { + portalled?: boolean; + portalRef?: React.RefObject; +} + +export const SelectContent = React.forwardRef< + HTMLDivElement, + SelectContentProps +>(function SelectContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props; + return ( + + + + + + ); +}); + +export interface SelectItemProps extends ChakraSelect.ItemProps { + item: SelectOption; +} + +export const SelectItem = React.forwardRef< + HTMLDivElement, + SelectItemProps +>(function SelectItem(props, ref) { + const { item, children, ...rest } = props; + + const startElement = item.icon; + + return ( + + { startElement } + { children } + + + + + ); +}); + +interface SelectValueTextProps extends Omit { + children?(items: Array): React.ReactNode; + size?: SelectRootProps['size']; + required?: boolean; + invalid?: boolean; + errorText?: string; +} + +export const SelectValueText = React.forwardRef< + HTMLSpanElement, + SelectValueTextProps +>(function SelectValueText(props, ref) { + const { children, size, required, invalid, errorText, ...rest } = props; + const context = useSelectContext(); + + const content = (() => { + const items = context.selectedItems; + + const placeholder = `${ props.placeholder }${ required ? '*' : '' }${ invalid && errorText ? ` - ${ errorText }` : '' }`; + + if (items.length === 0) return placeholder; + + if (children) return children(items); + + if (items.length === 1) { + const item = items[0] as SelectOption; + + if (!item) return placeholder; + + const label = size === 'lg' ? ( + + { placeholder } + + ) : null; + + return ( + <> + { label } + + { item.icon } + + { context.collection.stringifyItem(item) } + + + + ); + } + + // FIXME: we don't have multiple selection in the select yet + return `${ items.length } selected`; + })(); + + return ( + + { content } + + ); +}); + +export interface SelectRootProps extends ChakraSelect.RootProps {} + +export const SelectRoot = React.forwardRef< + HTMLDivElement, + ChakraSelect.RootProps +>(function SelectRoot(props, ref) { + const { lazyMount = true, unmountOnExit = true, ...rest } = props; + return ( + + { props.asChild ? ( + props.children + ) : ( + <> + + { props.children } + + ) } + + ); +}) as ChakraSelect.RootComponent; + +interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps { + label: React.ReactNode; +} + +export const SelectItemGroup = React.forwardRef< + HTMLDivElement, + SelectItemGroupProps +>(function SelectItemGroup(props, ref) { + const { children, label, ...rest } = props; + return ( + + { label } + { children } + + ); +}); + +export const SelectLabel = ChakraSelect.Label; +export const SelectItemText = ChakraSelect.ItemText; + +export interface SelectProps extends SelectRootProps { + collection: ListCollection; + placeholder: string; + portalled?: boolean; + loading?: boolean; + errorText?: string; + contentProps?: SelectContentProps; +} + +export const Select = React.forwardRef((props, ref) => { + const { collection, placeholder, portalled = true, loading, errorText, contentProps, ...rest } = props; + return ( + + + + + + { collection.items.map((item: SelectOption) => ( + + { item.label } + + )) } + + + ); +}); + +export interface SelectAsyncProps extends Omit { + placeholder: string; + portalled?: boolean; + loading?: boolean; + loadOptions: (input: string, currentValue: Array) => Promise>; + extraControls?: React.ReactNode; +} + +export const SelectAsync = React.forwardRef((props, ref) => { + const { placeholder, portalled = true, loading, loadOptions, extraControls, onValueChange, errorText, ...rest } = props; + + const [ collection, setCollection ] = React.useState>(createListCollection({ items: [] })); + const [ inputValue, setInputValue ] = React.useState(''); + const [ value, setValue ] = React.useState>([]); + + const debouncedInputValue = useDebounce(inputValue, 300); + + React.useEffect(() => { + loadOptions(debouncedInputValue, value).then(setCollection); + }, [ debouncedInputValue, loadOptions, value ]); + + const handleFilterChange = React.useCallback((value: string) => { + setInputValue(value); + }, [ ]); + + const handleValueChange = React.useCallback(({ value, items }: { value: Array; items: Array }) => { + setValue(value); + onValueChange?.({ value, items }); + }, [ onValueChange ]); + + return ( + + + + + + + + { extraControls } + + { collection.items.map((item) => ( + + { item.label } + + )) } + + + ); +}); diff --git a/explorer/frontend/toolkit/chakra/skeleton.tsx b/explorer/frontend/toolkit/chakra/skeleton.tsx new file mode 100644 index 000000000..b4da4d8f0 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/skeleton.tsx @@ -0,0 +1,62 @@ +import type { + SkeletonProps as ChakraSkeletonProps, + CircleProps, +} from '@chakra-ui/react'; +import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface SkeletonCircleProps extends ChakraSkeletonProps { + size?: CircleProps['size']; +} + +export const SkeletonCircle = React.forwardRef< + HTMLDivElement, + SkeletonCircleProps +>(function SkeletonCircle(props, ref) { + const { size, ...rest } = props; + return ( + + + + ); +}); + +export interface SkeletonTextProps extends ChakraSkeletonProps { + noOfLines?: number; +} + +export const SkeletonText = React.forwardRef( + function SkeletonText(props, ref) { + const { noOfLines = 3, gap, ...rest } = props; + return ( + + { Array.from({ length: noOfLines }).map((_, index) => ( + + )) } + + ); + }, +); + +export interface SkeletonProps extends Omit { + loading: boolean | undefined; +} + +export const Skeleton = React.forwardRef( + function Skeleton(props, ref) { + const { loading = false, ...rest } = props; + return ( + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/slider.tsx b/explorer/frontend/toolkit/chakra/slider.tsx new file mode 100644 index 000000000..5e1fddca2 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/slider.tsx @@ -0,0 +1,82 @@ +import { Slider as ChakraSlider, For, HStack } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface SliderProps extends ChakraSlider.RootProps { + marks?: Array; + label?: React.ReactNode; + showValue?: boolean; +} + +export const Slider = React.forwardRef( + function Slider(props, ref) { + const { marks: marksProp, label, showValue, ...rest } = props; + const value = props.defaultValue ?? props.value; + + const marks = marksProp?.map((mark) => { + if (typeof mark === 'number') return { value: mark, label: undefined }; + return mark; + }); + + const hasMarkLabel = Boolean(marks?.some((mark) => mark.label)); + + return ( + + { label && !showValue && ( + { label } + ) } + { label && showValue && ( + + { label } + + + ) } + + + + + + + + + ); + }, +); + +function SliderThumbs(props: { value?: Array }) { + const { value } = props; + return ( + + { (_, index) => ( + + + + ) } + + ); +} + +interface SliderMarksProps { + marks?: Array; +} + +const SliderMarks = React.forwardRef( + function SliderMarks(props, ref) { + const { marks } = props; + if (!marks?.length) return null; + + return ( + + { marks.map((mark, index) => { + const value = typeof mark === 'number' ? mark : mark.value; + const label = typeof mark === 'number' ? undefined : mark.label; + return ( + + + { label } + + ); + }) } + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/switch.tsx b/explorer/frontend/toolkit/chakra/switch.tsx new file mode 100644 index 000000000..ea48c54bb --- /dev/null +++ b/explorer/frontend/toolkit/chakra/switch.tsx @@ -0,0 +1,40 @@ +import { Switch as ChakraSwitch } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface SwitchProps extends ChakraSwitch.RootProps { + inputProps?: React.InputHTMLAttributes; + labelProps?: ChakraSwitch.LabelProps; + rootRef?: React.Ref; + trackLabel?: { on: React.ReactNode; off: React.ReactNode }; + thumbLabel?: { on: React.ReactNode; off: React.ReactNode }; +} + +export const Switch = React.forwardRef( + function Switch(props, ref) { + const { inputProps, children, rootRef, trackLabel, thumbLabel, labelProps, ...rest } = + props; + + return ( + + + + + { thumbLabel && ( + + { thumbLabel?.on } + + ) } + + { trackLabel && ( + + { trackLabel.on } + + ) } + + { children != null && ( + { children } + ) } + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/table.tsx b/explorer/frontend/toolkit/chakra/table.tsx new file mode 100644 index 000000000..1587fb783 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/table.tsx @@ -0,0 +1,112 @@ +import { Table as ChakraTable, Icon } from '@chakra-ui/react'; +import { throttle } from 'es-toolkit'; +import * as React from 'react'; + +import ArrowIcon from 'icons/arrows/east.svg'; + +import { Link } from './link'; + +export const TableRoot = ChakraTable.Root; +export const TableBody = ChakraTable.Body; +export const TableHeader = ChakraTable.Header; +export const TableRow = ChakraTable.Row; + +export interface TableCellProps extends ChakraTable.CellProps { + isNumeric?: boolean; +} + +export const TableCell = (props: TableCellProps) => { + const { isNumeric, ...rest } = props; + + return ; +}; + +export interface TableColumnHeaderProps extends ChakraTable.ColumnHeaderProps { + isNumeric?: boolean; +} + +export const TableColumnHeader = (props: TableColumnHeaderProps) => { + const { isNumeric, ...rest } = props; + + return ; +}; + +export interface TableColumnHeaderSortableProps extends TableColumnHeaderProps { + sortField: F; + sortValue: string; + onSortToggle: (sortField: F) => void; + disabled?: boolean; + indicatorPosition?: 'left' | 'right'; +} + +export const TableColumnHeaderSortable = (props: TableColumnHeaderSortableProps) => { + const { sortField, sortValue, onSortToggle, children, disabled, indicatorPosition = 'left', ...rest } = props; + + const handleSortToggle = React.useCallback(() => { + onSortToggle(sortField); + }, [ onSortToggle, sortField ]); + + return ( + + + { sortValue.includes(sortField) && ( + + + + ) } + { children } + + + ); +}; + +export interface TableHeaderProps extends ChakraTable.HeaderProps { + top?: number; +} + +export const TableHeaderSticky = (props: TableHeaderProps) => { + const { top, children, ...rest } = props; + + const ref = React.useRef(null); + const [ isStuck, setIsStuck ] = React.useState(false); + + const handleScroll = React.useCallback(() => { + if (Number(ref.current?.getBoundingClientRect().y) <= (top || 0)) { + setIsStuck(true); + } else { + setIsStuck(false); + } + }, [ top ]); + + React.useEffect(() => { + const throttledHandleScroll = throttle(handleScroll, 300); + + window.addEventListener('scroll', throttledHandleScroll); + + return () => { + window.removeEventListener('scroll', throttledHandleScroll); + }; + }, [ handleScroll ]); + + return ( + + { children } + + ); +}; diff --git a/explorer/frontend/toolkit/chakra/tabs.tsx b/explorer/frontend/toolkit/chakra/tabs.tsx new file mode 100644 index 000000000..905057b3c --- /dev/null +++ b/explorer/frontend/toolkit/chakra/tabs.tsx @@ -0,0 +1,46 @@ +import { Tabs as ChakraTabs, chakra } from '@chakra-ui/react'; +import * as React from 'react'; + +export interface TabsProps extends ChakraTabs.RootProps {} + +export const TabsRoot = React.forwardRef( + function TabsRoot(props, ref) { + const { lazyMount = true, unmountOnExit = true, ...rest } = props; + return ; + }, +); + +export const TabsList = ChakraTabs.List; + +export interface TabsTriggerProps extends ChakraTabs.TriggerProps {} + +export const TabsTrigger = React.forwardRef( + function TabsTrigger(props, ref) { + return ; + }, +); + +export const TabsContent = ChakraTabs.Content; + +export interface TabsCounterProps { + count?: number | null; +} + +export const TabsCounter = ({ count }: TabsCounterProps) => { + const COUNTER_OVERLOAD = 50; + + if (count === undefined || count === null) { + return null; + } + + return ( + 0 ? 'text.secondary' : { _light: 'blackAlpha.400', _dark: 'whiteAlpha.400' } } + _groupHover={{ + color: 'inherit', + }} + > + { count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count } + + ); +}; diff --git a/explorer/frontend/toolkit/chakra/tag.tsx b/explorer/frontend/toolkit/chakra/tag.tsx new file mode 100644 index 000000000..fdd22f958 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/tag.tsx @@ -0,0 +1,73 @@ +import { chakra, Tag as ChakraTag } from '@chakra-ui/react'; +import * as React from 'react'; + +import { nbsp } from 'toolkit/utils/htmlEntities'; + +import { TruncatedTextTooltip } from '../components/truncation/TruncatedTextTooltip'; +import { CloseButton } from './close-button'; +import { Skeleton } from './skeleton'; + +export interface TagProps extends ChakraTag.RootProps { + startElement?: React.ReactNode; + endElement?: React.ReactNode; + endElementProps?: ChakraTag.EndElementProps; + label?: string; + onClose?: VoidFunction; + closable?: boolean; + truncated?: boolean; + loading?: boolean; + selected?: boolean; +} + +export const Tag = React.forwardRef( + function Tag(props, ref) { + const { + startElement, + endElement, + endElementProps, + label, + onClose, + closable = Boolean(onClose), + children, + truncated = false, + loading, + selected, + ...rest + } = props; + + const labelElement = label ? ( + { label }:{ nbsp } + ) : null; + + const contentElement = truncated ? ( + + { labelElement }{ children } + + ) : { labelElement }{ children }; + + return ( + + + { startElement && ( + { startElement } + ) } + { contentElement } + { endElement && ( + { endElement } + ) } + { closable && ( + + + + + + ) } + + + ); + }, +); diff --git a/explorer/frontend/toolkit/chakra/textarea.tsx b/explorer/frontend/toolkit/chakra/textarea.tsx new file mode 100644 index 000000000..f870da4b2 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/textarea.tsx @@ -0,0 +1,6 @@ +import type { TextareaProps as ChakraTextareaProps } from '@chakra-ui/react'; +import { Textarea as ChakraTextarea } from '@chakra-ui/react'; + +export interface TextareaProps extends ChakraTextareaProps {} + +export const Textarea = ChakraTextarea; diff --git a/explorer/frontend/toolkit/chakra/toaster.tsx b/explorer/frontend/toolkit/chakra/toaster.tsx new file mode 100644 index 000000000..85b58f321 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/toaster.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { + Toaster as ChakraToaster, + Portal, + Spinner, + Stack, + Toast, + createToaster, +} from '@chakra-ui/react'; + +import { SECOND } from 'toolkit/utils/consts'; + +import { CloseButton } from './close-button'; + +export const toaster = createToaster({ + placement: 'top-end', + pauseOnPageIdle: true, + duration: 10 * SECOND, + offsets: { + top: '12px', + right: '12px', + bottom: '12px', + left: '12px', + }, +}); + +export const Toaster = () => { + return ( + + + { (toast) => { + const closable = toast.meta?.closable !== undefined ? toast.meta.closable : true; + + return ( + + { toast.type === 'loading' ? ( + + ) : null } + + { toast.title && { toast.title } } + { toast.description && ( + { toast.description } + ) } + + { toast.action && ( + { toast.action.label } + ) } + { closable && ( + + + + ) } + + ); + } } + + + ); +}; diff --git a/explorer/frontend/toolkit/chakra/tooltip.tsx b/explorer/frontend/toolkit/chakra/tooltip.tsx new file mode 100644 index 000000000..1eb871152 --- /dev/null +++ b/explorer/frontend/toolkit/chakra/tooltip.tsx @@ -0,0 +1,159 @@ +import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'; +import { useClickAway } from '@uidotdev/usehooks'; +import * as React from 'react'; + +import config from 'configs/app'; +import useIsMobile from 'lib/hooks/useIsMobile'; + +export interface TooltipProps extends ChakraTooltip.RootProps { + selected?: boolean; + showArrow?: boolean; + portalled?: boolean; + portalRef?: React.RefObject; + content: React.ReactNode; + contentProps?: ChakraTooltip.ContentProps; + triggerProps?: ChakraTooltip.TriggerProps; + disabled?: boolean; + disableOnMobile?: boolean; +} + +export const Tooltip = React.forwardRef( + function Tooltip(props, ref) { + const { + showArrow: showArrowProp, + onOpenChange, + variant, + selected, + children, + disabled, + disableOnMobile, + portalled = true, + content, + contentProps, + portalRef, + defaultOpen = false, + lazyMount = true, + unmountOnExit = true, + triggerProps, + closeDelay = 100, + openDelay = 100, + interactive, + ...rest + } = props; + + const [ open, setOpen ] = React.useState(defaultOpen); + const timeoutRef = React.useRef(null); + + const isMobile = useIsMobile(); + + const handleOpenChange = React.useCallback((details: { open: boolean }) => { + setOpen(details.open); + onOpenChange?.(details); + }, [ onOpenChange ]); + + const handleOpenChangeManual = React.useCallback((nextOpen: boolean) => { + timeoutRef.current && window.clearTimeout(timeoutRef.current); + timeoutRef.current = window.setTimeout(() => { + setOpen(nextOpen); + onOpenChange?.({ open: nextOpen }); + }, nextOpen ? openDelay : closeDelay); + }, [ closeDelay, openDelay, onOpenChange ]); + + const handleClickAway = React.useCallback((event: Event) => { + if (interactive) { + const closest = (event.target as HTMLElement)?.closest('.chakra-tooltip__positioner'); + if (closest) { + return; + } + } + + handleOpenChangeManual(false); + }, [ interactive, handleOpenChangeManual ]); + + const triggerRef = useClickAway(handleClickAway); + + const handleTriggerClick = React.useCallback(() => { + handleOpenChangeManual(!open); + }, [ handleOpenChangeManual, open ]); + + const handleContentClick = React.useCallback((event: React.MouseEvent) => { + // otherwise, the event will be propagated to the trigger + // and if the trigger is a link, navigation will be triggered + event.stopPropagation(); + + if (interactive) { + const closestLink = (event.target as HTMLElement)?.closest('a'); + if (closestLink) { + handleOpenChangeManual(false); + } + } + }, [ interactive, handleOpenChangeManual ]); + + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + if (disabled || (disableOnMobile && isMobile)) return children; + + const defaultShowArrow = variant === 'popover' ? false : true; + const showArrow = showArrowProp !== undefined ? showArrowProp : defaultShowArrow; + + const positioning = { + ...rest.positioning, + overflowPadding: 4, + offset: { + mainAxis: 4, + ...rest.positioning?.offset, + }, + }; + + return ( + + + { children } + + + + + { showArrow && ( + + + + ) } + { content } + + + + + ); + }, +); diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx b/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx new file mode 100644 index 000000000..5cce45715 --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx @@ -0,0 +1,88 @@ +import React from 'react'; + +import type { TabsProps } from '../../chakra/tabs'; +import { TabsContent, TabsRoot } from '../../chakra/tabs'; +import { useViewportSize } from '../../hooks/useViewportSize'; +import AdaptiveTabsList, { type BaseProps as AdaptiveTabsListProps } from './AdaptiveTabsList'; +import { getTabValue } from './utils'; + +export interface Props extends TabsProps, AdaptiveTabsListProps { } + +const AdaptiveTabs = (props: Props) => { + const { + tabs, + onValueChange, + defaultValue, + isLoading, + listProps, + rightSlot, + rightSlotProps, + leftSlot, + leftSlotProps, + stickyEnabled, + size, + variant, + ...rest + } = props; + + const [ activeTab, setActiveTab ] = React.useState(defaultValue || getTabValue(tabs[0])); + + const handleTabChange = React.useCallback(({ value }: { value: string }) => { + if (isLoading) { + return; + } + onValueChange ? onValueChange({ value }) : setActiveTab(value); + }, [ isLoading, onValueChange ]); + + const viewportSize = useViewportSize(); + + React.useEffect(() => { + if (defaultValue) { + setActiveTab(defaultValue); + } + }, [ defaultValue ]); + + if (tabs.length === 1) { + return
{ tabs[0].component }
; + } + + return ( + + tab.id).join(':') } + tabs={ tabs } + listProps={ listProps } + leftSlot={ leftSlot } + leftSlotProps={ leftSlotProps } + rightSlot={ rightSlot } + rightSlotProps={ rightSlotProps } + stickyEnabled={ stickyEnabled } + activeTab={ activeTab } + isLoading={ isLoading } + variant={ variant } + /> + { tabs.map((tab) => { + const value = getTabValue(tab); + return ( + + { tab.component } + + ); + }) } + + ); +}; + +export default React.memo(AdaptiveTabs); diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabsList.tsx b/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabsList.tsx new file mode 100644 index 000000000..e87304748 --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabsList.tsx @@ -0,0 +1,204 @@ +import type { HTMLChakraProps } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import type { TabItemRegular } from './types'; + +import { useScrollDirection } from 'lib/contexts/scrollDirection'; +import useIsMobile from 'lib/hooks/useIsMobile'; + +import { useIsSticky } from '../..//hooks/useIsSticky'; +import { Skeleton } from '../../chakra/skeleton'; +import type { TabsProps } from '../../chakra/tabs'; +import { TabsCounter, TabsList, TabsTrigger } from '../../chakra/tabs'; +import AdaptiveTabsMenu from './AdaptiveTabsMenu'; +import useAdaptiveTabs from './useAdaptiveTabs'; +import useScrollToActiveTab from './useScrollToActiveTab'; +import { menuButton, getTabValue } from './utils'; + +export interface SlotProps extends HTMLChakraProps<'div'> { + widthAllocation?: 'available' | 'fixed'; +} + +export interface BaseProps { + tabs: Array; + listProps?: HTMLChakraProps<'div'> | (({ isSticky, activeTab }: { isSticky: boolean; activeTab: string }) => HTMLChakraProps<'div'>); + rightSlot?: React.ReactNode; + rightSlotProps?: SlotProps; + leftSlot?: React.ReactNode; + leftSlotProps?: SlotProps; + stickyEnabled?: boolean; + isLoading?: boolean; +} + +interface Props extends BaseProps { + activeTab: string; + variant: TabsProps['variant']; +} + +const HIDDEN_ITEM_STYLES: HTMLChakraProps<'button'> = { + position: 'absolute', + top: '-9999px', + left: '-9999px', + visibility: 'hidden', +}; + +const getItemStyles = (index: number, tabsCut: number | undefined, isLoading: boolean | undefined) => { + if (tabsCut === undefined || isLoading) { + return HIDDEN_ITEM_STYLES as never; + } + + return index < tabsCut ? {} : HIDDEN_ITEM_STYLES as never; +}; + +const getMenuStyles = (tabsLength: number, tabsCut: number | undefined, isLoading: boolean | undefined) => { + if (tabsCut === undefined || isLoading) { + return HIDDEN_ITEM_STYLES; + } + + return tabsCut >= tabsLength ? HIDDEN_ITEM_STYLES : {}; +}; + +const AdaptiveTabsList = (props: Props) => { + + const { + tabs, + activeTab, + listProps, + rightSlot, + rightSlotProps, + leftSlot, + leftSlotProps, + stickyEnabled, + isLoading, + variant, + } = props; + + const scrollDirection = useScrollDirection(); + const isMobile = useIsMobile(); + + const tabsList = React.useMemo(() => { + return [ ...tabs, menuButton ]; + }, [ tabs ]); + + const { tabsCut, tabsRefs, listRef, rightSlotRef, leftSlotRef } = useAdaptiveTabs(tabsList, isLoading || isMobile); + const isSticky = useIsSticky(listRef, 5, stickyEnabled); + const activeTabIndex = tabsList.findIndex((tab) => getTabValue(tab) === activeTab) ?? 0; + useScrollToActiveTab({ activeTabIndex, listRef, tabsRefs, isMobile, isLoading }); + + const isReady = !isLoading && tabsCut !== undefined; + + return ( + + { leftSlot && ( + + { leftSlot } + + ) + } + { tabsList.map((tab, index) => { + const value = getTabValue(tab); + const ref = tabsRefs[index]; + + if (tab.id === 'menu') { + return ( + 0 && tabsCut !== undefined && tabsCut > 0 && activeTabIndex >= tabsCut } + { ...getMenuStyles(tabs.length, tabsCut, isLoading) } + /> + ); + } + + return ( + + { typeof tab.title === 'function' ? tab.title() : tab.title } + + + ); + }) } + { tabs.slice(0, isReady ? 0 : 5).map((tab, index) => { + const value = `${ getTabValue(tab) }-pre`; + return ( + + + { typeof tab.title === 'function' ? tab.title() : tab.title } + + + + ); + }) } + { + rightSlot ? ( + + { rightSlot } + + ) : + null + } + + ); +}; + +export default React.memo(AdaptiveTabsList); diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabsMenu.tsx b/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabsMenu.tsx new file mode 100644 index 000000000..696f7f25f --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/AdaptiveTabsMenu.tsx @@ -0,0 +1,76 @@ +import { Icon } from '@chakra-ui/react'; +import React from 'react'; + +import type { TabItem } from './types'; + +import DotsIcon from 'icons/dots.svg'; + +import { IconButton } from '../../chakra/icon-button'; +import type { IconButtonProps } from '../../chakra/icon-button'; +import { PopoverBody, PopoverCloseTriggerWrapper, PopoverContent, PopoverRoot, PopoverTrigger } from '../../chakra/popover'; +import { TabsCounter, TabsTrigger } from '../../chakra/tabs'; +import { getTabValue } from './utils'; + +interface Props extends IconButtonProps { + tabs: Array; + tabsCut: number; + isActive: boolean; +} + +const AdaptiveTabsMenu = ({ tabs, tabsCut, isActive, ...props }: Props, ref: React.Ref) => { + + return ( + + + + + + + + + { tabs.slice(tabsCut).map((tab) => { + const value = getTabValue(tab); + + return ( + + + { typeof tab.title === 'function' ? tab.title() : tab.title } + + + + ); + }) } + + + + ); +}; + +export default React.memo(React.forwardRef(AdaptiveTabsMenu)); diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/index.ts b/explorer/frontend/toolkit/components/AdaptiveTabs/index.ts new file mode 100644 index 000000000..1ea5116df --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/index.ts @@ -0,0 +1,4 @@ +export type { TabItemRegular, TabItemMenu, SubTabItem } from './types'; +export type { Props } from './AdaptiveTabs'; + +export { default } from './AdaptiveTabs'; diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/types.ts b/explorer/frontend/toolkit/components/AdaptiveTabs/types.ts new file mode 100644 index 000000000..bc3132ad2 --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/types.ts @@ -0,0 +1,22 @@ +import type React from 'react'; + +export interface TabItemRegular { + // NOTE, in case of array of ids, when switching tabs, the first id will be used + // switching between other ids should be handled in the underlying component + id: string | Array; + title: string | (() => React.ReactNode); + count?: number | null; + component: React.ReactNode; + subTabs?: Array; +} + +export interface TabItemMenu { + id: 'menu'; + title: string; + count?: never; + component: null; +} + +export type TabItem = TabItemRegular | TabItemMenu; + +export type SubTabItem = Omit; diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/useAdaptiveTabs.tsx b/explorer/frontend/toolkit/components/AdaptiveTabs/useAdaptiveTabs.tsx new file mode 100644 index 000000000..bf3be30f8 --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/useAdaptiveTabs.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import type { TabItem } from './types'; + +export default function useAdaptiveTabs(tabs: Array, disabled?: boolean) { + // to avoid flickering we set initial value to undefined + // so there will be no displayed tabs initially + const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : undefined); + const [ tabsRefs, setTabsRefs ] = React.useState>>([]); + const listRef = React.useRef(null); + const rightSlotRef = React.useRef(null); + const leftSlotRef = React.useRef(null); + + const calculateCut = React.useCallback(() => { + const listWidth = listRef.current?.getBoundingClientRect().width; + const rightSlotWidth = rightSlotRef.current?.getBoundingClientRect().width || 0; + const leftSlotWidth = leftSlotRef.current?.getBoundingClientRect().width || 0; + const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width); + const menuWidth = tabWidths[tabWidths.length - 1]; + + if (!listWidth || !menuWidth) { + return tabs.length; + } + + const { visibleNum } = tabWidths.slice(0, -1).reduce((result, item, index, array) => { + if (!item) { + return result; + } + + if (result.visibleNum < index) { + // means that we haven't increased visibleNum on the previous iteration, so there is no space left + // we skip now till the end of the loop + return result; + } + + if (index === array.length - 1) { + // last element + if (result.accWidth + item < listWidth - rightSlotWidth - leftSlotWidth) { + return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; + } + } else { + if (result.accWidth + item + menuWidth < listWidth - rightSlotWidth - leftSlotWidth) { + return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; + } + } + + return result; + }, { visibleNum: 0, accWidth: 0 }); + + return visibleNum; + }, [ tabs.length, tabsRefs ]); + + React.useEffect(() => { + setTabsRefs(tabs.map((_, index) => tabsRefs[index] || React.createRef())); + setTabsCut(disabled ? tabs.length : undefined); + // update refs only when disabled prop changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ disabled ]); + + React.useEffect(() => { + if (tabsRefs.length > 0 && !disabled) { + setTabsCut(calculateCut()); + } + }, [ calculateCut, disabled, tabsRefs ]); + + return React.useMemo(() => { + return { + tabsCut, + tabsRefs, + listRef, + rightSlotRef, + leftSlotRef, + }; + }, [ tabsCut, tabsRefs ]); +} diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/useScrollToActiveTab.tsx b/explorer/frontend/toolkit/components/AdaptiveTabs/useScrollToActiveTab.tsx new file mode 100644 index 000000000..777aece43 --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/useScrollToActiveTab.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +interface Props { + activeTabIndex: number; + tabsRefs: Array>; + listRef: React.RefObject; + isMobile?: boolean; + isLoading?: boolean; +} + +export default function useScrollToActiveTab({ activeTabIndex, tabsRefs, listRef, isMobile, isLoading }: Props) { + React.useEffect(() => { + if (isLoading) { + return; + } + + if (activeTabIndex < tabsRefs.length && isMobile) { + window.setTimeout(() => { + const activeTabRef = tabsRefs[activeTabIndex]; + + if (activeTabRef.current && listRef.current) { + const containerWidth = listRef.current.getBoundingClientRect().width; + const activeTabWidth = activeTabRef.current.getBoundingClientRect().width; + const left = tabsRefs.slice(0, activeTabIndex) + .map((tab) => tab.current?.getBoundingClientRect()) + .filter(Boolean) + .map((rect) => rect.width) + .reduce((result, item) => result + item, 0); + + const isWithinFirstPage = containerWidth > left + activeTabWidth; + + if (isWithinFirstPage) { + listRef.current.scrollTo({ left: 0 }); + return; + } + + listRef.current.scrollTo({ + left, + behavior: 'smooth', + }); + } + + // have to wait until DOM is updated and all styles to tabs is applied + }, 300); + } + // run only when tab index or device type is changed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ activeTabIndex, isMobile, isLoading ]); +} diff --git a/explorer/frontend/toolkit/components/AdaptiveTabs/utils.ts b/explorer/frontend/toolkit/components/AdaptiveTabs/utils.ts new file mode 100644 index 000000000..fd9387925 --- /dev/null +++ b/explorer/frontend/toolkit/components/AdaptiveTabs/utils.ts @@ -0,0 +1,17 @@ +import type { TabItem, TabItemMenu } from './types'; + +import { middot } from 'toolkit/utils/htmlEntities'; + +export const menuButton: TabItemMenu = { + id: 'menu', + title: `${ middot }${ middot }${ middot }`, + component: null, +}; + +export const getTabValue = (tab: TabItem): string => { + if (Array.isArray(tab.id)) { + return tab.id[0]; + } + + return tab.id; +}; diff --git a/explorer/frontend/toolkit/components/Hint/Hint.tsx b/explorer/frontend/toolkit/components/Hint/Hint.tsx new file mode 100644 index 000000000..c9b06cc31 --- /dev/null +++ b/explorer/frontend/toolkit/components/Hint/Hint.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@chakra-ui/react'; +import React from 'react'; + +import InfoIcon from 'icons/info.svg'; + +import type { IconButtonProps } from '../../chakra/icon-button'; +import { IconButton } from '../../chakra/icon-button'; +import type { TooltipProps } from '../../chakra/tooltip'; +import { Tooltip } from '../../chakra/tooltip'; + +interface Props extends IconButtonProps { + label: string | React.ReactNode; + tooltipProps?: Partial; + isLoading?: boolean; + as?: React.ElementType; +} + +export const Hint = React.memo(({ label, tooltipProps, isLoading, boxSize = 5, ...rest }: Props) => { + return ( + + + + + + + + ); +}); diff --git a/explorer/frontend/toolkit/components/RoutedTabs/RoutedTabs.tsx b/explorer/frontend/toolkit/components/RoutedTabs/RoutedTabs.tsx new file mode 100644 index 000000000..a1b325b57 --- /dev/null +++ b/explorer/frontend/toolkit/components/RoutedTabs/RoutedTabs.tsx @@ -0,0 +1,63 @@ +import { pickBy } from 'es-toolkit'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { Props as AdaptiveTabsProps } from '../AdaptiveTabs/AdaptiveTabs'; +import AdaptiveTabs from '../AdaptiveTabs/AdaptiveTabs'; +import { getTabValue } from '../AdaptiveTabs/utils'; +import useActiveTabFromQuery from './useActiveTabFromQuery'; + +interface Props extends AdaptiveTabsProps {} + +const RoutedTabs = (props: Props) => { + const { tabs, onValueChange, ...rest } = props; + + const router = useRouter(); + const activeTab = useActiveTabFromQuery(props.tabs); + const tabsRef = React.useRef(null); + + const handleValueChange = React.useCallback(({ value }: { value: string }) => { + const nextTab = tabs.find((tab) => getTabValue(tab) === value); + + if (!nextTab) { + return; + } + + const queryForPathname = pickBy(router.query, (_, key) => router.pathname.includes(`[${ key }]`)); + router.push( + { pathname: router.pathname, query: { ...queryForPathname, tab: value } }, + undefined, + { shallow: true }, + ); + + onValueChange?.({ value }); + }, [ tabs, router, onValueChange ]); + + React.useEffect(() => { + if (router.query.scroll_to_tabs) { + tabsRef?.current?.scrollIntoView(true); + delete router.query.scroll_to_tabs; + router.push( + { + pathname: router.pathname, + query: router.query, + }, + undefined, + { shallow: true }, + ); + } + // replicate componentDidMount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +}; + +export default React.memo(RoutedTabs); diff --git a/explorer/frontend/toolkit/components/RoutedTabs/index.ts b/explorer/frontend/toolkit/components/RoutedTabs/index.ts new file mode 100644 index 000000000..081aa5beb --- /dev/null +++ b/explorer/frontend/toolkit/components/RoutedTabs/index.ts @@ -0,0 +1,2 @@ +export { default as RoutedTabs } from './RoutedTabs'; +export { default as useActiveTabFromQuery } from './useActiveTabFromQuery'; diff --git a/explorer/frontend/toolkit/components/RoutedTabs/useActiveTabFromQuery.tsx b/explorer/frontend/toolkit/components/RoutedTabs/useActiveTabFromQuery.tsx new file mode 100644 index 000000000..b5c51a076 --- /dev/null +++ b/explorer/frontend/toolkit/components/RoutedTabs/useActiveTabFromQuery.tsx @@ -0,0 +1,22 @@ +import { useRouter } from 'next/router'; + +import type { TabItem } from '../AdaptiveTabs/types'; + +import { castToString } from '../../utils/guards'; + +export default function useActiveTabFromQuery(tabs: Array) { + const router = useRouter(); + const tabFromQuery = castToString(router.query.tab); + + if (!tabFromQuery) { + return; + } + + return tabs.find((tab) => { + if (Array.isArray(tab.id)) { + return tab.id.includes(tabFromQuery); + } + + return tab.id === tabFromQuery || ('subTabs' in tab && tab.subTabs?.some((id) => id === tabFromQuery)); + }); +} diff --git a/explorer/frontend/toolkit/components/buttons/BackToButton.tsx b/explorer/frontend/toolkit/components/buttons/BackToButton.tsx new file mode 100644 index 000000000..d2b8c72ee --- /dev/null +++ b/explorer/frontend/toolkit/components/buttons/BackToButton.tsx @@ -0,0 +1,36 @@ +import { Icon } from '@chakra-ui/react'; +import React from 'react'; + +import ArrowIcon from 'icons/arrows/east.svg'; + +import type { IconButtonProps } from '../../chakra/icon-button'; +import { IconButton } from '../../chakra/icon-button'; +import { Link } from '../../chakra/link'; +import { Tooltip } from '../../chakra/tooltip'; + +export interface BackToButtonProps extends IconButtonProps { + href?: string; + hint?: string; +} + +export const BackToButton = ({ href, hint, boxSize = 6, ...rest }: BackToButtonProps) => { + + const button = ( + + + + + + ); + + return ( + + { href ? { button } : button } + + ); +}; diff --git a/explorer/frontend/toolkit/components/buttons/ClearButton.tsx b/explorer/frontend/toolkit/components/buttons/ClearButton.tsx new file mode 100644 index 000000000..88a4e0669 --- /dev/null +++ b/explorer/frontend/toolkit/components/buttons/ClearButton.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import type { CloseButtonProps } from '../../chakra/close-button'; +import { CloseButton } from '../../chakra/close-button'; + +export interface ClearButtonProps extends CloseButtonProps { + visible?: boolean; +} + +export const ClearButton = ({ disabled, visible = true, ...rest }: ClearButtonProps) => { + return ( + + ); +}; diff --git a/explorer/frontend/toolkit/components/filters/FilterInput.tsx b/explorer/frontend/toolkit/components/filters/FilterInput.tsx new file mode 100644 index 000000000..99842e588 --- /dev/null +++ b/explorer/frontend/toolkit/components/filters/FilterInput.tsx @@ -0,0 +1,90 @@ +import { Icon } from '@chakra-ui/react'; +import type { ChangeEvent } from 'react'; +import React, { useCallback, useState } from 'react'; + +import SearchIcon from 'icons/search.svg'; + +import type { InputProps } from '../../chakra/input'; +import { Input } from '../../chakra/input'; +import { InputGroup } from '../../chakra/input-group'; +import type { SkeletonProps } from '../../chakra/skeleton'; +import { Skeleton } from '../../chakra/skeleton'; +import { ClearButton } from '../buttons/ClearButton'; + +export interface FilterInputProps extends Omit { + onChange?: (searchTerm: string) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; + loading?: boolean; + size?: 'sm' | 'md' | 'lg'; + placeholder: string; + initialValue?: string; + type?: React.HTMLInputTypeAttribute; + name?: string; + inputProps?: InputProps; +}; + +export const FilterInput = ({ + onChange, + size = 'sm', + placeholder, + initialValue, + type, + name, + loading = false, + onFocus, + onBlur, + inputProps, + ...rest +}: FilterInputProps) => { + const [ filterQuery, setFilterQuery ] = useState(initialValue || ''); + const inputRef = React.useRef(null); + + const handleFilterQueryChange = useCallback((event: ChangeEvent) => { + const { value } = event.target; + + setFilterQuery(value); + onChange?.(value); + }, [ onChange ]); + + const handleFilterQueryClear = useCallback(() => { + setFilterQuery(''); + onChange?.(''); + inputRef?.current?.focus(); + }, [ onChange ]); + + const startElement = ; + const endElement = 0 }/>; + + return ( + + + + + + ); +}; diff --git a/explorer/frontend/toolkit/components/forms/components/FormFieldError.tsx b/explorer/frontend/toolkit/components/forms/components/FormFieldError.tsx new file mode 100644 index 000000000..0e55a41e6 --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/components/FormFieldError.tsx @@ -0,0 +1,11 @@ +import { Box, chakra } from '@chakra-ui/react'; +import React from 'react'; + +interface Props { + message: string; + className?: string; +} + +export const FormFieldError = chakra(({ message, className }: Props) => { + return { message }; +}); diff --git a/explorer/frontend/toolkit/components/forms/components/index.ts b/explorer/frontend/toolkit/components/forms/components/index.ts new file mode 100644 index 000000000..5081dfdf1 --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/components/index.ts @@ -0,0 +1 @@ +export * from './FormFieldError'; diff --git a/explorer/frontend/toolkit/components/forms/fields/FormFieldAddress.tsx b/explorer/frontend/toolkit/components/forms/fields/FormFieldAddress.tsx new file mode 100644 index 000000000..1e56b0112 --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/fields/FormFieldAddress.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type { FieldValues } from 'react-hook-form'; + +import type { FormFieldPropsBase } from './types'; +import type { PartialBy } from 'types/utils'; + +import { addressValidator } from '../validators/address'; +import { FormFieldText } from './FormFieldText'; + +const FormFieldAddressContent = ( + props: PartialBy, 'placeholder'>, +) => { + const rules = React.useMemo( + () => ({ + ...props.rules, + validate: { + ...props.rules?.validate, + address: addressValidator, + }, + }), + [ props.rules ], + ); + + return ( + + ); +}; + +export const FormFieldAddress = React.memo(FormFieldAddressContent) as typeof FormFieldAddressContent; diff --git a/explorer/frontend/toolkit/components/forms/fields/FormFieldCheckbox.tsx b/explorer/frontend/toolkit/components/forms/fields/FormFieldCheckbox.tsx new file mode 100644 index 000000000..9d0aa1939 --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/fields/FormFieldCheckbox.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useController, useFormContext, type FieldValues, type Path } from 'react-hook-form'; + +import type { FormFieldPropsBase } from './types'; + +import { Checkbox } from '../../../chakra/checkbox'; +import type { CheckboxProps } from '../../../chakra/checkbox'; + +interface Props< + FormFields extends FieldValues, + Name extends Path = Path, +> extends Pick, 'rules' | 'name' | 'onChange' | 'readOnly'>, Omit { + label: string; +} + +const FormFieldCheckboxContent = < + FormFields extends FieldValues, + Name extends Path = Path, +>({ + name, + label, + rules, + onChange, + readOnly, + ...rest +}: Props) => { + const { control } = useFormContext(); + const { field, formState } = useController({ + control, + name, + rules, + }); + + const isDisabled = formState.isSubmitting; + + const handleChange: typeof field.onChange = React.useCallback(({ checked }: { checked: boolean }) => { + field.onChange(checked); + onChange?.(); + }, [ field, onChange ]); + + return ( + + { label } + + ); +}; + +export const FormFieldCheckbox = React.memo(FormFieldCheckboxContent) as typeof FormFieldCheckboxContent; diff --git a/explorer/frontend/toolkit/components/forms/fields/FormFieldColor.tsx b/explorer/frontend/toolkit/components/forms/fields/FormFieldColor.tsx new file mode 100644 index 000000000..8203cc81f --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/fields/FormFieldColor.tsx @@ -0,0 +1,108 @@ +import type { BoxProps } from '@chakra-ui/react'; +import { Circle } from '@chakra-ui/react'; +import React from 'react'; +import type { FieldValues, Path } from 'react-hook-form'; +import { useController, useFormContext } from 'react-hook-form'; + +import type { FormFieldPropsBase } from './types'; + +import { Field } from '../../../chakra/field'; +import type { InputProps } from '../../../chakra/input'; +import { Input } from '../../../chakra/input'; +import { InputGroup } from '../../../chakra/input-group'; +import { getFormFieldErrorText } from '../utils/getFormFieldErrorText'; +import { colorValidator } from '../validators/color'; + +interface Props< + FormFields extends FieldValues, + Name extends Path = Path, +> extends FormFieldPropsBase { + sampleDefaultBgColor?: BoxProps['bgColor']; +} + +const FormFieldColorContent = < + FormFields extends FieldValues, + Name extends Path = Path, +>({ + name, + placeholder, + rules, + onBlur, + group, + inputProps, + size = 'lg', + disabled, + sampleDefaultBgColor, + ...restProps +}: Props) => { + const { control } = useFormContext(); + const { field, fieldState, formState } = useController({ + control, + name, + rules: { + ...rules, + required: restProps.required, + validate: colorValidator, + maxLength: 7, + }, + }); + + const [ value, setValue ] = React.useState(''); + + const handleChange = React.useCallback((event: React.ChangeEvent) => { + const nextValue = (() => { + const value = event.target.value; + if (value) { + if (value.length === 1 && value[0] !== '#') { + return `#${ value }`; + } + } + return value; + })(); + setValue(nextValue); + field.onChange(nextValue); + }, [ field ]); + + const handleBlur = React.useCallback(() => { + field.onBlur(); + onBlur?.(); + }, [ field, onBlur ]); + + const endElement = ( + + ); + + return ( + + + + + + ); +}; + +export const FormFieldColor = React.memo(FormFieldColorContent) as typeof FormFieldColorContent; diff --git a/explorer/frontend/toolkit/components/forms/fields/FormFieldEmail.tsx b/explorer/frontend/toolkit/components/forms/fields/FormFieldEmail.tsx new file mode 100644 index 000000000..c7e4851ff --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/fields/FormFieldEmail.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import type { FieldValues } from 'react-hook-form'; + +import type { FormFieldPropsBase } from './types'; +import type { PartialBy } from 'types/utils'; + +import { EMAIL_REGEXP } from '../validators/email'; +import { FormFieldText } from './FormFieldText'; + +const FormFieldEmailContent = ( + props: PartialBy, 'placeholder'>, +) => { + const rules = React.useMemo( + () => ({ + ...props.rules, + pattern: EMAIL_REGEXP, + }), + [ props.rules ], + ); + + return ( + + ); +}; + +export const FormFieldEmail = React.memo(FormFieldEmailContent) as typeof FormFieldEmailContent; diff --git a/explorer/frontend/toolkit/components/forms/fields/FormFieldSelect.tsx b/explorer/frontend/toolkit/components/forms/fields/FormFieldSelect.tsx new file mode 100644 index 000000000..d91eade89 --- /dev/null +++ b/explorer/frontend/toolkit/components/forms/fields/FormFieldSelect.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import type { Path, FieldValues } from 'react-hook-form'; +import { useController, useFormContext } from 'react-hook-form'; + +import type { FormFieldPropsBase } from './types'; + +import type { SelectProps } from '../../../chakra/select'; +import { Select } from '../../../chakra/select'; +import { getFormFieldErrorText } from '../utils/getFormFieldErrorText'; + +type Props< + FormFields extends FieldValues, + Name extends Path, +> = FormFieldPropsBase & SelectProps; + +const FormFieldSelectContent = < + FormFields extends FieldValues, + Name extends Path, +>(props: Props) => { + const { name, rules, size = 'lg', ...rest } = props; + + const { control } = useFormContext(); + const { field, fieldState, formState } = useController({ + control, + name, + rules, + }); + + const isDisabled = formState.isSubmitting; + + const handleChange = React.useCallback(({ value }: { value: Array }) => { + field.onChange(value); + }, [ field ]); + + const handleBlur = React.useCallback(() => { + field.onBlur(); + }, [ field ]); + + return ( +