Commit 0fb327b3 authored by 贾浩@五瓣科技's avatar 贾浩@五瓣科技

init

parents

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

{
"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"
}
}
}
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
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
FAVICON_GENERATOR_API_KEY=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
\ No newline at end of file
node_modules
node_modules_linux
playwright/envs.js
deploy/tools/envs-validator/index.js
deploy/tools/feature-reporter/build/**
deploy/tools/feature-reporter/index.js
public/**
\ No newline at end of file
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' },
],
patterns: [
'icons/*',
],
};
module.exports = {
env: {
es6: true,
browser: true,
node: true,
},
'extends': [
'next/core-web-vitals',
'eslint:recommended',
'plugin:react/recommended',
'plugin:regexp/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:playwright/playwright-test',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
plugins: [
'es5',
'react',
'regexp',
'@typescript-eslint',
'react-hooks',
'jsx-a11y',
'eslint-plugin-import-helpers',
'jest',
'eslint-plugin-no-cyrillic-string',
'@tanstack/query',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
pragma: 'React',
version: 'detect',
},
},
rules: {
'@typescript-eslint/array-type': [ 'error', {
'default': 'generic',
readonly: 'generic',
} ],
'@typescript-eslint/brace-style': [ 'error', '1tbs' ],
'@typescript-eslint/consistent-type-imports': [ 'error' ],
'@typescript-eslint/indent': [ 'error', 2 ],
'@typescript-eslint/member-delimiter-style': [ 'error' ],
'@typescript-eslint/naming-convention': [ 'error',
{
selector: 'default',
format: [ 'camelCase' ],
leadingUnderscore: 'allow',
trailingUnderscore: 'forbid',
},
{
selector: 'class',
format: [ 'PascalCase' ],
},
{
selector: 'enum',
format: [ 'PascalCase', 'UPPER_CASE' ],
},
{
selector: 'enumMember',
format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
},
{
selector: 'function',
format: [ 'camelCase', 'PascalCase' ],
},
{
selector: 'interface',
format: [ 'PascalCase' ],
},
{
selector: 'method',
format: [ 'camelCase', 'snake_case', 'UPPER_CASE' ],
leadingUnderscore: 'allow',
},
{
selector: 'parameter',
format: [ 'camelCase', 'PascalCase' ],
leadingUnderscore: 'allow',
},
{
selector: 'property',
format: null,
},
{
selector: 'typeAlias',
format: [ 'PascalCase' ],
},
{
selector: 'typeParameter',
format: [ 'PascalCase', 'UPPER_CASE' ],
},
{
selector: 'variable',
format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
leadingUnderscore: 'allow',
},
],
'@typescript-eslint/no-duplicate-imports': [ 'error' ],
'@typescript-eslint/no-empty-function': [ 'off' ],
'@typescript-eslint/no-unused-vars': [ 'error', { ignoreRestSiblings: true } ],
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-useless-constructor': [ 'error' ],
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/no-explicit-any': [ 'error', { ignoreRestArgs: true } ],
// disabled in favor of @typescript-eslint
'brace-style': 'off',
camelcase: 'off',
indent: 'off',
'no-unused-vars': 'off',
'no-use-before-define': 'off',
'no-useless-constructor': 'off',
'array-bracket-spacing': [ 'error', 'always' ],
'arrow-spacing': [ 'error', { before: true, after: true } ],
'comma-dangle': [ 'error', 'always-multiline' ],
'comma-spacing': [ 'error' ],
'comma-style': [ 'error', 'last' ],
curly: [ 'error', 'all' ],
'eol-last': 'error',
eqeqeq: [ 'error', 'allow-null' ],
'id-match': [ 'error', '^[\\w$]+$' ],
'jsx-quotes': [ 'error', 'prefer-double' ],
'key-spacing': [ 'error', {
beforeColon: false,
afterColon: true,
} ],
'keyword-spacing': 'error',
'linebreak-style': [ 'error', 'unix' ],
'lines-around-comment': [ 'error', {
beforeBlockComment: true,
allowBlockStart: true,
} ],
'max-len': [ 'error', 160, 4 ],
'no-console': 'error',
'no-empty': [ 'error', { allowEmptyCatch: true } ],
'no-implicit-coercion': [ 'error', {
number: true,
'boolean': true,
string: true,
} ],
'no-mixed-operators': [ 'error', {
groups: [
[ '&&', '||' ],
],
} ],
'no-mixed-spaces-and-tabs': 'error',
'no-multiple-empty-lines': [ 'error', {
max: 1,
maxEOF: 0,
maxBOF: 0,
} ],
'no-multi-spaces': 'error',
'no-multi-str': 'error',
'no-nested-ternary': 'error',
'no-trailing-spaces': 'error',
'no-spaced-func': 'error',
'no-with': 'error',
'object-curly-spacing': [ 'error', 'always' ],
'object-shorthand': 'off',
'one-var': [ 'error', 'never' ],
'operator-linebreak': [ 'error', 'after' ],
'prefer-const': 'error',
'quote-props': [ 'error', 'as-needed', {
keywords: true,
numbers: true,
} ],
quotes: [ 'error', 'single', {
allowTemplateLiterals: true,
} ],
'space-before-function-paren': [ 'error', 'never' ],
'space-before-blocks': [ 'error', 'always' ],
'space-in-parens': [ 'error', 'never' ],
'space-infix-ops': 'error',
'space-unary-ops': 'off',
'template-curly-spacing': [ 'error', 'always' ],
'wrap-iife': [ 'error', 'inside' ],
semi: [ 'error', 'always' ],
'import-helpers/order-imports': [
'error',
{
newlinesBetween: 'always',
groups: [
'module',
'/types/',
[
'/^nextjs/',
],
[
'/^configs/',
'/^data/',
'/^deploy/',
'/^icons/',
'/^jest/',
'/^lib/',
'/^mocks/',
'/^pages/',
'/^playwright/',
'/^stubs/',
'/^theme/',
'/^ui/',
],
[ 'parent', 'sibling', 'index' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
},
],
'no-restricted-imports': [ 'error', RESTRICTED_MODULES ],
'no-restricted-properties': [ 2, {
object: 'process',
property: 'env',
// FIXME: restrict the rule only NEXT_PUBLIC variables
message: 'Please use configs/app/index.ts to import any NEXT_PUBLIC environment variables. For other properties please disable this rule for a while.',
} ],
'react/jsx-key': 'error',
'react/jsx-no-bind': [ 'error', {
ignoreRefs: true,
} ],
'react/jsx-curly-brace-presence': [ 'error', {
props: 'never',
children: 'never',
} ],
'react/jsx-curly-spacing': [ 'error', {
when: 'always',
children: true,
spacing: {
objectLiterals: 'never',
},
} ],
'react/jsx-equals-spacing': [ 'error', 'never' ],
'react/jsx-fragments': [ 'error', 'syntax' ],
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-target-blank': 'off',
'react/jsx-no-useless-fragment': 'error',
'react/jsx-tag-spacing': [ 'error', {
afterOpening: 'never',
beforeSelfClosing: 'never',
closingSlash: 'never',
} ],
'react/jsx-wrap-multilines': [ 'error', {
declaration: 'parens-new-line',
assignment: 'parens-new-line',
'return': 'parens-new-line',
arrow: 'parens-new-line',
condition: 'parens-new-line',
logical: 'parens-new-line',
prop: 'parens-new-line',
} ],
'react/no-access-state-in-setstate': 'error',
'react/no-deprecated': 'error',
'react/no-direct-mutation-state': 'error',
'react/no-find-dom-node': 'off',
'react/no-redundant-should-component-update': 'error',
'react/no-render-return-value': 'error',
'react/no-string-refs': 'off',
'react/no-unknown-property': 'error',
'react/no-unused-state': 'error',
'react/require-optimization': [ 'error' ],
'react/void-dom-elements-no-children': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'regexp/confusing-quantifier': 'error',
'regexp/control-character-escape': 'error',
'regexp/negation': 'error',
'regexp/no-dupe-disjunctions': 'error',
'regexp/no-empty-alternative': 'error',
'regexp/no-empty-capturing-group': 'error',
'regexp/no-lazy-ends': 'error',
'regexp/no-obscure-range': [ 'error', {
allowed: [ 'alphanumeric' ],
} ],
'regexp/no-optional-assertion': 'error',
'regexp/no-unused-capturing-group': [ 'error', {
fixable: true,
} ],
'regexp/no-useless-character-class': 'error',
'regexp/no-useless-dollar-replacements': 'error',
'no-cyrillic-string/no-cyrillic-string': 'error',
},
overrides: [
{
files: [ '*.js', '*.jsx' ],
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: [
'*.config.ts',
'*.config.js',
'playwright/**',
'deploy/tools/**',
'middleware.ts',
'nextjs/**',
'instrumentation*.ts',
],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
},
},
],
};
__snapshots__/** filter=lfs diff=lfs merge=lfs -text
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
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
name: Checks
on:
workflow_call:
workflow_dispatch:
pull_request:
types: [ opened, synchronize, unlabeled ]
paths-ignore:
- '.github/ISSUE_TEMPLATE/**'
- '.husky/**'
- '.vscode/**'
- 'deploy/**'
- 'docs/**'
- 'public/**'
- 'stub/**'
- 'tools/**'
# concurrency:
# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }}
# cancel-in-progress: true
jobs:
code_quality:
name: Code quality
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip checks') && !(github.event.action == 'unlabeled' && github.event.label.name != 'skip checks') }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Run ESLint
run: yarn lint:eslint
- name: Compile TypeScript
run: yarn lint:tsc
envs_validation:
name: ENV variables validation
runs-on: ubuntu-latest
needs: [ code_quality ]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Install script dependencies
run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile --ignore-optional
- name: Run validation tests
run: |
set +e
cd ./deploy/tools/envs-validator && yarn test
exitcode="$?"
echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
exit "$exitcode"
jest_tests:
name: Jest tests
needs: [ code_quality, envs_validation ]
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Run Jest
run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests
pw_affected_tests:
name: Resolve affected Playwright tests
runs-on: ubuntu-latest
needs: [ code_quality, envs_validation ]
if: github.event_name == 'pull_request'
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Install script dependencies
run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile
- name: Run script
run: yarn test:pw:detect-affected
- name: Upload result file
uses: actions/upload-artifact@v4
with:
name: playwright-affected-tests
path: ./playwright/affected-tests.txt
retention-days: 3
pw_tests:
name: 'Playwright tests / Project: ${{ matrix.project }}'
needs: [ code_quality, envs_validation, pw_affected_tests ]
if: |
always() &&
needs.code_quality.result == 'success' &&
needs.envs_validation.result == 'success' &&
(needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.41.1-focal
strategy:
fail-fast: false
matrix:
project: [ default, mobile, dark-color-mode ]
steps:
- name: Install git-lfs
run: apt-get update && apt-get install git-lfs
- name: Checkout repo
uses: actions/checkout@v4
with:
lfs: 'true'
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Download affected tests list
if: ${{ needs.pw_affected_tests.result == 'success' }}
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: playwright-affected-tests
path: ./playwright
- name: Run PlayWright
run: yarn test:pw:ci --affected=${{ github.event_name == 'pull_request' }} --pass-with-no-tests
env:
HOME: /root
PW_PROJECT: ${{ matrix.project }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.project }}
path: playwright-report
retention-days: 10
\ No newline at end of file
name: Cleanup environments
on:
pull_request:
types:
- closed
- merged
workflow_dispatch:
jobs:
cleanup_release:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master
with:
appName: review-l2-$GITHUB_REF_NAME_SLUG
globalEnv: review
helmfileDir: deploy
kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
vaultRole: ci-dev
secrets: inherit
cleanup_l2_release:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master
with:
appName: review-$GITHUB_REF_NAME_SLUG
globalEnv: review
helmfileDir: deploy
kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
vaultRole: ci-dev
secrets: inherit
cleanup_docker_image:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master
with:
dockerImage: review-$GITHUB_REF_NAME_SLUG
secrets: inherit
name: Deploy from main branch
on:
push:
branches:
- main
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
deploy_main:
name: Deploy frontend
needs: publish_image
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
with:
appName: front
globalEnv: main
helmfileDir: deploy
kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
vaultRole: ci-dev
secrets: inherit
deploy_l2:
name: Deploy frontend (L2)
needs: publish_image
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
with:
appName: l2-optimism-goerli
globalEnv: optimism-goerli
helmfileDir: deploy
kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
vaultRole: ci-dev
secrets: inherit
name: Deploy review environment (L2)
on:
workflow_dispatch:
jobs:
make_slug:
name: Make GitHub reference slug
runs-on: ubuntu-latest
outputs:
REF_SLUG: ${{ steps.output.outputs.REF_SLUG }}
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set output
id: output
run: echo "REF_SLUG=${{ env.GITHUB_REF_NAME_SLUG }}" >> $GITHUB_OUTPUT
publish_image:
name: Publish Docker image
needs: make_slug
uses: './.github/workflows/publish-image.yml'
with:
tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }}
secrets: inherit
deploy_review_l2:
name: Deploy frontend (L2)
needs: [ make_slug, publish_image ]
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
with:
appName: review-l2-${{ needs.make_slug.outputs.REF_SLUG }}
globalEnv: review
helmfileDir: deploy
kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
vaultRole: ci-dev
secrets: inherit
name: Deploy review environment
on:
workflow_dispatch:
jobs:
make_slug:
name: Make GitHub reference slug
runs-on: ubuntu-latest
outputs:
REF_SLUG: ${{ steps.output.outputs.REF_SLUG }}
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set output
id: output
run: echo "REF_SLUG=${{ env.GITHUB_REF_NAME_SLUG }}" >> $GITHUB_OUTPUT
publish_image:
name: Publish Docker image
needs: make_slug
uses: './.github/workflows/publish-image.yml'
with:
tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }}
secrets: inherit
deploy_review:
name: Deploy frontend
needs: [ make_slug, publish_image ]
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
with:
appName: review-${{ needs.make_slug.outputs.REF_SLUG }}
globalEnv: review
helmfileDir: deploy
kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev
vaultRole: ci-dev
secrets: inherit
name: Run E2E tests k8s
on:
workflow_dispatch:
workflow_call:
# concurrency:
# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }}
# cancel-in-progress: true
jobs:
publish_image:
name: Publish Docker image
uses: './.github/workflows/publish-image.yml'
secrets: inherit
deploy_e2e:
name: Deploy E2E instance
needs: publish_image
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Get Vault credentials
id: retrieve-vault-secrets
uses: hashicorp/vault-action@v2.4.1
with:
url: https://vault.k8s.blockscout.com
role: ci-dev
path: github-jwt
method: jwt
tlsSkipVerify: false
exportToken: true
secrets: |
ci/data/dev/github token | WORKFLOW_TRIGGER_TOKEN ;
- name: Trigger deploy
uses: convictional/trigger-workflow-and-wait@v1.6.1
with:
owner: blockscout
repo: deployment-values
github_token: ${{ env.WORKFLOW_TRIGGER_TOKEN }}
workflow_file_name: deploy_blockscout.yaml
ref: main
wait_interval: 30
client_payload: '{ "instance": "dev", "globalEnv": "e2e"}'
test:
name: Run tests
needs: deploy_e2e
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
secrets: inherit
\ No newline at end of file
This diff is collapsed.
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
upload_source_maps:
name: Upload source maps to Sentry
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
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
review_requested_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;
review_requested_tasks:
name: Update status for issues in review
needs: [ review_requested_issues ]
if: ${{ needs.review_requested_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.review_requested_issues.outputs.issues }}
name: Publish Docker image
on:
workflow_dispatch:
inputs:
tags:
description: Image tags
required: false
type: string
platforms:
description: Image platforms (you can specify multiple platforms separated by comma)
required: false
type: string
default: linux/amd64
workflow_call:
inputs:
tags:
description: Image tags
required: false
type: string
platforms:
description: Image platforms (you can specify multiple platforms separated by comma)
required: false
type: string
default: linux/amd64
jobs:
run:
name: Run
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/blockscout/frontend
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
- name: Debug
env:
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
run: |
echo "ref_type: $REF_TYPE"
echo "ref_name: $REF_NAME"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
cache-from: type=gha
tags: ${{ inputs.tags || steps.meta.outputs.tags }}
platforms: ${{ inputs.platforms }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }}
\ No newline at end of file
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
upload_source_maps:
name: Upload source maps to Sentry
needs: publish_image
uses: './.github/workflows/upload-source-maps.yml'
secrets: inherit
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
This diff is collapsed.
name: Upload source maps to Sentry
on:
workflow_call:
workflow_dispatch:
env:
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
build_and_upload:
name: Build app with source maps and upload to Sentry
runs-on: ubuntu-latest
if: ${{ github.ref_type == 'tag' }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Make production build with source maps
run: yarn build
env:
NODE_ENV: production
- name: Inject Sentry debug ID
run: yarn sentry-cli sourcemaps inject ./.next
- name: Upload source maps to Sentry
run: yarn sentry-cli sourcemaps upload --release=${{ github.ref_name }} --url-prefix=~/_next/ --validate ./.next
\ No newline at end of file
# 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/
/public/envs.js
/public/icons/sprite.svg
/public/icons/README.md
/analyze
# production
/build
# misc
.DS_Store
*.pem
.tools
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
/configs/envs/.env.secrets
/configs/envs/.samples
# typescript
*.tsbuildinfo
.eslintcache
# Sentry
.sentryclirc
**.decrypted~**
/test-results/
/playwright-report/
/playwright/.cache/
/playwright/.browser/
/playwright/envs.js
/playwright/affected-tests.txt
**.dec**
__screenshots__
#!/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 "$@"
#!/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 "$@"
#!/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 "$@"
#!/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
#!/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 "$@"
20.11.0
{
"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"
]
}
{
// 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
{
"typescript.tsdk": "node_modules/typescript/lib"
}
\ No newline at end of file
This diff is collapsed.
# 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
# *****************************
# *** STAGE 1: Dependencies ***
# *****************************
FROM node:20.11.0-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
### APP
# Install dependencies
WORKDIR /app
COPY package.json yarn.lock ./
RUN apk add git
RUN yarn --frozen-lockfile
### FEATURE REPORTER
# Install dependencies
WORKDIR /feature-reporter
COPY ./deploy/tools/feature-reporter/package.json ./deploy/tools/feature-reporter/yarn.lock ./
RUN yarn --frozen-lockfile
### ENV VARIABLES CHECKER
# Install dependencies
WORKDIR /envs-validator
COPY ./deploy/tools/envs-validator/package.json ./deploy/tools/envs-validator/yarn.lock ./
RUN yarn --frozen-lockfile
# *****************************
# ****** STAGE 2: Build *******
# *****************************
FROM node:20.11.0-alpine AS builder
RUN apk add --no-cache --upgrade libc6-compat bash
# pass commit sha and git tag to the app image
ARG GIT_COMMIT_SHA
ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
ARG GIT_TAG
ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG
ENV NODE_ENV production
### APP
# Copy dependencies and source code
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate .env.registry with ENVs list and save build args into .env file
COPY --chmod=+x ./deploy/scripts/collect_envs.sh ./
RUN ./collect_envs.sh ./docs/ENVS.md
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
# Build app for production
RUN yarn build
RUN yarn svg:build-sprite
### FEATURE REPORTER
# Copy dependencies and source code, then build
COPY --from=deps /feature-reporter/node_modules ./deploy/tools/feature-reporter/node_modules
RUN cd ./deploy/tools/feature-reporter && yarn compile_config
RUN cd ./deploy/tools/feature-reporter && yarn build
### ENV VARIABLES CHECKER
# Copy dependencies and source code, then build
COPY --from=deps /envs-validator/node_modules ./deploy/tools/envs-validator/node_modules
RUN cd ./deploy/tools/envs-validator && yarn build
# *****************************
# ******* STAGE 3: Run ********
# *****************************
# Production image, copy all the files and run next
FROM node:20.11.0-alpine AS runner
RUN apk add --no-cache --upgrade bash curl jq unzip
### APP
WORKDIR /app
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/deploy/tools/envs-validator/index.js ./envs-validator.js
COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-reporter.js
# Copy scripts
## Entripoint
COPY --chmod=+x ./deploy/scripts/entrypoint.sh .
## ENV validator and client script maker
COPY --chmod=+x ./deploy/scripts/validate_envs.sh .
COPY --chmod=+x ./deploy/scripts/make_envs_script.sh .
## Assets downloader
COPY --chmod=+x ./deploy/scripts/download_assets.sh .
## Favicon generator
COPY --chmod=+x ./deploy/scripts/favicon_generator.sh .
COPY ./deploy/tools/favicon-generator ./deploy/tools/favicon-generator
RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"]
RUN ["chmod", "-R", "777", "./public"]
# Copy ENVs files
COPY --from=builder /app/.env.registry .
COPY --from=builder /app/.env .
# 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"]
This diff is collapsed.
<h1 align="center">Blockscout frontend</h1>
<p align="center">
<span>Frontend application for </span>
<a href="https://github.com/blockscout/blockscout/blob/master/README.md">Blockscout</a>
<span> blockchain explorer</span>
</p>
## 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 <path-to-your-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.
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { getEnvValue } from './utils';
const apiHost = getEnvValue('NEXT_PUBLIC_API_HOST');
const apiSchema = getEnvValue('NEXT_PUBLIC_API_PROTOCOL') || 'https';
const apiPort = getEnvValue('NEXT_PUBLIC_API_PORT');
const apiEndpoint = [
apiSchema || 'https',
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('');
const socketSchema = getEnvValue('NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL') || 'wss';
const socketEndpoint = [
socketSchema,
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('');
const api = Object.freeze({
host: apiHost,
protocol: apiSchema,
port: apiPort,
endpoint: apiEndpoint,
socket: socketEndpoint,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_API_BASE_PATH') || ''),
});
export default api;
import { getEnvValue } from './utils';
const appPort = getEnvValue('NEXT_PUBLIC_APP_PORT');
const appSchema = getEnvValue('NEXT_PUBLIC_APP_PROTOCOL');
const appHost = getEnvValue('NEXT_PUBLIC_APP_HOST');
const baseUrl = [
appSchema || 'https',
'://',
appHost,
appPort && ':' + appPort,
].filter(Boolean).join('');
const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development';
const app = Object.freeze({
isDev,
protocol: appSchema,
host: appHost,
port: appPort,
baseUrl,
useProxy: getEnvValue('NEXT_PUBLIC_USE_NEXT_JS_PROXY') === 'true',
});
export default app;
import { getEnvValue } from './utils';
const DEFAULT_CURRENCY_DECIMALS = 18;
const chain = Object.freeze({
id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'),
name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'),
shortName: getEnvValue('NEXT_PUBLIC_NETWORK_SHORT_NAME'),
currency: {
name: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_NAME'),
weiName: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME'),
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'),
decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS,
},
governanceToken: {
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL'),
},
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'),
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
verificationType: getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') || 'mining',
});
export default chain;
import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import app from '../app';
import { getEnvValue } from '../utils';
const authUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_AUTH_URL') || app.baseUrl);
const logoutUrl = (() => {
try {
const envUrl = getEnvValue('NEXT_PUBLIC_LOGOUT_URL');
const auth0ClientId = getEnvValue('NEXT_PUBLIC_AUTH0_CLIENT_ID');
const returnUrl = authUrl + '/auth/logout';
if (!envUrl || !auth0ClientId) {
throw Error();
}
const url = new URL(envUrl);
url.searchParams.set('client_id', auth0ClientId);
url.searchParams.set('returnTo', returnUrl);
return url.toString();
} catch (error) {
return;
}
})();
const title = 'My account';
const config: Feature<{ authUrl: string; logoutUrl: string }> = (() => {
if (
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
authUrl &&
logoutUrl
) {
return Object.freeze({
title,
isEnabled: true,
authUrl,
logoutUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import account from './account';
import verifiedTokens from './verifiedTokens';
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Address verification in "My account"';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (account.isEnabled && verifiedTokens.isEnabled && adminServiceApiHost) {
return Object.freeze({
title: 'Address verification in "My account"',
isEnabled: true,
api: {
endpoint: adminServiceApiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import type { AdButlerConfig } from 'types/client/adButlerConfig';
import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders';
import type { AdBannerProviders } 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 title = 'Banner ads';
type AdsBannerFeaturePayload = {
provider: Exclude<AdBannerProviders, 'adbutler' | 'none'>;
} | {
provider: 'adbutler';
adButler: {
config: {
desktop: AdButlerConfig;
mobile: AdButlerConfig;
};
};
}
const config: Feature<AdsBannerFeaturePayload> = (() => {
if (provider === 'adbutler') {
const desktopConfig = parseEnvJson<AdButlerConfig>(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP'));
const mobileConfig = parseEnvJson<AdButlerConfig>(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') {
return Object.freeze({
title,
isEnabled: true,
provider,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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;
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;
import type { Feature } from './types';
import chain from '../chain';
import { getEnvValue } from '../utils';
const walletConnectProjectId = getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID');
const title = 'Blockchain interaction (writing to contract, etc.)';
const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
if (
// all chain parameters are required for wagmi provider
// @wagmi/chains/dist/index.d.ts
chain.id &&
chain.name &&
chain.currency.name &&
chain.currency.symbol &&
chain.currency.decimals &&
chain.rpcUrl &&
walletConnectProjectId
) {
return Object.freeze({
title,
isEnabled: true,
walletConnect: {
projectId: walletConnectProjectId,
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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<BridgedTokenChain>; bridges: Array<TokenBridge> }> = (() => {
const chains = parseEnvJson<Array<BridgedTokenChain>>(getEnvValue('NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS'));
const bridges = parseEnvJson<Array<TokenBridge>>(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;
import type { Feature } from './types';
import services from '../services';
const title = 'Export data to CSV file';
const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
if (services.reCaptcha.siteKey) {
return Object.freeze({
title,
isEnabled: true,
reCaptcha: {
siteKey: services.reCaptcha.siteKey,
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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<GasUnit> => {
const envValue = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_UNITS');
if (!envValue) {
return [ 'usd', 'gwei' ];
}
const units = parseEnvJson<Array<GasUnit>>(envValue)?.filter((type) => GAS_UNITS.includes(type)) || [];
return units;
})();
const title = 'Gas tracker';
const config: Feature<{ units: Array<GasUnit> }> = (() => {
if (!isDisabled && units.length > 0) {
return Object.freeze({
title,
isEnabled: true,
units,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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;
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 }> = (() => {
return Object.freeze({
title,
isEnabled: true,
defaultTxHash,
});
})();
export default config;
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;
export { default as account } from './account';
export { default as addressVerification } from './addressVerification';
export { default as adsBanner } from './adsBanner';
export { default as adsText } from './adsText';
export { default as beaconChain } from './beaconChain';
export { default as bridgedTokens } from './bridgedTokens';
export { default as blockchainInteraction } from './blockchainInteraction';
export { default as csvExport } from './csvExport';
export { default as gasTracker } from './gasTracker';
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 nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
export { default as safe } from './safe';
export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats';
export { default as suave } from './suave';
export { default as swapButton } from './swapButton';
export { default as txInterpretation } from './txInterpretation';
export { default as userOps } from './userOps';
export { default as validators } from './validators';
export { default as verifiedTokens } from './verifiedTokens';
export { default as web3Wallet } from './web3Wallet';
import type { Feature } from './types';
import chain from '../chain';
import { getEnvValue, getExternalAssetFilePath } from '../utils';
// config file will be downloaded at run-time and saved in the public folder
const enabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ENABLED');
const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL');
const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM');
const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM');
const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Marketplace';
const config: Feature<(
{ configUrl: string } |
{ api: { endpoint: string; basePath: string } }
) & { submitFormUrl: string; categoriesUrl: string | undefined; suggestIdeasFormUrl: string | undefined }
> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
if (configUrl) {
return Object.freeze({
title,
isEnabled: true,
configUrl,
submitFormUrl,
categoriesUrl,
suggestIdeasFormUrl,
});
} else if (adminServiceApiHost) {
return Object.freeze({
title,
isEnabled: true,
submitFormUrl,
categoriesUrl,
suggestIdeasFormUrl,
api: {
endpoint: adminServiceApiHost,
basePath: '',
},
});
}
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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;
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;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST');
const title = 'Name service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const specUrl = getEnvValue('NEXT_PUBLIC_API_SPEC_URL') || `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml`;
const title = 'REST API documentation';
const config: Feature<{ specUrl: string }> = (() => {
return Object.freeze({
title,
isEnabled: true,
specUrl,
});
})();
export default config;
import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import { getEnvValue } from '../utils';
const type = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE');
return ROLLUP_TYPES.find((type) => type === envValue);
})();
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL');
const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL');
const title = 'Rollup (L2) chain';
const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string }> = (() => {
if (type && L1BaseUrl) {
return Object.freeze({
title,
isEnabled: true,
type,
L1BaseUrl,
L2WithdrawalUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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;
import type { Feature } from './types';
import app from '../app';
import { getEnvValue } from '../utils';
const dsn = getEnvValue('NEXT_PUBLIC_SENTRY_DSN');
const instance = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_APP_INSTANCE');
if (envValue) {
return envValue;
}
return app.host?.replace('.blockscout.com', '').replaceAll('-', '_');
})();
const environment = getEnvValue('NEXT_PUBLIC_APP_ENV') || 'production';
const release = getEnvValue('NEXT_PUBLIC_GIT_TAG');
const title = 'Sentry error monitoring';
const config: Feature<{
dsn: string;
instance: string;
release: string | undefined;
environment: string;
enableTracing: boolean;
}> = (() => {
if (dsn && instance && environment) {
return Object.freeze({
title,
isEnabled: true,
dsn,
instance,
release,
environment,
enableTracing: getEnvValue('NEXT_PUBLIC_SENTRY_ENABLE_TRACING') === 'true',
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST');
const title = 'Solidity to UML diagrams';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiEndpoint,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue('NEXT_PUBLIC_STATS_API_HOST');
const title = 'Blockchain statistics';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiEndpoint,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import marketplace from './marketplace';
const value = getEnvValue('NEXT_PUBLIC_SWAP_BUTTON_URL');
const title = 'Swap button';
function isValidUrl(string: string) {
try {
new URL(string);
return true;
} catch (error) {
return false;
}
}
const config: Feature<{ dappId: string } | { url: string }> = (() => {
if (value) {
if (isValidUrl(value)) {
return Object.freeze({
title,
isEnabled: true,
url: value,
});
} else if (marketplace.isEnabled) {
return Object.freeze({
title,
isEnabled: true,
dappId: value,
});
}
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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;
type FeatureEnabled<Payload extends Record<string, unknown> = Record<string, never>> = { title: string; isEnabled: true } & Payload;
type FeatureDisabled = { title: string; isEnabled: false };
export type Feature<Payload extends Record<string, unknown> = Record<string, never>> = FeatureEnabled<Payload> | 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 = <Payload extends Record<string, unknown>>(feature: Feature<Payload>): Payload | undefined => {
return feature.isEnabled ? feature : undefined;
};
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;
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;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const contractInfoApiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST');
const title = 'Verified tokens info';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (contractInfoApiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: contractInfoApiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
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<WalletType> | undefined => {
const envValue = getEnvValue('NEXT_PUBLIC_WEB3_WALLETS');
if (envValue === 'none') {
return;
}
const wallets = parseEnvJson<Array<WalletType>>(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<WalletType>; 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;
import api from './api';
import app from './app';
import chain from './chain';
import * as features from './features';
import meta from './meta';
import services from './services';
import UI from './ui';
const config = Object.freeze({
app,
chain,
api,
UI,
features,
services,
meta,
});
export default config;
import app from './app';
import { getEnvValue, getExternalAssetFilePath } from './utils';
const defaultImageUrl = app.baseUrl + '/static/og_placeholder.png';
const meta = Object.freeze({
promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') || 'true',
og: {
description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '',
imageUrl: getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl,
},
});
export default meta;
import { getEnvValue } from './utils';
export default Object.freeze({
reCaptcha: {
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'),
},
});
import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId } from 'types/client/navigation-items';
import type { ChainIndicatorId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks';
import * as views from './ui/views';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
const hiddenLinks = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(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<NavigationLinkId, boolean>);
return result;
})();
// eslint-disable-next-line max-len
const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
const UI = Object.freeze({
sidebar: {
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,
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [],
featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'),
},
footer: {
links: getExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS'),
frontendVersion: getEnvValue('NEXT_PUBLIC_GIT_TAG'),
frontendCommit: getEnvValue('NEXT_PUBLIC_GIT_COMMIT_SHA'),
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [],
plate: {
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT,
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white',
},
showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
},
views,
indexingAlert: {
blocks: {
isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS') === 'true' ? true : false,
},
intTxs: {
isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS') === 'true' ? true : false,
},
},
maintenanceAlert: {
message: getEnvValue('NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE'),
},
explorers: {
items: parseEnvJson<Array<NetworkExplorer>>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [],
},
ides: {
items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
},
hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false,
});
export default UI;
import type { AddressViewId, IdenticonType } from 'types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
const identiconType: IdenticonType = (() => {
const value = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE');
return IDENTICON_TYPES.find((type) => value === type) || 'jazzicon';
})();
const hiddenViews = (() => {
const parsedValue = parseEnvJson<Array<AddressViewId>>(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<AddressViewId, boolean>);
return result;
})();
const config = Object.freeze({
identiconType,
hiddenViews,
solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
});
export default config;
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<Array<BlockFieldId>>(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<BlockFieldId, boolean>);
return result;
})();
const config = Object.freeze({
hiddenFields: blockHiddenFields,
});
export default config;
export { default as address } from './address';
export { default as block } from './block';
export { default as nft } from './nft';
export { default as tx } from './tx';
import type { NftMarketplaceItem } from 'types/views/nft';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
const config = Object.freeze({
marketplaces: parseEnvJson<Array<NftMarketplaceItem>>(getEnvValue('NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES')) || [],
});
export default config;
import type { TxAdditionalFieldsId, TxFieldsId, TxViewId } from 'types/views/tx';
import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS, TX_VIEWS_IDS } from 'types/views/tx';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
const hiddenFields = (() => {
const parsedValue = parseEnvJson<Array<TxFieldsId>>(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<TxFieldsId, boolean>);
return result;
})();
const additionalFields = (() => {
const parsedValue = parseEnvJson<Array<TxAdditionalFieldsId>>(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<TxAdditionalFieldsId, boolean>);
return result;
})();
const hiddenViews = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_VIEWS_TX_HIDDEN_VIEWS');
if (!envValue) {
return undefined;
}
const parsedValue = parseEnvJson<Array<TxViewId>>(envValue);
if (!Array.isArray(parsedValue)) {
return undefined;
}
const result = TX_VIEWS_IDS.reduce((result, item) => {
result[item] = parsedValue.includes(item);
return result;
}, {} as Record<TxViewId, boolean>);
return result;
})();
const config = Object.freeze({
hiddenFields,
additionalFields,
hiddenViews,
});
export default config;
import isBrowser from 'lib/isBrowser';
import * as regexp from 'lib/regexp';
export const replaceQuotes = (value: string | undefined) => value?.replaceAll('\'', '"');
export const getEnvValue = (envName: string) => {
// eslint-disable-next-line no-restricted-properties
const envs = isBrowser() ? window.__envs : process.env;
if (isBrowser() && envs.NEXT_PUBLIC_APP_INSTANCE === 'pw') {
const storageValue = localStorage.getItem(envName);
if (typeof storageValue === 'string') {
return storageValue;
}
}
return replaceQuotes(envs[envName]);
};
export const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
try {
return JSON.parse(env || 'null') as DataType | null;
} catch (error) {
return null;
}
};
export const getExternalAssetFilePath = (envName: string) => {
const parsedValue = getEnvValue(envName);
if (!parsedValue) {
return;
}
return buildExternalAssetFilePath(envName, parsedValue);
};
export const buildExternalAssetFilePath = (name: string, value: string) => {
try {
const fileName = name.replace(/^NEXT_PUBLIC_/, '').replace(/_URL$/, '').toLowerCase();
const url = new URL(value);
const fileExtension = url.pathname.match(regexp.FILE_EXTENSION)?.[1];
return `/assets/${ fileName }.${ fileExtension }`;
} catch (error) {
return;
}
};
# Set of ENVs for Ethereum network explorer
# https://eth.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME=ETH
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://eth.llamarpc.com
# api configuration
NEXT_PUBLIC_API_HOST=eth.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# 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.json
## footer
##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':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS="[{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Blockchair','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/blockchair.png?raw=true','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/sentio.png?raw=true','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/tenderly.png?raw=true','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/0xPPl.png?raw=true','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/3xpl.png?raw=true','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ]"
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/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_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
# Set of ENVs for Goerli testnet network explorer
# https://eth-goerli.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_IS_TESTNET=true
# api configuration
NEXT_PUBLIC_API_HOST=eth-goerli.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
## footer
##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
\ No newline at end of file
# Set of ENVs for Gnosis network explorer
# https://gnosis.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Gnosis
NEXT_PUBLIC_NETWORK_SHORT_NAME=Gnosis
NEXT_PUBLIC_NETWORK_ID=100
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=xDAI
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=xDAI
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com
# api configuration
NEXT_PUBLIC_API_HOST=gnosis.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(46, 74, 60)"
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgb(255, 255, 255)"
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/gnosis-chain-mainnet.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/gnosis.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/gnosis.svg
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/gnosis.json
## views
## misc
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x082762f95047d39d612daafec832f88163f3815fde4ddd8944f2a5198a396e0f
# NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL=GNO
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/gnosis-chain.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrmiO9mDGJoPNmJe
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
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_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_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'},{'type':'amb','title':'Arbitrary Message Bridge','short_title':'AMB'}]
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/polygon-mainnet.png?raw=true
# Set of ENVs for Jest unit tests
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Blockscout
NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout
NEXT_PUBLIC_NETWORK_ID=1
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://localhost:1111
NEXT_PUBLIC_IS_TESTNET=true
# api configuration
NEXT_PUBLIC_API_HOST=localhost
NEXT_PUBLIC_API_PORT=3003
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
## sidebar
NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_LOGO_DARK=
NEXT_PUBLIC_NETWORK_ICON=
NEXT_PUBLIC_NETWORK_ICON_DARK=
NEXT_PUBLIC_FEATURED_NETWORKS=
## footer
NEXT_PUBLIC_FOOTER_LINKS=
NEXT_PUBLIC_GIT_TAG=v1.0.11
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=jest
NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
# 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
# Set of ENVs for Develompent network explorer
# https://blockscout-main.k8s-dev.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_IS_TESTNET=true
# api configuration
NEXT_PUBLIC_API_HOST=blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
## footer
##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
## views
# NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts']
# NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','tx_fee','gas_fees','burnt_fees']
# NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas']
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap
# Set of ENVs for Develompent L2 network explorer
# https://blockscout-optimism-goerli.k8s-dev.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Base Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Base
NEXT_PUBLIC_NETWORK_ID=84531
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://goerli.base.org
NEXT_PUBLIC_IS_TESTNET=true
# api configuration
NEXT_PUBLIC_API_HOST=blockscout-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
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_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/base-goerli.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg
## footer
## misc
## views
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
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_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_WEB3_WALLETS=['coinbase']
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
# Set of ENVs for zkevm (dev only)
# https://eth.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME='OP Goerli'
NEXT_PUBLIC_NETWORK_SHORT_NAME='OP Goerli'
NEXT_PUBLIC_NETWORK_ID=420
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://goerli.optimism.io
# api configuration
NEXT_PUBLIC_API_HOST=optimism-goerli.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json
## footer
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup
NEXT_PUBLIC_ROLLUP_TYPE='optimistic'
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-goerli.blockscout.com/
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
declare module 'react-identicons'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment