diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..cc48cec34c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "blockscout dev", + "image": "mcr.microsoft.com/devcontainers/typescript-node:20", + "forwardPorts": [ 3000 ], + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/bin/zsh" + } + } + }, + "extensions": [ + "streetsidesoftware.code-spell-checker", + "formulahendry.auto-close-tag", + "formulahendry.auto-rename-tag", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "yatki.vscode-surround", + "simonsiefke.svg-preview" + ] + } + }, + "features": { + "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": { + "plugins": "npm", + "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions" + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..13a543a449 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +Dockerfile +.dockerignore +node_modules +/**/node_modules +node_modules_linux +npm-debug.log +README.md +.next +.git +*.tsbuildinfo +.eslintcache +/test-results/ +/playwright-report/ \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..4bdc5535b1 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +NEXT_PUBLIC_SENTRY_DSN=https://sentry.io +SENTRY_CSP_REPORT_URI=https://sentry.io +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X +NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx +NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx +NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx +FAVICON_GENERATOR_API_KEY=xxx +NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..37523e907c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +node_modules +node_modules_linux + +playwright/envs.js +deploy/tools/envs-validator/index.js +deploy/tools/feature-reporter/build/** +deploy/tools/feature-reporter/index.js +public/** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..49e2ad4c43 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,341 @@ +const RESTRICTED_MODULES = { + paths: [ + { name: 'dayjs', message: 'Please use lib/date/dayjs.ts instead of directly importing dayjs' }, + { name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' }, + { name: '@metamask/providers', message: 'Please lazy-load @metamask/providers or use useProvider hook instead' }, + { name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' }, + { name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' }, + { + name: '@chakra-ui/react', + importNames: [ 'Popover', 'Menu', 'useToast' ], + message: 'Please use corresponding component or hook from ui/shared/chakra component instead', + }, + { + name: 'lodash', + message: 'Please use `import [package] from \'lodash/[package]\'` instead.', + }, + ], + patterns: [ + 'icons/*', + '!lodash/*', + ], +}; + +module.exports = { + env: { + es6: true, + browser: true, + node: true, + }, + 'extends': [ + 'next/core-web-vitals', + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:regexp/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:jest/recommended', + 'plugin:playwright/playwright-test', + 'plugin:@tanstack/eslint-plugin-query/recommended', + ], + plugins: [ + 'es5', + 'react', + 'regexp', + '@typescript-eslint', + 'react-hooks', + 'jsx-a11y', + 'eslint-plugin-import-helpers', + 'jest', + 'eslint-plugin-no-cyrillic-string', + '@tanstack/query', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + settings: { + react: { + pragma: 'React', + version: 'detect', + }, + }, + rules: { + '@typescript-eslint/array-type': [ 'error', { + 'default': 'generic', + readonly: 'generic', + } ], + '@typescript-eslint/brace-style': [ 'error', '1tbs' ], + '@typescript-eslint/consistent-type-imports': [ 'error' ], + '@typescript-eslint/indent': [ 'error', 2 ], + '@typescript-eslint/member-delimiter-style': [ 'error' ], + '@typescript-eslint/naming-convention': [ 'error', + { + selector: 'default', + format: [ 'camelCase' ], + leadingUnderscore: 'allow', + trailingUnderscore: 'forbid', + }, + { + selector: 'class', + format: [ 'PascalCase' ], + }, + { + selector: 'enum', + format: [ 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'enumMember', + format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'function', + format: [ 'camelCase', 'PascalCase' ], + }, + { + selector: 'interface', + format: [ 'PascalCase' ], + }, + { + selector: 'method', + format: [ 'camelCase', 'snake_case', 'UPPER_CASE' ], + leadingUnderscore: 'allow', + }, + { + selector: 'parameter', + format: [ 'camelCase', 'PascalCase' ], + leadingUnderscore: 'allow', + }, + { + selector: 'property', + format: null, + }, + { + selector: 'typeAlias', + format: [ 'PascalCase' ], + }, + { + selector: 'typeParameter', + format: [ 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'variable', + format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ], + leadingUnderscore: 'allow', + }, + ], + '@typescript-eslint/no-duplicate-imports': [ 'error' ], + '@typescript-eslint/no-empty-function': [ 'off' ], + '@typescript-eslint/no-unused-vars': [ 'error', { ignoreRestSiblings: true } ], + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-useless-constructor': [ 'error' ], + '@typescript-eslint/type-annotation-spacing': 'error', + '@typescript-eslint/no-explicit-any': [ 'error', { ignoreRestArgs: true } ], + + // disabled in favor of @typescript-eslint + 'brace-style': 'off', + camelcase: 'off', + indent: 'off', + 'no-unused-vars': 'off', + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', + + 'array-bracket-spacing': [ 'error', 'always' ], + 'arrow-spacing': [ 'error', { before: true, after: true } ], + 'comma-dangle': [ 'error', 'always-multiline' ], + 'comma-spacing': [ 'error' ], + 'comma-style': [ 'error', 'last' ], + curly: [ 'error', 'all' ], + 'eol-last': 'error', + eqeqeq: [ 'error', 'allow-null' ], + 'id-match': [ 'error', '^[\\w$]+$' ], + 'jsx-quotes': [ 'error', 'prefer-double' ], + 'key-spacing': [ 'error', { + beforeColon: false, + afterColon: true, + } ], + 'keyword-spacing': 'error', + 'linebreak-style': [ 'error', 'unix' ], + 'lines-around-comment': [ 'error', { + beforeBlockComment: true, + allowBlockStart: true, + } ], + 'max-len': [ 'error', 160, 4 ], + 'no-console': 'error', + 'no-empty': [ 'error', { allowEmptyCatch: true } ], + 'no-implicit-coercion': [ 'error', { + number: true, + 'boolean': true, + string: true, + } ], + 'no-mixed-operators': [ 'error', { + groups: [ + [ '&&', '||' ], + ], + } ], + 'no-mixed-spaces-and-tabs': 'error', + 'no-multiple-empty-lines': [ 'error', { + max: 1, + maxEOF: 0, + maxBOF: 0, + } ], + 'no-multi-spaces': 'error', + 'no-multi-str': 'error', + 'no-nested-ternary': 'error', + 'no-trailing-spaces': 'error', + 'no-spaced-func': 'error', + 'no-with': 'error', + 'object-curly-spacing': [ 'error', 'always' ], + 'object-shorthand': 'off', + 'one-var': [ 'error', 'never' ], + 'operator-linebreak': [ 'error', 'after' ], + 'prefer-const': 'error', + 'quote-props': [ 'error', 'as-needed', { + keywords: true, + numbers: true, + } ], + quotes: [ 'error', 'single', { + allowTemplateLiterals: true, + } ], + 'space-before-function-paren': [ 'error', 'never' ], + 'space-before-blocks': [ 'error', 'always' ], + 'space-in-parens': [ 'error', 'never' ], + 'space-infix-ops': 'error', + 'space-unary-ops': 'off', + 'template-curly-spacing': [ 'error', 'always' ], + 'wrap-iife': [ 'error', 'inside' ], + semi: [ 'error', 'always' ], + + 'import-helpers/order-imports': [ + 'error', + { + newlinesBetween: 'always', + groups: [ + 'module', + '/types/', + [ + '/^nextjs/', + ], + [ + '/^configs/', + '/^data/', + '/^deploy/', + '/^icons/', + '/^jest/', + '/^lib/', + '/^mocks/', + '/^pages/', + '/^playwright/', + '/^stubs/', + '/^theme/', + '/^ui/', + ], + [ 'parent', 'sibling', 'index' ], + ], + alphabetize: { order: 'asc', ignoreCase: true }, + }, + ], + + 'no-restricted-imports': [ 'error', RESTRICTED_MODULES ], + 'no-restricted-properties': [ 2, { + object: 'process', + property: 'env', + // FIXME: restrict the rule only NEXT_PUBLIC variables + message: 'Please use configs/app/index.ts to import any NEXT_PUBLIC environment variables. For other properties please disable this rule for a while.', + } ], + + 'react/jsx-key': 'error', + 'react/jsx-no-bind': [ 'error', { + ignoreRefs: true, + } ], + 'react/jsx-curly-brace-presence': [ 'error', { + props: 'never', + children: 'never', + } ], + 'react/jsx-curly-spacing': [ 'error', { + when: 'always', + children: true, + spacing: { + objectLiterals: 'never', + }, + } ], + 'react/jsx-equals-spacing': [ 'error', 'never' ], + 'react/jsx-fragments': [ 'error', 'syntax' ], + 'react/jsx-no-duplicate-props': 'error', + 'react/jsx-no-target-blank': 'off', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-tag-spacing': [ 'error', { + afterOpening: 'never', + beforeSelfClosing: 'never', + closingSlash: 'never', + } ], + 'react/jsx-wrap-multilines': [ 'error', { + declaration: 'parens-new-line', + assignment: 'parens-new-line', + 'return': 'parens-new-line', + arrow: 'parens-new-line', + condition: 'parens-new-line', + logical: 'parens-new-line', + prop: 'parens-new-line', + } ], + 'react/no-access-state-in-setstate': 'error', + 'react/no-deprecated': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-find-dom-node': 'off', + 'react/no-redundant-should-component-update': 'error', + 'react/no-render-return-value': 'error', + 'react/no-string-refs': 'off', + 'react/no-unknown-property': 'error', + 'react/no-unused-state': 'error', + 'react/require-optimization': [ 'error' ], + 'react/void-dom-elements-no-children': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + + 'regexp/confusing-quantifier': 'error', + 'regexp/control-character-escape': 'error', + 'regexp/negation': 'error', + 'regexp/no-dupe-disjunctions': 'error', + 'regexp/no-empty-alternative': 'error', + 'regexp/no-empty-capturing-group': 'error', + 'regexp/no-lazy-ends': 'error', + 'regexp/no-obscure-range': [ 'error', { + allowed: [ 'alphanumeric' ], + } ], + 'regexp/no-optional-assertion': 'error', + 'regexp/no-unused-capturing-group': [ 'error', { + fixable: true, + } ], + 'regexp/no-useless-character-class': 'error', + 'regexp/no-useless-dollar-replacements': 'error', + + 'no-cyrillic-string/no-cyrillic-string': 'error', + }, + overrides: [ + { + files: [ '*.js', '*.jsx' ], + rules: { + '@typescript-eslint/no-var-requires': 'off', + }, + }, + { + files: [ + '*.config.ts', + '*.config.js', + 'playwright/**', + 'deploy/tools/**', + 'middleware.ts', + 'nextjs/**', + 'instrumentation*.ts', + ], + rules: { + // for configs allow to consume env variables from process.env directly + 'no-restricted-properties': [ 0 ], + }, + }, + ], +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..d4431e2963 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +__snapshots__/** filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..732e01d370 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,78 @@ +name: Bug Report +description: File a bug report +labels: [ "bug", "triage" ] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug 🐛! + + Please search open/closed issues before submitting. Someone might have had the similar problem before 😉! + + - type: textarea + id: description + attributes: + label: Description + description: A brief description of the issue. + placeholder: | + When I ____, I expected ____ to happen but ____ happened instead. + validations: + required: true + + - type: input + id: link + attributes: + label: Link to the page + description: The link to the page where the issue occurs. + placeholder: https://eth.blockscout.com + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: | + Explain how to reproduce the issue in the development environment. + value: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + + - type: input + id: version + attributes: + label: App version + description: The version of the front-end app you use. You can find it in the footer of the page. + placeholder: v1.2.0 + validations: + required: true + + - type: input + id: browser + # validations: + # required: true + attributes: + label: Browser + description: What browsers are you seeing the problem on? Please specify browser vendor and its version. + placeholder: Google Chrome 111 + + - type: dropdown + id: operating-system + # validations: + # required: true + attributes: + label: Operating system + description: The operating system this issue occurred with. + options: + - macOS + - Windows + - Linux + + - type: textarea + id: additional-information + attributes: + label: Additional information + description: | + Use this section to provide any additional information you might have (e.g screenshots or screencasts). \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..439bca677d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Feature Request + url: https://blockscout.canny.io/feature-requests + about: Request a feature or enhancement + - name: Ask a question + url: https://github.com/orgs/blockscout/discussions + about: Ask questions and discuss topics with other community members + - name: Join our Discord Server + url: https://discord.gg/blockscout + about: The official Blockscout Discord community \ No newline at end of file diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000000..9496bb37e6 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,228 @@ +name: Checks +on: + workflow_call: + workflow_dispatch: + pull_request: + types: [ opened, synchronize, unlabeled ] + paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.husky/**' + - '.vscode/**' + - 'deploy/**' + - 'docs/**' + - 'public/**' + - 'stub/**' + - 'tools/**' + +# concurrency: +# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }} +# cancel-in-progress: true + +jobs: + code_quality: + name: Code quality + runs-on: ubuntu-latest + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip checks') && !(github.event.action == 'unlabeled' && github.event.label.name != 'skip checks') }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.11.0 + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile --ignore-optional + + - name: Run ESLint + run: yarn lint:eslint + + - name: Compile TypeScript + run: yarn lint:tsc + + envs_validation: + name: ENV variables validation + runs-on: ubuntu-latest + needs: [ code_quality ] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.11.0 + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile --ignore-optional + + - name: Install script dependencies + run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile --ignore-optional + + - name: Run validation tests + run: | + set +e + cd ./deploy/tools/envs-validator && yarn test + exitcode="$?" + echo "exitcode=$exitcode" >> $GITHUB_OUTPUT + exit "$exitcode" + + jest_tests: + name: Jest tests + needs: [ code_quality, envs_validation ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.11.0 + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile --ignore-optional + + - name: Run Jest + run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests + + pw_affected_tests: + name: Resolve affected Playwright tests + runs-on: ubuntu-latest + needs: [ code_quality, envs_validation ] + if: github.event_name == 'pull_request' + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.11.0 + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile --ignore-optional + + - name: Install script dependencies + run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile + + - name: Run script + run: yarn test:pw:detect-affected + + - name: Upload result file + uses: actions/upload-artifact@v4 + with: + name: playwright-affected-tests + path: ./playwright/affected-tests.txt + retention-days: 3 + + pw_tests: + name: 'Playwright tests / Project: ${{ matrix.project }}' + needs: [ code_quality, envs_validation, pw_affected_tests ] + if: | + always() && + needs.code_quality.result == 'success' && + needs.envs_validation.result == 'success' && + (needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped') + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.41.1-focal + + strategy: + fail-fast: false + matrix: + project: [ default, mobile, dark-color-mode ] + + steps: + - name: Install git-lfs + run: apt-get update && apt-get install git-lfs + + - name: Checkout repo + uses: actions/checkout@v4 + with: + lfs: 'true' + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.11.0 + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile --ignore-optional + + - name: Download affected tests list + if: ${{ needs.pw_affected_tests.result == 'success' }} + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: playwright-affected-tests + path: ./playwright + + - name: Run PlayWright + run: yarn test:pw:ci --affected=${{ github.event_name == 'pull_request' }} --pass-with-no-tests + env: + HOME: /root + PW_PROJECT: ${{ matrix.project }} + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report-${{ matrix.project }} + path: playwright-report + retention-days: 10 \ No newline at end of file diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000000..a8ed3ee27a --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,33 @@ +name: Cleanup environments + +on: + pull_request: + types: + - closed + - merged + workflow_dispatch: + +jobs: + cleanup_release: + uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master + with: + appName: review-l2-$GITHUB_REF_NAME_SLUG + globalEnv: review + helmfileDir: deploy + kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev + vaultRole: ci-dev + secrets: inherit + cleanup_l2_release: + uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master + with: + appName: review-$GITHUB_REF_NAME_SLUG + globalEnv: review + helmfileDir: deploy + kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev + vaultRole: ci-dev + secrets: inherit + cleanup_docker_image: + uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master + with: + dockerImage: review-$GITHUB_REF_NAME_SLUG + secrets: inherit diff --git a/.github/workflows/copy-issues-labels.yml b/.github/workflows/copy-issues-labels.yml new file mode 100644 index 0000000000..e05b6e88ee --- /dev/null +++ b/.github/workflows/copy-issues-labels.yml @@ -0,0 +1,116 @@ +name: Copy issues labels to pull request + +on: + workflow_dispatch: + inputs: + pr_number: + description: Pull request number + required: true + type: string + issues: + description: JSON encoded list of issue ids + required: true + type: string + workflow_call: + inputs: + pr_number: + description: Pull request number + required: true + type: string + issues: + description: JSON encoded list of issue ids + required: true + type: string + +jobs: + run: + name: Run + runs-on: ubuntu-latest + steps: + - name: Find unique labels + id: find_unique_labels + uses: actions/github-script@v7 + env: + ISSUES: ${{ inputs.issues }} + with: + script: | + const issues = JSON.parse(process.env.ISSUES); + + const WHITE_LISTED_LABELS = [ + 'client feature', + 'feature', + + 'bug', + + 'dependencies', + 'performance', + + 'chore', + 'enhancement', + 'refactoring', + 'tech', + 'ENVs', + ] + + const labels = await Promise.all(issues.map(getIssueLabels)); + const uniqueLabels = uniqueStringArray(labels.flat().filter((label) => WHITE_LISTED_LABELS.includes(label))); + + if (uniqueLabels.length === 0) { + core.info('No labels found.\n'); + return []; + } + + core.info(`Found following labels: ${ uniqueLabels.join(', ') }.\n`); + return uniqueLabels; + + async function getIssueLabels(issue) { + core.info(`Obtaining labels list for the issue #${ issue }...`); + + try { + const response = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/labels', { + owner: 'blockscout', + repo: 'frontend', + issue_number: issue, + }); + return response.data.map(({ name }) => name); + } catch (error) { + core.error(`Failed to obtain labels for the issue #${ issue }: ${ error.message }`); + return []; + } + } + + function uniqueStringArray(array) { + return Array.from(new Set(array)); + } + + - name: Update pull request labels + id: update_pr_labels + uses: actions/github-script@v7 + env: + LABELS: ${{ steps.find_unique_labels.outputs.result }} + PR_NUMBER: ${{ inputs.pr_number }} + with: + script: | + const labels = JSON.parse(process.env.LABELS); + const prNumber = Number(process.env.PR_NUMBER); + + if (labels.length === 0) { + core.info('Nothing to update.\n'); + return; + } + + for (const label of labels) { + await addLabelToPr(prNumber, label); + } + core.info('Done.\n'); + + async function addLabelToPr(prNumber, label) { + console.log(`Adding label to the pull request #${ prNumber }...`); + + return await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', { + owner: 'blockscout', + repo: 'frontend', + issue_number: prNumber, + labels: [ label ], + }); + } \ No newline at end of file diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml new file mode 100644 index 0000000000..73bdd72cf7 --- /dev/null +++ b/.github/workflows/deploy-main.yml @@ -0,0 +1,27 @@ +name: Deploy from main branch + +on: + push: + branches: + - main + paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.husky/**' + - '.vscode/**' + - 'docs/**' + - 'jest/**' + - 'mocks/**' + - 'playwright/**' + - 'stubs/**' + - 'tools/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + publish_image: + name: Publish Docker image + uses: './.github/workflows/publish-image.yml' + secrets: inherit diff --git a/.github/workflows/deploy-review-l2.yml b/.github/workflows/deploy-review-l2.yml new file mode 100644 index 0000000000..cd9cdcae53 --- /dev/null +++ b/.github/workflows/deploy-review-l2.yml @@ -0,0 +1,62 @@ +name: Deploy review environment (L2) + +on: + workflow_dispatch: + inputs: + envs_preset: + description: ENVs preset + required: false + default: "" + type: choice + options: + - none + - arbitrum + - base + - celo_alfajores + - garnet + - gnosis + - eth + - eth_sepolia + - eth_goerli + - optimism + - optimism_sepolia + - polygon + - rootstock + - stability + - zkevm + - zksync + +jobs: + make_slug: + name: Make GitHub reference slug + runs-on: ubuntu-latest + outputs: + REF_SLUG: ${{ steps.output.outputs.REF_SLUG }} + steps: + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.4.1 + + - name: Set output + id: output + run: echo "REF_SLUG=${{ env.GITHUB_REF_NAME_SLUG }}" >> $GITHUB_OUTPUT + + publish_image: + name: Publish Docker image + needs: make_slug + uses: './.github/workflows/publish-image.yml' + with: + tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }} + build_args: ENVS_PRESET=${{ inputs.envs_preset }} + secrets: inherit + + deploy_review_l2: + name: Deploy frontend (L2) + needs: [ make_slug, publish_image ] + uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master + with: + appName: review-l2-${{ needs.make_slug.outputs.REF_SLUG }} + globalEnv: review + helmfileDir: deploy + kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev + vaultRole: ci-dev + secrets: inherit diff --git a/.github/workflows/deploy-review.yml b/.github/workflows/deploy-review.yml new file mode 100644 index 0000000000..9bc8fb616f --- /dev/null +++ b/.github/workflows/deploy-review.yml @@ -0,0 +1,62 @@ +name: Deploy review environment + +on: + workflow_dispatch: + inputs: + envs_preset: + description: ENVs preset + required: false + default: "" + type: choice + options: + - none + - arbitrum + - base + - celo_alfajores + - garnet + - gnosis + - eth + - eth_sepolia + - eth_goerli + - optimism + - optimism_sepolia + - polygon + - rootstock + - stability + - zkevm + - zksync + +jobs: + make_slug: + name: Make GitHub reference slug + runs-on: ubuntu-latest + outputs: + REF_SLUG: ${{ steps.output.outputs.REF_SLUG }} + steps: + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.4.1 + + - name: Set output + id: output + run: echo "REF_SLUG=${{ env.GITHUB_REF_NAME_SLUG }}" >> $GITHUB_OUTPUT + + publish_image: + name: Publish Docker image + needs: make_slug + uses: './.github/workflows/publish-image.yml' + with: + tags: ghcr.io/blockscout/frontend:review-${{ needs.make_slug.outputs.REF_SLUG }} + build_args: ENVS_PRESET=${{ inputs.envs_preset }} + secrets: inherit + + deploy_review: + name: Deploy frontend + needs: [ make_slug, publish_image ] + uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master + with: + appName: review-${{ needs.make_slug.outputs.REF_SLUG }} + globalEnv: review + helmfileDir: deploy + kubeConfigSecret: ci/data/dev/kubeconfig/k8s-dev + vaultRole: ci-dev + secrets: inherit diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000000..6aab1f48ff --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,50 @@ +name: Run E2E tests k8s + +on: + workflow_dispatch: + workflow_call: + +# concurrency: +# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }} +# cancel-in-progress: true + +jobs: + publish_image: + name: Publish Docker image + uses: './.github/workflows/publish-image.yml' + secrets: inherit + + deploy_e2e: + name: Deploy E2E instance + needs: publish_image + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Get Vault credentials + id: retrieve-vault-secrets + uses: hashicorp/vault-action@v2.4.1 + with: + url: https://vault.k8s.blockscout.com + role: ci-dev + path: github-jwt + method: jwt + tlsSkipVerify: false + exportToken: true + secrets: | + ci/data/dev/github token | WORKFLOW_TRIGGER_TOKEN ; + - name: Trigger deploy + uses: convictional/trigger-workflow-and-wait@v1.6.1 + with: + owner: blockscout + repo: deployment-values + github_token: ${{ env.WORKFLOW_TRIGGER_TOKEN }} + workflow_file_name: deploy_blockscout.yaml + ref: main + wait_interval: 30 + client_payload: '{ "instance": "dev", "globalEnv": "e2e"}' + + test: + name: Run tests + needs: deploy_e2e + uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/label-issues-in-release.yml b/.github/workflows/label-issues-in-release.yml new file mode 100644 index 0000000000..b95fd45616 --- /dev/null +++ b/.github/workflows/label-issues-in-release.yml @@ -0,0 +1,282 @@ +name: Label issues in release + +on: + workflow_dispatch: + inputs: + tag: + description: 'Release tag' + required: true + type: string + label_name: + description: 'Name of the label' + required: true + type: string + label_description: + description: 'Description of the label' + default: '' + required: false + type: string + label_color: + description: 'A color of the added label' + default: 'FFFFFF' + required: false + type: string + workflow_call: + inputs: + tag: + description: 'Release tag' + required: true + type: string + label_name: + description: 'Name of the label' + required: true + type: string + label_description: + description: 'Description of the label' + default: '' + required: false + type: string + label_color: + description: 'A color of the added label' + default: 'FFFFFF' + required: false + type: string + outputs: + issues: + description: "JSON encoded list of issues linked to commits in the release" + value: ${{ jobs.run.outputs.issues }} + +# concurrency: +# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }} +# cancel-in-progress: true + +jobs: + run: + name: Run + runs-on: ubuntu-latest + outputs: + issues: ${{ steps.linked_issues.outputs.result }} + steps: + - name: Getting tags of the two latestest releases + id: tags + uses: actions/github-script@v7 + env: + TAG: ${{ inputs.tag }} + with: + script: | + const { repository: { releases: { nodes: releases } } } = await github.graphql(` + query ($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + releases(first: 10, orderBy: { field: CREATED_AT, direction: DESC }) { + nodes { + name + tagName + tagCommit { + oid + } + isPrerelease + isDraft + publishedAt + } + } + } + } + `, + { + owner: context.repo.owner, + repo: context.repo.repo, + } + ); + + if (releases[0].tagName !== process.env.TAG) { + core.info(`Current latest tag: ${ releases[0].tagName }`); + core.setFailed(`Release with tag ${ process.env.TAG } is not latest one.`); + return; + } + + const latestTag = process.env.TAG; + const [ { tagName: previousTag } ] = releases + .slice(1) + .filter(({ isDraft }) => !isDraft); + + core.info('Found following tags:'); + core.info(` latest: ${ latestTag }`); + core.info(` second latest: ${ previousTag }`); + + core.setOutput('latest', latestTag); + core.setOutput('previous', previousTag); + + - name: Looking for commits between two releases + id: commits + uses: actions/github-script@v7 + env: + PREVIOUS_TAG: ${{ steps.tags.outputs.previous }} + LATEST_TAG: ${{ steps.tags.outputs.latest }} + with: + script: | + const { data: { commits: commitsInRelease } } = await github.request('GET /repos/{owner}/{repo}/compare/{basehead}', { + owner: context.repo.owner, + repo: context.repo.repo, + basehead: `${ process.env.PREVIOUS_TAG }...${ process.env.LATEST_TAG }`, + }); + + if (commitsInRelease.length === 0) { + core.notice(`No commits found between ${ process.env.PREVIOUS_TAG } and ${ process.env.LATEST_TAG }`); + return []; + } + + const commits = commitsInRelease.map(({ sha }) => sha); + + core.startGroup(`Found ${ commits.length } commits`); + commits.forEach((sha) => { + core.info(sha); + }) + core.endGroup(); + + return commits; + + - name: Looking for issues linked to commits + id: linked_issues + uses: actions/github-script@v7 + env: + COMMITS: ${{ steps.commits.outputs.result }} + with: + script: | + const commits = JSON.parse(process.env.COMMITS); + + if (commits.length === 0) { + return []; + } + + const map = {}; + + core.startGroup(`Looking for linked issues`); + for (const sha of commits) { + const result = await getLinkedIssuesForCommitPR(sha); + result.forEach((issue) => { + map[issue] = issue; + }); + } + core.endGroup(); + + const issues = Object.values(map); + + if (issues.length > 0) { + core.startGroup(`Found ${ issues.length } unique issues`); + issues.sort().forEach((issue) => { + core.info(`#${ issue } - https://github.com/${ context.repo.owner }/${ context.repo.repo }/issues/${ issue }`); + }) + core.endGroup(); + } else { + core.notice('No linked issues found.'); + } + + return issues; + + async function getLinkedIssuesForCommitPR(sha) { + core.info(`Fetching issues for commit: ${ sha }`); + + const response = await github.graphql(` + query ($owner: String!, $repo: String!, $sha: GitObjectID!) { + repository(owner: $owner, name: $repo) { + object(oid: $sha) { + ... on Commit { + associatedPullRequests(first: 10) { + nodes { + number + title + state + merged + closingIssuesReferences(first: 10) { + nodes { + number + title + closed + } + } + } + } + } + } + } + } + `, { + owner: context.repo.owner, + repo: context.repo.repo, + sha, + }); + + if (!response) { + core.info('Nothing has found.'); + return []; + } + + const { repository: { object: { associatedPullRequests } } } = response; + + const issues = associatedPullRequests + .nodes + .map(({ closingIssuesReferences: { nodes: issues } }) => issues.map(({ number }) => number)) + .flat(); + + core.info(`Found following issues: ${ issues.join(', ') || '-' }\n`); + + return issues; + } + + - name: Creating label + id: label_creating + uses: actions/github-script@v7 + env: + LABEL_NAME: ${{ inputs.label_name }} + LABEL_COLOR: ${{ inputs.label_color }} + LABEL_DESCRIPTION: ${{ inputs.label_description }} + with: + script: | + try { + const result = await github.request('POST /repos/{owner}/{repo}/labels', { + owner: context.repo.owner, + repo: context.repo.repo, + name: process.env.LABEL_NAME, + color: process.env.LABEL_COLOR, + description: process.env.LABEL_DESCRIPTION, + }); + + core.info('Label was created.'); + } catch (error) { + if (error.status === 422) { + core.info('Label already exist.'); + } else { + core.setFailed(error.message); + } + } + + + - name: Adding label to issues + id: labeling_issues + uses: actions/github-script@v7 + env: + LABEL_NAME: ${{ inputs.label_name }} + ISSUES: ${{ steps.linked_issues.outputs.result }} + with: + script: | + const issues = JSON.parse(process.env.ISSUES); + + if (issues.length === 0) { + core.notice('No issues has found. Nothing to label.'); + return; + } + + for (const issue of issues) { + core.info(`Adding label to the issue #${ issue }...`); + await addLabelToIssue(issue, process.env.LABEL_NAME); + core.info('Done.\n'); + } + + async function addLabelToIssue(issue, label) { + return await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue, + labels: [ label ], + }); + } diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000000..05155cbd32 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,64 @@ +name: Pre-release + +on: + workflow_dispatch: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+-[a-z]+*' # e.g v1.2.3-alpha.2 + +jobs: + checks: + name: Run code checks + uses: "./.github/workflows/checks.yml" + secrets: inherit + + # publish_image: + # image will be published in e2e-tests.yml workflow + # name: Publish Docker image + # uses: './.github/workflows/publish-image.yml' + # secrets: inherit + + e2e_tests: + name: Run e2e tests + needs: checks + uses: "./.github/workflows/e2e-tests.yml" + secrets: inherit + + version: + name: Pre-release version info + runs-on: ubuntu-latest + outputs: + is_initial: ${{ steps.is_initial.outputs.result }} + steps: + - name: Determine if it is the initial version of the pre-release + id: is_initial + uses: actions/github-script@v7 + env: + TAG: ${{ github.ref_name }} + with: + script: | + const tag = process.env.TAG; + const REGEXP = /^v[0-9]+.[0-9]+.[0-9]+-[a-z]+((\.|-)\d+)?$/i; + const match = tag.match(REGEXP); + const isInitial = match && !match[1] ? true : false; + core.info('is_initial flag value: ', isInitial); + return isInitial; + + label_issues: + name: Add pre-release label to issues + uses: './.github/workflows/label-issues-in-release.yml' + needs: [ version ] + if: ${{ needs.version.outputs.is_initial == 'true' }} + with: + tag: ${{ github.ref_name }} + label_name: 'pre-release' + label_description: Tasks in pre-release right now + secrets: inherit + + # Temporary disable this step because it is broken + # There is an issue with building web3modal deps + upload_source_maps: + name: Upload source maps to Sentry + if: false + uses: './.github/workflows/upload-source-maps.yml' + secrets: inherit diff --git a/.github/workflows/project-management.yml b/.github/workflows/project-management.yml new file mode 100644 index 0000000000..1da733baeb --- /dev/null +++ b/.github/workflows/project-management.yml @@ -0,0 +1,99 @@ +name: Project management +on: + issues: + types: [ closed ] + pull_request: + types: [ review_requested ] + +jobs: + not_planned_issue: + name: Update task for not planned issue + if: ${{ github.event.issue && github.event.action == 'closed' && github.event.issue.state_reason == 'not_planned' }} + uses: './.github/workflows/update-project-cards.yml' + with: + project_name: ${{ vars.PROJECT_NAME }} + field_name: Status + field_value: Done + issues: "[${{ github.event.issue.number }}]" + secrets: inherit + + completed_issue: + name: Update task for completed issue + if: ${{ github.event.issue && github.event.action == 'closed' && github.event.issue.state_reason == 'completed' }} + uses: './.github/workflows/update-project-cards.yml' + with: + project_name: ${{ vars.PROJECT_NAME }} + field_name: Status + field_value: Ready For Realease + issues: "[${{ github.event.issue.number }}]" + secrets: inherit + + pr_linked_issues: + name: Get issues linked to PR + runs-on: ubuntu-latest + if: ${{ github.event.pull_request && github.event.action == 'review_requested' }} + outputs: + issues: ${{ steps.linked_issues.outputs.result }} + steps: + - name: Fetching issues linked to pull request + id: linked_issues + uses: actions/github-script@v7 + env: + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const response = await github.graphql(` + query ($owner: String!, $repo: String!, $pr: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pr) { + number + title + closingIssuesReferences(first: 100) { + nodes { + number + title + closed + } + } + } + } + } + `, { + owner: context.repo.owner, + repo: context.repo.repo, + pr: Number(process.env.PULL_REQUEST_NUMBER), + }); + + const { repository: { pullRequest: { closingIssuesReferences } } } = response; + const issues = closingIssuesReferences.nodes.map(({ number }) => number); + + if (!issues.length) { + core.notice(`No linked issues found for pull request #${ process.env.PULL_REQUEST_NUMBER }`); + return; + } + + core.info(`Found ${ issues.length } issue(s): ${ issues.join(', ') || '-' }`); + + return issues; + + issues_in_review: + name: Update status for issues in review + needs: [ pr_linked_issues ] + if: ${{ needs.pr_linked_issues.outputs.issues }} + uses: './.github/workflows/update-project-cards.yml' + secrets: inherit + with: + project_name: ${{ vars.PROJECT_NAME }} + field_name: Status + field_value: Review + issues: ${{ needs.pr_linked_issues.outputs.issues }} + + copy_labels: + name: Copy issues labels to pull request + needs: [ pr_linked_issues ] + if: ${{ needs.pr_linked_issues.outputs.issues }} + uses: './.github/workflows/copy-issues-labels.yml' + secrets: inherit + with: + pr_number: ${{ github.event.pull_request.number }} + issues: ${{ needs.pr_linked_issues.outputs.issues }} diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 0000000000..97c1102e8f --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,84 @@ +name: Publish Docker image + +on: + workflow_dispatch: + inputs: + tags: + description: Image tags + required: false + type: string + build_args: + description: Build-time variables + required: false + type: string + platforms: + description: Image platforms (you can specify multiple platforms separated by comma) + required: false + type: string + default: linux/amd64 + workflow_call: + inputs: + tags: + description: Image tags + required: false + type: string + build_args: + description: Build-time variables + required: false + type: string + platforms: + description: Image platforms (you can specify multiple platforms separated by comma) + required: false + type: string + default: linux/amd64 + +jobs: + run: + name: Run + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/blockscout/frontend + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Add SHORT_SHA env property with commit short sha + run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + + - name: Debug + env: + REF_TYPE: ${{ github.ref_type }} + REF_NAME: ${{ github.ref_name }} + run: | + echo "ref_type: $REF_TYPE" + echo "ref_name: $REF_NAME" + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + cache-from: type=gha + tags: ${{ inputs.tags || steps.meta.outputs.tags }} + platforms: ${{ inputs.platforms }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + GIT_COMMIT_SHA=${{ env.SHORT_SHA }} + GIT_TAG=${{ github.ref_type == 'tag' && github.ref_name || '' }} + ${{ inputs.build_args }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..df2c0433b1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,91 @@ +name: Release + +on: + workflow_dispatch: + release: + types: [ released ] + +jobs: + remove_prerelease_label: + name: Remove pre-release label from issues + runs-on: ubuntu-latest + steps: + - name: Remove label + id: tags + uses: actions/github-script@v7 + env: + LABEL_NAME: pre-release + with: + script: | + const { data: issues } = await github.request('GET /repos/{owner}/{repo}/issues', { + owner: context.repo.owner, + repo: context.repo.repo, + labels: process.env.LABEL_NAME, + state: 'all' + }); + + if (issues.length === 0) { + core.notice(`No issues with label "${ process.env.LABEL_NAME }" found.`); + return; + } + + const issueIds = issues.map(({ node_id }) => node_id); + const labelId = issues[0].labels.find(({ name }) => name === process.env.LABEL_NAME).node_id; + + core.info(`Found ${ issueIds.length } issues with label "${ process.env.LABEL_NAME }"`); + + for (const issueId of issueIds) { + core.info(`Removing label for issue with node_id ${ issueId }...`); + + await github.graphql(` + mutation($input: RemoveLabelsFromLabelableInput!) { + removeLabelsFromLabelable(input: $input) { + clientMutationId + } + } + `, { + input: { + labelIds: [ labelId ], + labelableId: issueId + }, + }); + + core.info('Done.\n'); + } + + + label_released_issues: + name: Label released issues + uses: './.github/workflows/label-issues-in-release.yml' + with: + tag: ${{ github.ref_name }} + label_name: ${{ github.ref_name }} + label_description: Release ${{ github.ref_name }} + secrets: inherit + + update_project_cards: + name: Update project tasks statuses + needs: label_released_issues + uses: './.github/workflows/update-project-cards.yml' + with: + project_name: ${{ vars.PROJECT_NAME }} + field_name: Status + field_value: Released + issues: ${{ needs.label_released_issues.outputs.issues }} + secrets: inherit + + publish_image: + name: Publish Docker image + uses: './.github/workflows/publish-image.yml' + secrets: inherit + with: + platforms: linux/amd64,linux/arm64/v8 + + # Temporary disable this step because it is broken + # There is an issue with building web3modal deps + upload_source_maps: + name: Upload source maps to Sentry + if: false + needs: publish_image + uses: './.github/workflows/upload-source-maps.yml' + secrets: inherit diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 0000000000..bcc1a36ca1 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,29 @@ +name: Close inactive issues +on: + schedule: + - cron: "55 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + # issues + only-issue-labels: "need info" + days-before-issue-stale: 14 + days-before-issue-close: 7 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 14 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." + + # pull requests + days-before-pr-stale: -1 + days-before-pr-close: -1 + + # other settings + repo-token: ${{ secrets.GITHUB_TOKEN }} + close-issue-reason: "not_planned" \ No newline at end of file diff --git a/.github/workflows/update-project-cards.yml b/.github/workflows/update-project-cards.yml new file mode 100644 index 0000000000..b071ba61da --- /dev/null +++ b/.github/workflows/update-project-cards.yml @@ -0,0 +1,240 @@ +name: Update project cards for issues + +on: + workflow_dispatch: + inputs: + project_name: + description: Name of the project + default: Front-end tasks + required: true + type: string + field_name: + description: Field name to be updated + default: Status + required: true + type: string + field_value: + description: New value of the field + default: Released + required: true + type: string + issues: + description: JSON encoded list of issue numbers to be updated + required: true + type: string + workflow_call: + inputs: + project_name: + description: Name of the project + required: true + type: string + field_name: + description: Field name to be updated + required: true + type: string + field_value: + description: New value of the field + required: true + type: string + issues: + description: JSON encoded list of issue numbers to be updated + required: true + type: string + +jobs: + run: + name: Run + runs-on: ubuntu-latest + steps: + - name: Getting project info + id: project_info + uses: actions/github-script@v7 + env: + PROJECT_NAME: ${{ inputs.project_name }} + FIELD_NAME: ${{ inputs.field_name }} + FIELD_VALUE: ${{ inputs.field_value }} + with: + github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }} + script: | + const response = await github.graphql(` + query ($login: String!, $q: String!) { + organization(login: $login) { + projectsV2(query: $q, first: 1) { + nodes { + id, + title, + number, + fields(first: 20) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + } + `, { + login: context.repo.owner, + q: process.env.PROJECT_NAME, + }); + + const { organization: { projectsV2: { nodes: projects } } } = response; + + if (projects.length === 0) { + core.setFailed('Project not found.'); + return; + } + + if (projects.length > 1) { + core.info(`Fould ${ projects.length } with the similar name:`); + projects.forEach((issue) => { + core.info(` #${ projects.number } - ${ projects.title }`); + }) + core.setFailed('Fould multiple projects with the similar name. Cannot determine which one to use.'); + return; + } + + const { id: projectId, fields: { nodes: fields } } = projects[0]; + const field = fields.find((field) => field.name === process.env.FIELD_NAME); + + if (!field) { + core.setFailed(`Field with name "${ process.env.FIELD_NAME }" not found in the project.`); + return; + } + + const option = field.options.find((option) => option.name === process.env.FIELD_VALUE); + + if (!option) { + core.setFailed(`Option with name "${ process.env.FIELD_VALUE }" not found in the field possible values.`); + return; + } + + core.info('Found following info:'); + core.info(` project_id: ${ projectId }`); + core.info(` field_id: ${ field.id }`); + core.info(` field_value_id: ${ option.id }`); + + core.setOutput('id', projectId); + core.setOutput('field_id', field.id); + core.setOutput('field_value_id', option.id); + + - name: Getting project items that linked to the issues + id: items + uses: actions/github-script@v7 + env: + ISSUES: ${{ inputs.issues }} + with: + github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }} + script: | + const result = []; + const issues = JSON.parse(process.env.ISSUES); + + for (const issue of issues) { + const response = await getProjectItemId(issue); + response?.length > 0 && result.push(...response); + } + + return result; + + async function getProjectItemId(issueId) { + core.info(`Fetching project items for issue #${ issueId }...`); + + try { + const response = await github.graphql(` + query ($owner: String!, $repo: String!, $id: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $id) { + title, + projectItems(first: 10) { + nodes { + id, + } + } + } + } + } + `, + { + owner: context.repo.owner, + repo: context.repo.repo, + id: issueId, + } + ); + + const { repository: { issue: { projectItems: { nodes: projectItems } } } } = response; + + if (projectItems.length === 0) { + core.info('No project items found.\n'); + return []; + } + + const ids = projectItems.map((item) => item.id); + core.info(`Found [ ${ ids.join(', ') } ].\n`); + return ids; + + } catch (error) { + if (error.status === 404) { + core.info('Nothing has found.\n'); + return []; + } + } + } + + - name: Updating field value of the project items + id: updating_items + uses: actions/github-script@v7 + env: + ITEMS: ${{ steps.items.outputs.result }} + PROJECT_ID: ${{ steps.project_info.outputs.id }} + FIELD_ID: ${{ steps.project_info.outputs.field_id }} + FIELD_VALUE_ID: ${{ steps.project_info.outputs.field_value_id }} + with: + github-token: ${{ secrets.BOT_LABEL_ISSUE_TOKEN }} + script: | + const items = JSON.parse(process.env.ITEMS); + + if (items.length === 0) { + core.info('Nothing to update.'); + core.notice('No project items found for provided issues. Nothing to update.'); + return; + } + + for (const item of items) { + core.info(`Changing field value for item ${ item }...`); + await changeItemFieldValue(item); + core.info('Done.\n'); + } + + async function changeItemFieldValue(itemId) { + return await github.graphql( + ` + mutation($input: UpdateProjectV2ItemFieldValueInput!) { + updateProjectV2ItemFieldValue(input: $input) { + clientMutationId + } + } + `, + { + input: { + projectId: process.env.PROJECT_ID, + fieldId: process.env.FIELD_ID, + itemId, + value: { + singleSelectOptionId: process.env.FIELD_VALUE_ID, + }, + }, + } + ); + }; + diff --git a/.github/workflows/upload-source-maps.yml b/.github/workflows/upload-source-maps.yml new file mode 100644 index 0000000000..55b18067fe --- /dev/null +++ b/.github/workflows/upload-source-maps.yml @@ -0,0 +1,48 @@ +name: Upload source maps to Sentry +on: + workflow_call: + workflow_dispatch: + +env: + SENTRY_ORG: ${{ vars.SENTRY_ORG }} + SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + +jobs: + build_and_upload: + name: Build app with source maps and upload to Sentry + runs-on: ubuntu-latest + if: ${{ github.ref_type == 'tag' }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.11.0 + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile --ignore-optional + + - name: Make production build with source maps + run: yarn build + env: + NODE_ENV: production + + - name: Inject Sentry debug ID + run: yarn sentry-cli sourcemaps inject ./.next + + - name: Upload source maps to Sentry + run: yarn sentry-cli sourcemaps upload --release=${{ github.ref_name }} --url-prefix=~/_next/ --validate ./.next \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..24bd046ba4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/node_modules_linux +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +/public/assets/envs.js +/public/assets/configs +/public/icons/sprite.svg +/public/icons/README.md +/analyze + +# production +/build + +# misc +.DS_Store +*.pem +.tools +grafana + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local +/configs/envs/.env.secrets +/configs/envs/.samples + +# typescript +*.tsbuildinfo + +.eslintcache + +# Sentry +.sentryclirc + +**.decrypted~** +/test-results/ +/playwright-report/ +/playwright/.cache/ +/playwright/.browser/ +/playwright/envs.js +/playwright/affected-tests.txt + +**.dec** + +# build outputs +/tools/preset-sync/index.js diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100755 index 0000000000..c37815e2b5 --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; } +git lfs post-checkout "$@" diff --git a/.husky/post-commit b/.husky/post-commit new file mode 100755 index 0000000000..e5230c305f --- /dev/null +++ b/.husky/post-commit @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; } +git lfs post-commit "$@" diff --git a/.husky/post-merge b/.husky/post-merge new file mode 100755 index 0000000000..c99b752a52 --- /dev/null +++ b/.husky/post-merge @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; } +git lfs post-merge "$@" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..c81f321caf --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# lint js/ts files +echo 🧿 Running file linter... +npx lint-staged + +# format svg +echo 🧿 Running svg formatter... +for file in `git diff --diff-filter=ACMRT --cached --name-only | grep ".svg\$"` + do + echo "Formatting $file" + ./node_modules/.bin/svgo -q $file + git add $file + done + +echo ✅ All pre-commit jobs are done diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000000..216e91527e --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; } +git lfs pre-push "$@" diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..8b0beab16a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.11.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..e2c659e246 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "formulahendry.auto-close-tag", + "formulahendry.auto-rename-tag", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "ms-vscode-remote.remote-containers", + "ms-azuretools.vscode-docker", + "github.vscode-pull-request-github", + "yatki.vscode-surround", + "simonsiefke.svg-preview" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..b24083bbe1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest: watch current file", + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + "args": [ + "${fileBasename}", + "--runInBand", + "--verbose", + "-i", + "--no-cache", + "--watchAll", + "--testTimeout=1000000000", + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..55712c19f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..0d8a70e1ea --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,381 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + // DEV SERVER + { + "type": "shell", + "command": "yarn dev:preset ${input:dev_config_preset}", + "problemMatcher": [], + "label": "dev server", + "detail": "start local dev server", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiMagenta", + "id": "server-process" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "shell", + "command": "yarn dev:preset:sync --name=${input:dev_config_preset}", + "problemMatcher": [], + "label": "dev preset sync", + "detail": "syncronize dev preset", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiMagenta", + "id": "repo-sync" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + + // CODE CHECKS + { + "type": "typescript", + "label": "tsc build", + "detail": "compile typescript", + "tsconfig": "tsconfig.json", + "problemMatcher": [ + "$tsc" + ], + "icon": { + "color": "terminal.ansiCyan", + "id": "symbol-type-parameter" + }, + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "group": "build", + }, + { + "type": "npm", + "script": "lint:eslint:fix", + "problemMatcher": [], + "label": "eslint", + "detail": "run eslint", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiYellow", + "id": "zap" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + + // PW TESTS + { + "type": "shell", + "command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}", + "problemMatcher": [], + "label": "pw: local", + "detail": "run visual components tests for current file", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "beaker" + }, + "runOptions": { + "instanceLimit": 1 + }, + }, + { + "type": "shell", + "command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}", + "problemMatcher": [], + "label": "pw: docker", + "detail": "run visual components tests for current file", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "beaker" + }, + "runOptions": { + "instanceLimit": 1 + }, + }, + { + "type": "shell", + "command": "yarn test:pw:docker ${input:pwArgs}", + "problemMatcher": [], + "label": "pw: docker all", + "detail": "run visual components tests", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "beaker" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "shell", + "command": "npx playwright show-report", + "problemMatcher": [], + "label": "pw: report", + "detail": "serve test report", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "output" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "shell", + "command": "yarn test:pw:detect-affected", + "problemMatcher": [], + "label": "pw: detect affected", + "detail": "detect PW tests affected by changes in current branch", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "diff" + }, + "runOptions": { + "instanceLimit": 1 + }, + }, + + // JEST TESTS + { + "type": "npm", + "script": "test:jest", + "problemMatcher": [], + "label": "jest", + "detail": "run jest tests", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "beaker" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "npm", + "script": "test:jest:watch", + "problemMatcher": [], + "label": "jest: watch all", + "detail": "run jest tests in watch mode", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "beaker" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "shell", + "command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch", + "problemMatcher": [], + "label": "jest: watch", + "detail": "run jest tests in watch mode for current file", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiBlue", + "id": "beaker" + }, + "runOptions": { + "instanceLimit": 1 + }, + }, + + { + "type": "npm", + "script": "build:docker", + "problemMatcher": [], + "label": "docker: build", + "detail": "build docker image", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiRed", + "id": "symbol-structure" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "shell", + "command": "yarn start:docker:preset ${input:dev_config_preset}", + "problemMatcher": [], + "label": "docker: run", + "detail": "run docker container with env preset", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiRed", + "id": "browser" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + { + "type": "npm", + "script": "svg:format", + "problemMatcher": [], + "label": "format svg", + "detail": "format svg files with svgo", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true, + "close": false, + "revealProblems": "onProblem", + }, + "icon": { + "color": "terminal.ansiCyan", + "id": "combine" + }, + "runOptions": { + "instanceLimit": 1 + } + }, + ], + "inputs": [ + { + "type": "pickString", + "id": "pwDebugFlag", + "description": "What debug flag you want to use?", + "options": [ + "", + "PWDEBUG=1", + "DEBUG=pw:browser,pw:api", + "DEBUG=*", + ], + "default": "" + }, + { + "type": "pickString", + "id": "pwArgs", + "description": "What args you want to pass?", + "options": [ + "", + "--update-snapshots", + "--update-snapshots --affected", + "--ui", + ], + "default": "" + }, + { + "type": "pickString", + "id": "dev_config_preset", + "description": "Choose dev server config preset:", + "options": [ + "main", + "localhost", + "arbitrum", + "base", + "celo_alfajores", + "garnet", + "gnosis", + "eth", + "eth_goerli", + "eth_sepolia", + "optimism", + "optimism_sepolia", + "polygon", + "rootstock_testnet", + "stability_testnet", + "zkevm", + "zksync", + ], + "default": "main" + }, + ], +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..5726fbc353 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at andrew@poa.network. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..a325403e92 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,142 @@ +# ***************************** +# *** STAGE 1: Dependencies *** +# ***************************** +FROM node:20.11.0-alpine AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat python3 make g++ +RUN ln -sf /usr/bin/python3 /usr/bin/python + +### APP +# Install dependencies +WORKDIR /app +COPY package.json yarn.lock ./ +RUN apk add git +RUN yarn --frozen-lockfile + + +### FEATURE REPORTER +# Install dependencies +WORKDIR /feature-reporter +COPY ./deploy/tools/feature-reporter/package.json ./deploy/tools/feature-reporter/yarn.lock ./ +RUN yarn --frozen-lockfile + + +### ENV VARIABLES CHECKER +# Install dependencies +WORKDIR /envs-validator +COPY ./deploy/tools/envs-validator/package.json ./deploy/tools/envs-validator/yarn.lock ./ +RUN yarn --frozen-lockfile + + +# ***************************** +# ****** STAGE 2: Build ******* +# ***************************** +FROM node:20.11.0-alpine AS builder +RUN apk add --no-cache --upgrade libc6-compat bash + +# pass build args to env variables +ARG GIT_COMMIT_SHA +ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA +ARG GIT_TAG +ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG +ARG NEXT_OPEN_TELEMETRY_ENABLED +ENV NEXT_OPEN_TELEMETRY_ENABLED=$NEXT_OPEN_TELEMETRY_ENABLED + +ENV NODE_ENV production + +### APP +# Copy dependencies and source code +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Generate .env.registry with ENVs list and save build args into .env file +COPY --chmod=755 ./deploy/scripts/collect_envs.sh ./ +RUN ./collect_envs.sh ./docs/ENVS.md + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +# Build app for production +RUN yarn svg:build-sprite +RUN yarn build + + +### FEATURE REPORTER +# Copy dependencies and source code, then build +COPY --from=deps /feature-reporter/node_modules ./deploy/tools/feature-reporter/node_modules +RUN cd ./deploy/tools/feature-reporter && yarn compile_config +RUN cd ./deploy/tools/feature-reporter && yarn build + + +### ENV VARIABLES CHECKER +# Copy dependencies and source code, then build +COPY --from=deps /envs-validator/node_modules ./deploy/tools/envs-validator/node_modules +RUN cd ./deploy/tools/envs-validator && yarn build + + +# ***************************** +# ******* STAGE 3: Run ******** +# ***************************** +# Production image, copy all the files and run next +FROM node:20.11.0-alpine AS runner +RUN apk add --no-cache --upgrade bash curl jq unzip + +### APP +WORKDIR /app + +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +COPY --from=builder /app/next.config.js ./ +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/deploy/tools/envs-validator/index.js ./envs-validator.js +COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-reporter.js + +# Copy scripts +## Entripoint +COPY --chmod=755 ./deploy/scripts/entrypoint.sh . +## ENV validator and client script maker +COPY --chmod=755 ./deploy/scripts/validate_envs.sh . +COPY --chmod=755 ./deploy/scripts/make_envs_script.sh . +## Assets downloader +COPY --chmod=755 ./deploy/scripts/download_assets.sh . +## Favicon generator +COPY --chmod=755 ./deploy/scripts/favicon_generator.sh . +COPY ./deploy/tools/favicon-generator ./deploy/tools/favicon-generator +RUN ["chmod", "-R", "777", "./deploy/tools/favicon-generator"] +RUN ["chmod", "-R", "777", "./public"] + +# Copy ENVs files +COPY --from=builder /app/.env.registry . +COPY --from=builder /app/.env . + +# Copy ENVs presets +ARG ENVS_PRESET +ENV ENVS_PRESET=$ENVS_PRESET +COPY ./configs/envs ./configs/envs + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +ENTRYPOINT ["./entrypoint.sh"] + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 + +CMD ["node", "server.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..20d40b6bce --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000..1256043efe --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +

Blockscout frontend

+ +

+ Frontend application for + Blockscout + blockchain explorer +

+ +## Running and configuring the app + +App is distributed as a docker image. Here you can find information about the [package](https://github.com/blockscout/frontend/pkgs/container/frontend) and its recent [releases](https://github.com/blockscout/frontend/releases). + +You can configure your app by passing necessary environment variables when starting the container. See full list of ENVs and their description [here](./docs/ENVS.md). + +```sh +docker run -p 3000:3000 --env-file ghcr.io/blockscout/frontend:latest +``` + +Alternatively, you can build your own docker image and run your app from that. Please follow this [guide](./docs/CUSTOM_BUILD.md). + +For more information on migrating from the previous frontend, please see the [frontend migration docs](https://docs.blockscout.com/for-developers/frontend-migration). + +## Contributing + +See our [Contribution guide](./docs/CONTRIBUTING.md) for pull request protocol. We expect contributors to follow our [code of conduct](./CODE_OF_CONDUCT.md) when submitting code or comments. + +## Resources +- [App ENVs list](./docs/ENVS.md) +- [Contribution guide](./docs/CONTRIBUTING.md) +- [Making a custom build](./docs/CUSTOM_BUILD.md) +- [Frontend migration guide](https://docs.blockscout.com/for-developers/frontend-migration) +- [Manual deployment guide with backend and microservices](https://docs.blockscout.com/for-developers/deployment/manual-deployment-guide) + +## License + +[![License: GPL v3.0](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + +This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000000..30ec4f8b6f --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,33 @@ +## 🚀 New Features +- Description of the new feature 1. +- Description of the new feature 2. + +## 🐛 Bug Fixes +- Description of the bug fix 1. +- Description of the bug fix 2. + +## ⚡ Performance Improvements +- Description of the performance improvement 1. +- Description of the performance improvement 2. + +## 📦 Dependencies updates +- Updated dependency: PackageName 1 to version x.x.x. +- Updated dependency: PackageName 2 to version x.x.x. + +## ✨ Other Changes +- Another minor change 1. +- Another minor change 2. + +## 🚨 Changes in ENV variables +- Added new environment variable: ENV_VARIABLE_NAME with value. +- Updated existing environment variable: ENV_VARIABLE_NAME to new value. + +**Full list of the ENV variables**: [v1.2.3](https://github.com/blockscout/frontend/blob/v1.2.3/docs/ENVS.md) + +## 🦄 New Contributors +- @contributor1 made their first contribution in https://github.com/blockscout/frontend/pull/1 +- @contributor2 made their first contribution in https://github.com/blockscout/frontend/pull/2 + +--- + +**Full Changelog**: https://github.com/blockscout/frontend/compare/v1.2.2...v1.2.3 diff --git a/configs/app/api.ts b/configs/app/api.ts new file mode 100644 index 0000000000..0afe9a8aec --- /dev/null +++ b/configs/app/api.ts @@ -0,0 +1,32 @@ +import stripTrailingSlash from 'lib/stripTrailingSlash'; + +import { getEnvValue } from './utils'; + +const apiHost = getEnvValue('NEXT_PUBLIC_API_HOST'); +const apiSchema = getEnvValue('NEXT_PUBLIC_API_PROTOCOL') || 'https'; +const apiPort = getEnvValue('NEXT_PUBLIC_API_PORT'); +const apiEndpoint = [ + apiSchema || 'https', + '://', + apiHost, + apiPort && ':' + apiPort, +].filter(Boolean).join(''); + +const socketSchema = getEnvValue('NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL') || 'wss'; +const socketEndpoint = [ + socketSchema, + '://', + apiHost, + apiPort && ':' + apiPort, +].filter(Boolean).join(''); + +const api = Object.freeze({ + host: apiHost, + protocol: apiSchema, + port: apiPort, + endpoint: apiEndpoint, + socket: socketEndpoint, + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_API_BASE_PATH') || ''), +}); + +export default api; diff --git a/configs/app/app.ts b/configs/app/app.ts new file mode 100644 index 0000000000..b0403fc673 --- /dev/null +++ b/configs/app/app.ts @@ -0,0 +1,23 @@ +import { getEnvValue } from './utils'; + +const appPort = getEnvValue('NEXT_PUBLIC_APP_PORT'); +const appSchema = getEnvValue('NEXT_PUBLIC_APP_PROTOCOL'); +const appHost = getEnvValue('NEXT_PUBLIC_APP_HOST'); +const baseUrl = [ + appSchema || 'https', + '://', + appHost, + appPort && ':' + appPort, +].filter(Boolean).join(''); +const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development'; + +const app = Object.freeze({ + isDev, + protocol: appSchema, + host: appHost, + port: appPort, + baseUrl, + useProxy: getEnvValue('NEXT_PUBLIC_USE_NEXT_JS_PROXY') === 'true', +}); + +export default app; diff --git a/configs/app/chain.ts b/configs/app/chain.ts new file mode 100644 index 0000000000..0ee6d2de6f --- /dev/null +++ b/configs/app/chain.ts @@ -0,0 +1,25 @@ +import { getEnvValue } from './utils'; + +const DEFAULT_CURRENCY_DECIMALS = 18; + +const chain = Object.freeze({ + id: getEnvValue('NEXT_PUBLIC_NETWORK_ID'), + name: getEnvValue('NEXT_PUBLIC_NETWORK_NAME'), + shortName: getEnvValue('NEXT_PUBLIC_NETWORK_SHORT_NAME'), + currency: { + name: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_NAME'), + weiName: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME'), + symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'), + decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS, + }, + secondaryCoin: { + symbol: getEnvValue('NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL'), + }, + hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true', + tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC', + rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), + isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', + verificationType: getEnvValue('NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE') || 'mining', +}); + +export default chain; diff --git a/configs/app/features/account.ts b/configs/app/features/account.ts new file mode 100644 index 0000000000..120a94b7e9 --- /dev/null +++ b/configs/app/features/account.ts @@ -0,0 +1,52 @@ +import type { Feature } from './types'; + +import stripTrailingSlash from 'lib/stripTrailingSlash'; + +import app from '../app'; +import { getEnvValue } from '../utils'; + +const authUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_AUTH_URL') || app.baseUrl); + +const logoutUrl = (() => { + try { + const envUrl = getEnvValue('NEXT_PUBLIC_LOGOUT_URL'); + const auth0ClientId = getEnvValue('NEXT_PUBLIC_AUTH0_CLIENT_ID'); + const returnUrl = authUrl + '/auth/logout'; + + if (!envUrl || !auth0ClientId) { + throw Error(); + } + + const url = new URL(envUrl); + url.searchParams.set('client_id', auth0ClientId); + url.searchParams.set('returnTo', returnUrl); + + return url.toString(); + } catch (error) { + return; + } +})(); + +const title = 'My account'; + +const config: Feature<{ authUrl: string; logoutUrl: string }> = (() => { + if ( + getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && + authUrl && + logoutUrl + ) { + return Object.freeze({ + title, + isEnabled: true, + authUrl, + logoutUrl, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/addressMetadata.ts b/configs/app/features/addressMetadata.ts new file mode 100644 index 0000000000..5ca5b78ada --- /dev/null +++ b/configs/app/features/addressMetadata.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST'); + +const title = 'Address metadata'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (apiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/addressVerification.ts b/configs/app/features/addressVerification.ts new file mode 100644 index 0000000000..f5f6cf4cf3 --- /dev/null +++ b/configs/app/features/addressVerification.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; +import account from './account'; +import verifiedTokens from './verifiedTokens'; + +const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); + +const title = 'Address verification in "My account"'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (account.isEnabled && verifiedTokens.isEnabled && adminServiceApiHost) { + return Object.freeze({ + title: 'Address verification in "My account"', + isEnabled: true, + api: { + endpoint: adminServiceApiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/adsBanner.ts b/configs/app/features/adsBanner.ts new file mode 100644 index 0000000000..f9860d1afc --- /dev/null +++ b/configs/app/features/adsBanner.ts @@ -0,0 +1,89 @@ +import type { Feature } from './types'; +import type { AdButlerConfig } from 'types/client/adButlerConfig'; +import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders'; +import type { AdBannerProviders, AdBannerAdditionalProviders } from 'types/client/adProviders'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const provider: AdBannerProviders = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_AD_BANNER_PROVIDER') as AdBannerProviders; + + return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise'; +})(); + +const additionalProvider = getEnvValue('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER') as AdBannerAdditionalProviders; + +const title = 'Banner ads'; + +type AdsBannerFeaturePayload = { + provider: Exclude; +} | { + provider: 'adbutler'; + adButler: { + config: { + desktop: AdButlerConfig; + mobile: AdButlerConfig; + }; + }; +} | { + provider: Exclude; + additionalProvider: 'adbutler'; + adButler: { + config: { + desktop: AdButlerConfig; + mobile: AdButlerConfig; + }; + }; +} + +const config: Feature = (() => { + if (provider === 'adbutler') { + const desktopConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP')); + const mobileConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE')); + + if (desktopConfig && mobileConfig) { + return Object.freeze({ + title, + isEnabled: true, + provider, + adButler: { + config: { + desktop: desktopConfig, + mobile: mobileConfig, + }, + }, + }); + } + } else if (provider !== 'none') { + + if (additionalProvider === 'adbutler') { + const desktopConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP')); + const mobileConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE')); + + return Object.freeze({ + title, + isEnabled: true, + provider, + additionalProvider, + adButler: { + config: { + desktop: desktopConfig, + mobile: mobileConfig, + }, + }, + }); + } + return Object.freeze({ + title, + isEnabled: true, + provider, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/adsText.ts b/configs/app/features/adsText.ts new file mode 100644 index 0000000000..82946f984f --- /dev/null +++ b/configs/app/features/adsText.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; +import { SUPPORTED_AD_TEXT_PROVIDERS } from 'types/client/adProviders'; +import type { AdTextProviders } from 'types/client/adProviders'; + +import { getEnvValue } from '../utils'; + +const provider: AdTextProviders = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_AD_TEXT_PROVIDER') as AdTextProviders; + return envValue && SUPPORTED_AD_TEXT_PROVIDERS.includes(envValue) ? envValue : 'coinzilla'; +})(); + +const title = 'Text ads'; + +const config: Feature<{ provider: AdTextProviders }> = (() => { + if (provider !== 'none') { + return Object.freeze({ + title, + isEnabled: true, + provider, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/beaconChain.ts b/configs/app/features/beaconChain.ts new file mode 100644 index 0000000000..a29540b756 --- /dev/null +++ b/configs/app/features/beaconChain.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'Beacon chain'; + +const config: Feature<{ currency: { symbol: string } }> = (() => { + if (getEnvValue('NEXT_PUBLIC_HAS_BEACON_CHAIN') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + currency: { + symbol: + getEnvValue('NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL') || + getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL') || + '', // maybe we need some other default value here + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/blockchainInteraction.ts b/configs/app/features/blockchainInteraction.ts new file mode 100644 index 0000000000..788e059ec9 --- /dev/null +++ b/configs/app/features/blockchainInteraction.ts @@ -0,0 +1,38 @@ +import type { Feature } from './types'; + +import chain from '../chain'; +import { getEnvValue } from '../utils'; + +const walletConnectProjectId = getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID'); + +const title = 'Blockchain interaction (writing to contract, etc.)'; + +const config: Feature<{ walletConnect: { projectId: string } }> = (() => { + + if ( + // all chain parameters are required for wagmi provider + // @wagmi/chains/dist/index.d.ts + chain.id && + chain.name && + chain.currency.name && + chain.currency.symbol && + chain.currency.decimals && + chain.rpcUrl && + walletConnectProjectId + ) { + return Object.freeze({ + title, + isEnabled: true, + walletConnect: { + projectId: walletConnectProjectId, + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/bridgedTokens.ts b/configs/app/features/bridgedTokens.ts new file mode 100644 index 0000000000..ae275c5397 --- /dev/null +++ b/configs/app/features/bridgedTokens.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; +import type { BridgedTokenChain, TokenBridge } from 'types/client/token'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const title = 'Bridged tokens'; + +const config: Feature<{ chains: Array; bridges: Array }> = (() => { + const chains = parseEnvJson>(getEnvValue('NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS')); + const bridges = parseEnvJson>(getEnvValue('NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES')); + + if (chains && chains.length > 0 && bridges && bridges.length > 0) { + return Object.freeze({ + title, + isEnabled: true, + chains, + bridges, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/csvExport.ts b/configs/app/features/csvExport.ts new file mode 100644 index 0000000000..442b4bedc7 --- /dev/null +++ b/configs/app/features/csvExport.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import services from '../services'; + +const title = 'Export data to CSV file'; + +const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => { + if (services.reCaptcha.siteKey) { + return Object.freeze({ + title, + isEnabled: true, + reCaptcha: { + siteKey: services.reCaptcha.siteKey, + }, + }); + } + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/dataAvailability.ts b/configs/app/features/dataAvailability.ts new file mode 100644 index 0000000000..add9e5fec5 --- /dev/null +++ b/configs/app/features/dataAvailability.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'Data availability'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/deFiDropdown.ts b/configs/app/features/deFiDropdown.ts new file mode 100644 index 0000000000..62b8fdcd14 --- /dev/null +++ b/configs/app/features/deFiDropdown.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; +import type { DeFiDropdownItem } from 'types/client/deFiDropdown'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const items = parseEnvJson>(getEnvValue('NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS')) || []; + +const title = 'DeFi dropdown'; + +const config: Feature<{ items: Array }> = items.length > 0 ? + Object.freeze({ + title, + isEnabled: true, + items, + }) : + Object.freeze({ + title, + isEnabled: false, + }); + +export default config; diff --git a/configs/app/features/faultProofSystem.ts b/configs/app/features/faultProofSystem.ts new file mode 100644 index 0000000000..38a8f021db --- /dev/null +++ b/configs/app/features/faultProofSystem.ts @@ -0,0 +1,22 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; +import rollup from './rollup'; + +const title = 'Fault proof system'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (rollup.isEnabled && rollup.type === 'optimistic' && getEnvValue('NEXT_PUBLIC_FAULT_PROOF_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/gasTracker.ts b/configs/app/features/gasTracker.ts new file mode 100644 index 0000000000..c20242e602 --- /dev/null +++ b/configs/app/features/gasTracker.ts @@ -0,0 +1,37 @@ +import type { Feature } from './types'; +import { GAS_UNITS } from 'types/client/gasTracker'; +import type { GasUnit } from 'types/client/gasTracker'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const isDisabled = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_ENABLED') === 'false'; + +const units = ((): Array => { + const envValue = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_UNITS'); + if (!envValue) { + return [ 'usd', 'gwei' ]; + } + + const units = parseEnvJson>(envValue)?.filter((type) => GAS_UNITS.includes(type)) || []; + + return units; +})(); + +const title = 'Gas tracker'; + +const config: Feature<{ units: Array }> = (() => { + if (!isDisabled && units.length > 0) { + return Object.freeze({ + title, + isEnabled: true, + units, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/getGasButton.ts b/configs/app/features/getGasButton.ts new file mode 100644 index 0000000000..db392f3c9a --- /dev/null +++ b/configs/app/features/getGasButton.ts @@ -0,0 +1,35 @@ +import type { Feature } from './types'; +import type { GasRefuelProviderConfig } from 'types/client/gasRefuelProviderConfig'; + +import chain from '../chain'; +import { getEnvValue, parseEnvJson } from '../utils'; +import marketplace from './marketplace'; + +const value = parseEnvJson(getEnvValue('NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG')); + +const title = 'Get gas button'; + +const config: Feature<{ + name: string; + logoUrl?: string; + url: string; + dappId?: string; +}> = (() => { + if (value) { + return Object.freeze({ + title, + isEnabled: true, + name: value.name, + logoUrl: value.logo, + url: value.url_template.replace('{chainId}', chain.id || ''), + dappId: marketplace.isEnabled ? value.dapp_id : undefined, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/googleAnalytics.ts b/configs/app/features/googleAnalytics.ts new file mode 100644 index 0000000000..4fe9a9bf9e --- /dev/null +++ b/configs/app/features/googleAnalytics.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const propertyId = getEnvValue('NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID'); + +const title = 'Google analytics'; + +const config: Feature<{ propertyId: string }> = (() => { + if (propertyId) { + return Object.freeze({ + title, + isEnabled: true, + propertyId, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/graphqlApiDocs.ts b/configs/app/features/graphqlApiDocs.ts new file mode 100644 index 0000000000..d26c3bfde3 --- /dev/null +++ b/configs/app/features/graphqlApiDocs.ts @@ -0,0 +1,25 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const defaultTxHash = getEnvValue('NEXT_PUBLIC_GRAPHIQL_TRANSACTION'); + +const title = 'GraphQL API documentation'; + +const config: Feature<{ defaultTxHash: string | undefined }> = (() => { + + if (defaultTxHash === 'none') { + return Object.freeze({ + title, + isEnabled: false, + }); + } + + return Object.freeze({ + title, + isEnabled: true, + defaultTxHash, + }); +})(); + +export default config; diff --git a/configs/app/features/growthBook.ts b/configs/app/features/growthBook.ts new file mode 100644 index 0000000000..af672c5ac9 --- /dev/null +++ b/configs/app/features/growthBook.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY'); + +const title = 'GrowthBook feature flagging and A/B testing'; + +const config: Feature<{ clientKey: string }> = (() => { + if (clientKey) { + return Object.freeze({ + title, + isEnabled: true, + clientKey, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts new file mode 100644 index 0000000000..3b25e5570c --- /dev/null +++ b/configs/app/features/index.ts @@ -0,0 +1,36 @@ +export { default as account } from './account'; +export { default as addressVerification } from './addressVerification'; +export { default as addressMetadata } from './addressMetadata'; +export { default as adsBanner } from './adsBanner'; +export { default as adsText } from './adsText'; +export { default as beaconChain } from './beaconChain'; +export { default as bridgedTokens } from './bridgedTokens'; +export { default as blockchainInteraction } from './blockchainInteraction'; +export { default as csvExport } from './csvExport'; +export { default as dataAvailability } from './dataAvailability'; +export { default as deFiDropdown } from './deFiDropdown'; +export { default as faultProofSystem } from './faultProofSystem'; +export { default as gasTracker } from './gasTracker'; +export { default as getGasButton } from './getGasButton'; +export { default as googleAnalytics } from './googleAnalytics'; +export { default as graphqlApiDocs } from './graphqlApiDocs'; +export { default as growthBook } from './growthBook'; +export { default as marketplace } from './marketplace'; +export { default as metasuites } from './metasuites'; +export { default as mixpanel } from './mixpanel'; +export { default as mudFramework } from './mudFramework'; +export { default as multichainButton } from './multichainButton'; +export { default as nameService } from './nameService'; +export { default as publicTagsSubmission } from './publicTagsSubmission'; +export { default as restApiDocs } from './restApiDocs'; +export { default as rollup } from './rollup'; +export { default as safe } from './safe'; +export { default as sentry } from './sentry'; +export { default as sol2uml } from './sol2uml'; +export { default as stats } from './stats'; +export { default as suave } from './suave'; +export { default as txInterpretation } from './txInterpretation'; +export { default as userOps } from './userOps'; +export { default as validators } from './validators'; +export { default as verifiedTokens } from './verifiedTokens'; +export { default as web3Wallet } from './web3Wallet'; diff --git a/configs/app/features/marketplace.ts b/configs/app/features/marketplace.ts new file mode 100644 index 0000000000..72f5d1ebde --- /dev/null +++ b/configs/app/features/marketplace.ts @@ -0,0 +1,77 @@ +import type { Feature } from './types'; + +import chain from '../chain'; +import { getEnvValue, getExternalAssetFilePath } from '../utils'; + +// config file will be downloaded at run-time and saved in the public folder +const enabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ENABLED'); +const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL'); +const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM'); +const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM'); +const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL'); +const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); +const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL'); +const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP'); +const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL'); +const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL'); +const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY'); +const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID'); + +const title = 'Marketplace'; + +const config: Feature<( + { configUrl: string } | + { api: { endpoint: string; basePath: string } } +) & { + submitFormUrl: string; + categoriesUrl: string | undefined; + suggestIdeasFormUrl: string | undefined; + securityReportsUrl: string | undefined; + featuredApp: string | undefined; + banner: { contentUrl: string; linkUrl: string } | undefined; + rating: { airtableApiKey: string; airtableBaseId: string } | undefined; +}> = (() => { + if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { + const props = { + submitFormUrl, + categoriesUrl, + suggestIdeasFormUrl, + securityReportsUrl, + featuredApp, + banner: bannerContentUrl && bannerLinkUrl ? { + contentUrl: bannerContentUrl, + linkUrl: bannerLinkUrl, + } : undefined, + rating: ratingAirtableApiKey && ratingAirtableBaseId ? { + airtableApiKey: ratingAirtableApiKey, + airtableBaseId: ratingAirtableBaseId, + } : undefined, + }; + + if (configUrl) { + return Object.freeze({ + title, + isEnabled: true, + configUrl, + ...props, + }); + } else if (adminServiceApiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: adminServiceApiHost, + basePath: '', + }, + ...props, + }); + } + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/metasuites.ts b/configs/app/features/metasuites.ts new file mode 100644 index 0000000000..333e7d5a8a --- /dev/null +++ b/configs/app/features/metasuites.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'MetaSuites extension'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_METASUITES_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/mixpanel.ts b/configs/app/features/mixpanel.ts new file mode 100644 index 0000000000..ef9fabd91f --- /dev/null +++ b/configs/app/features/mixpanel.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const projectToken = getEnvValue('NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN'); + +const title = 'Mixpanel analytics'; + +const config: Feature<{ projectToken: string }> = (() => { + if (projectToken) { + return Object.freeze({ + title, + isEnabled: true, + projectToken, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/mudFramework.ts b/configs/app/features/mudFramework.ts new file mode 100644 index 0000000000..86df2af34a --- /dev/null +++ b/configs/app/features/mudFramework.ts @@ -0,0 +1,22 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; +import rollup from './rollup'; + +const title = 'MUD framework'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (rollup.isEnabled && rollup.type === 'optimistic' && getEnvValue('NEXT_PUBLIC_HAS_MUD_FRAMEWORK') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/multichainButton.ts b/configs/app/features/multichainButton.ts new file mode 100644 index 0000000000..47b1433d05 --- /dev/null +++ b/configs/app/features/multichainButton.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; +import type { MultichainProviderConfig } from 'types/client/multichainProviderConfig'; + +import { getEnvValue, parseEnvJson } from '../utils'; +import marketplace from './marketplace'; + +const value = parseEnvJson(getEnvValue('NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG')); + +const title = 'Multichain balance'; + +const config: Feature<{name: string; logoUrl?: string; urlTemplate: string; dappId?: string }> = (() => { + if (value) { + return Object.freeze({ + title, + isEnabled: true, + name: value.name, + logoUrl: value.logo, + urlTemplate: value.url_template, + dappId: marketplace.isEnabled ? value.dapp_id : undefined, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/nameService.ts b/configs/app/features/nameService.ts new file mode 100644 index 0000000000..3af536bbe2 --- /dev/null +++ b/configs/app/features/nameService.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST'); + +const title = 'Name service integration'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (apiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/publicTagsSubmission.ts b/configs/app/features/publicTagsSubmission.ts new file mode 100644 index 0000000000..296d5e143a --- /dev/null +++ b/configs/app/features/publicTagsSubmission.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; + +import services from '../services'; +import { getEnvValue } from '../utils'; +import addressMetadata from './addressMetadata'; + +const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); + +const title = 'Public tag submission'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (services.reCaptcha.siteKey && addressMetadata.isEnabled && apiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/restApiDocs.ts b/configs/app/features/restApiDocs.ts new file mode 100644 index 0000000000..ae25f05c0a --- /dev/null +++ b/configs/app/features/restApiDocs.ts @@ -0,0 +1,25 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const DEFAULT_URL = `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml`; +const envValue = getEnvValue('NEXT_PUBLIC_API_SPEC_URL'); + +const title = 'REST API documentation'; + +const config: Feature<{ specUrl: string }> = (() => { + if (envValue === 'none') { + return Object.freeze({ + title, + isEnabled: false, + }); + } + + return Object.freeze({ + title, + isEnabled: true, + specUrl: envValue || DEFAULT_URL, + }); +})(); + +export default config; diff --git a/configs/app/features/rollup.ts b/configs/app/features/rollup.ts new file mode 100644 index 0000000000..2560734097 --- /dev/null +++ b/configs/app/features/rollup.ts @@ -0,0 +1,35 @@ +import type { Feature } from './types'; +import type { RollupType } from 'types/client/rollup'; +import { ROLLUP_TYPES } from 'types/client/rollup'; + +import { getEnvValue } from '../utils'; + +const type = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE'); + return ROLLUP_TYPES.find((type) => type === envValue); +})(); + +const L1BaseUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL'); +const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL'); + +const title = 'Rollup (L2) chain'; + +const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string }> = (() => { + + if (type && L1BaseUrl) { + return Object.freeze({ + title, + isEnabled: true, + type, + L1BaseUrl, + L2WithdrawalUrl, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/safe.ts b/configs/app/features/safe.ts new file mode 100644 index 0000000000..b2762a78da --- /dev/null +++ b/configs/app/features/safe.ts @@ -0,0 +1,33 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +function getApiUrl(): string | undefined { + try { + const envValue = getEnvValue('NEXT_PUBLIC_SAFE_TX_SERVICE_URL'); + return new URL('/api/v1/safes', envValue).toString(); + } catch (error) { + return; + } +} + +const title = 'Safe address tags'; + +const config: Feature<{ apiUrl: string }> = (() => { + const apiUrl = getApiUrl(); + + if (apiUrl) { + return Object.freeze({ + title, + isEnabled: true, + apiUrl, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/sentry.ts b/configs/app/features/sentry.ts new file mode 100644 index 0000000000..fb840ca9e5 --- /dev/null +++ b/configs/app/features/sentry.ts @@ -0,0 +1,44 @@ +import type { Feature } from './types'; + +import app from '../app'; +import { getEnvValue } from '../utils'; + +const dsn = getEnvValue('NEXT_PUBLIC_SENTRY_DSN'); +const instance = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_APP_INSTANCE'); + if (envValue) { + return envValue; + } + + return app.host?.replace('.blockscout.com', '').replaceAll('-', '_'); +})(); +const environment = getEnvValue('NEXT_PUBLIC_APP_ENV') || 'production'; +const release = getEnvValue('NEXT_PUBLIC_GIT_TAG'); +const title = 'Sentry error monitoring'; + +const config: Feature<{ + dsn: string; + instance: string; + release: string | undefined; + environment: string; + enableTracing: boolean; +}> = (() => { + if (dsn && instance && environment) { + return Object.freeze({ + title, + isEnabled: true, + dsn, + instance, + release, + environment, + enableTracing: getEnvValue('NEXT_PUBLIC_SENTRY_ENABLE_TRACING') === 'true', + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/sol2uml.ts b/configs/app/features/sol2uml.ts new file mode 100644 index 0000000000..5a0ac2d4be --- /dev/null +++ b/configs/app/features/sol2uml.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; + +import stripTrailingSlash from 'lib/stripTrailingSlash'; + +import { getEnvValue } from '../utils'; + +const apiEndpoint = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST'); + +const title = 'Solidity to UML diagrams'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (apiEndpoint) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiEndpoint, + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_VISUALIZE_API_BASE_PATH') || ''), + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/stats.ts b/configs/app/features/stats.ts new file mode 100644 index 0000000000..d3a90ce061 --- /dev/null +++ b/configs/app/features/stats.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; + +import stripTrailingSlash from 'lib/stripTrailingSlash'; + +import { getEnvValue } from '../utils'; + +const apiEndpoint = getEnvValue('NEXT_PUBLIC_STATS_API_HOST'); + +const title = 'Blockchain statistics'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (apiEndpoint) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiEndpoint, + basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_STATS_API_BASE_PATH') || ''), + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/suave.ts b/configs/app/features/suave.ts new file mode 100644 index 0000000000..f96b80ede4 --- /dev/null +++ b/configs/app/features/suave.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'SUAVE chain'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_IS_SUAVE_CHAIN') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/txInterpretation.ts b/configs/app/features/txInterpretation.ts new file mode 100644 index 0000000000..c22067ee27 --- /dev/null +++ b/configs/app/features/txInterpretation.ts @@ -0,0 +1,34 @@ +import type { Feature } from './types'; +import type { Provider } from 'types/client/txInterpretation'; +import { PROVIDERS } from 'types/client/txInterpretation'; + +import { getEnvValue } from '../utils'; + +const title = 'Transaction interpretation'; + +const provider: Provider = (() => { + const value = getEnvValue('NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER'); + + if (value && PROVIDERS.includes(value as Provider)) { + return value as Provider; + } + + return 'none'; +})(); + +const config: Feature<{ provider: Provider }> = (() => { + if (provider !== 'none') { + return Object.freeze({ + title, + provider, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/types.ts b/configs/app/features/types.ts new file mode 100644 index 0000000000..e0c4a26b52 --- /dev/null +++ b/configs/app/features/types.ts @@ -0,0 +1,10 @@ +type FeatureEnabled = Record> = { title: string; isEnabled: true } & Payload; +type FeatureDisabled = { title: string; isEnabled: false }; + +export type Feature = Record> = FeatureEnabled | FeatureDisabled; + +// typescript cannot properly resolve unions in nested objects - https://github.com/microsoft/TypeScript/issues/18758 +// so we use this little helper where it is needed +export const getFeaturePayload = >(feature: Feature): Payload | undefined => { + return feature.isEnabled ? feature : undefined; +}; diff --git a/configs/app/features/userOps.ts b/configs/app/features/userOps.ts new file mode 100644 index 0000000000..0e127f62f6 --- /dev/null +++ b/configs/app/features/userOps.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'User operations'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_HAS_USER_OPS') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/validators.ts b/configs/app/features/validators.ts new file mode 100644 index 0000000000..668501e28c --- /dev/null +++ b/configs/app/features/validators.ts @@ -0,0 +1,29 @@ +import type { Feature } from './types'; +import { VALIDATORS_CHAIN_TYPE } from 'types/client/validators'; +import type { ValidatorsChainType } from 'types/client/validators'; + +import { getEnvValue } from '../utils'; + +const chainType = ((): ValidatorsChainType | undefined => { + const envValue = getEnvValue('NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE') as ValidatorsChainType | undefined; + return envValue && VALIDATORS_CHAIN_TYPE.includes(envValue) ? envValue : undefined; +})(); + +const title = 'Validators list'; + +const config: Feature<{ chainType: ValidatorsChainType }> = (() => { + if (chainType) { + return Object.freeze({ + title, + isEnabled: true, + chainType, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/verifiedTokens.ts b/configs/app/features/verifiedTokens.ts new file mode 100644 index 0000000000..521851d295 --- /dev/null +++ b/configs/app/features/verifiedTokens.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const contractInfoApiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST'); + +const title = 'Verified tokens info'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (contractInfoApiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: contractInfoApiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/web3Wallet.ts b/configs/app/features/web3Wallet.ts new file mode 100644 index 0000000000..f4df4d56ea --- /dev/null +++ b/configs/app/features/web3Wallet.ts @@ -0,0 +1,43 @@ +import type { Feature } from './types'; +import { SUPPORTED_WALLETS } from 'types/client/wallets'; +import type { WalletType } from 'types/client/wallets'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +const wallets = ((): Array | undefined => { + const envValue = getEnvValue('NEXT_PUBLIC_WEB3_WALLETS'); + if (envValue === 'none') { + return; + } + + const wallets = parseEnvJson>(envValue)?.filter((type) => SUPPORTED_WALLETS.includes(type)); + + if (!wallets || wallets.length === 0) { + return [ 'metamask' ]; + } + + return wallets; +})(); + +const title = 'Web3 wallet integration (add token or network to the wallet)'; + +const config: Feature<{ wallets: Array; addToken: { isDisabled: boolean }}> = (() => { + if (wallets && wallets.length > 0) { + return Object.freeze({ + title, + isEnabled: true, + wallets, + addToken: { + isDisabled: getEnvValue('NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET') === 'true', + }, + addNetwork: {}, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/index.ts b/configs/app/index.ts new file mode 100644 index 0000000000..a946604593 --- /dev/null +++ b/configs/app/index.ts @@ -0,0 +1,19 @@ +import api from './api'; +import app from './app'; +import chain from './chain'; +import * as features from './features'; +import meta from './meta'; +import services from './services'; +import UI from './ui'; + +const config = Object.freeze({ + app, + chain, + api, + UI, + features, + services, + meta, +}); + +export default config; diff --git a/configs/app/meta.ts b/configs/app/meta.ts new file mode 100644 index 0000000000..3d7b777e03 --- /dev/null +++ b/configs/app/meta.ts @@ -0,0 +1,18 @@ +import app from './app'; +import { getEnvValue, getExternalAssetFilePath } from './utils'; + +const defaultImageUrl = '/static/og_placeholder.png'; + +const meta = Object.freeze({ + promoteBlockscoutInTitle: getEnvValue('NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE') === 'false' ? false : true, + og: { + description: getEnvValue('NEXT_PUBLIC_OG_DESCRIPTION') || '', + imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl), + enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true', + }, + seo: { + enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED') === 'true', + }, +}); + +export default meta; diff --git a/configs/app/services.ts b/configs/app/services.ts new file mode 100644 index 0000000000..86df538215 --- /dev/null +++ b/configs/app/services.ts @@ -0,0 +1,7 @@ +import { getEnvValue } from './utils'; + +export default Object.freeze({ + reCaptcha: { + siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'), + }, +}); diff --git a/configs/app/ui.ts b/configs/app/ui.ts new file mode 100644 index 0000000000..73308ede5b --- /dev/null +++ b/configs/app/ui.ts @@ -0,0 +1,93 @@ +import type { ContractCodeIde } from 'types/client/contract'; +import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation'; +import type { ChainIndicatorId } from 'types/homepage'; +import type { NetworkExplorer } from 'types/networks'; +import type { ColorThemeId } from 'types/settings'; + +import { COLOR_THEMES } from 'lib/settings/colorTheme'; + +import * as views from './ui/views'; +import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils'; + +const hiddenLinks = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS')) || []; + + if (!Array.isArray(parsedValue)) { + return undefined; + } + + const result = NAVIGATION_LINK_IDS.reduce((result, item) => { + result[item] = parsedValue.includes(item); + return result; + }, {} as Record); + + return result; +})(); + +const highlightedRoutes = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES')); + return Array.isArray(parsedValue) ? parsedValue : []; +})(); + +const defaultColorTheme = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined; + return COLOR_THEMES.find((theme) => theme.id === envValue); +})(); + +// eslint-disable-next-line max-len +const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; + +const UI = Object.freeze({ + navigation: { + logo: { + 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO'), + dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO_DARK'), + }, + icon: { + 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON'), + dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'), + }, + hiddenLinks, + highlightedRoutes, + otherLinks: parseEnvJson>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [], + featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'), + layout: (getEnvValue('NEXT_PUBLIC_NAVIGATION_LAYOUT') || 'vertical') as NavigationLayout, + }, + footer: { + links: getExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS'), + frontendVersion: getEnvValue('NEXT_PUBLIC_GIT_TAG'), + frontendCommit: getEnvValue('NEXT_PUBLIC_GIT_COMMIT_SHA'), + }, + homepage: { + charts: parseEnvJson>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [], + plate: { + background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT, + textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white', + }, + showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true, + }, + views, + indexingAlert: { + blocks: { + isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS') === 'true' ? true : false, + }, + intTxs: { + isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS') === 'true' ? true : false, + }, + }, + maintenanceAlert: { + message: getEnvValue('NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE'), + }, + explorers: { + items: parseEnvJson>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [], + }, + ides: { + items: parseEnvJson>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [], + }, + hasContractAuditReports: getEnvValue('NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS') === 'true' ? true : false, + colorTheme: { + 'default': defaultColorTheme, + }, +}); + +export default UI; diff --git a/configs/app/ui/views/address.ts b/configs/app/ui/views/address.ts new file mode 100644 index 0000000000..088d288deb --- /dev/null +++ b/configs/app/ui/views/address.ts @@ -0,0 +1,51 @@ +import type { SmartContractVerificationMethodExtra } from 'types/client/contract'; +import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS } from 'types/client/contract'; +import type { AddressViewId, IdenticonType } from 'types/views/address'; +import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address'; + +import { getEnvValue, parseEnvJson } from 'configs/app/utils'; + +const identiconType: IdenticonType = (() => { + const value = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE'); + + return IDENTICON_TYPES.find((type) => value === type) || 'jazzicon'; +})(); + +const hiddenViews = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS')) || []; + + if (!Array.isArray(parsedValue)) { + return undefined; + } + + const result = ADDRESS_VIEWS_IDS.reduce((result, item) => { + result[item] = parsedValue.includes(item); + return result; + }, {} as Record); + + return result; +})(); + +const extraVerificationMethods: Array = (() => { + const envValue = getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS'); + if (envValue === 'none') { + return []; + } + + if (!envValue) { + return SMART_CONTRACT_EXTRA_VERIFICATION_METHODS; + } + + const parsedMethods = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS')) || []; + + return SMART_CONTRACT_EXTRA_VERIFICATION_METHODS.filter((method) => parsedMethods.includes(method)); +})(); + +const config = Object.freeze({ + identiconType, + hiddenViews, + solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true', + extraVerificationMethods, +}); + +export default config; diff --git a/configs/app/ui/views/block.ts b/configs/app/ui/views/block.ts new file mode 100644 index 0000000000..5e42136a42 --- /dev/null +++ b/configs/app/ui/views/block.ts @@ -0,0 +1,25 @@ +import type { BlockFieldId } from 'types/views/block'; +import { BLOCK_FIELDS_IDS } from 'types/views/block'; + +import { getEnvValue, parseEnvJson } from 'configs/app/utils'; + +const blockHiddenFields = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS')) || []; + + if (!Array.isArray(parsedValue)) { + return undefined; + } + + const result = BLOCK_FIELDS_IDS.reduce((result, item) => { + result[item] = parsedValue.includes(item); + return result; + }, {} as Record); + + return result; +})(); + +const config = Object.freeze({ + hiddenFields: blockHiddenFields, +}); + +export default config; diff --git a/configs/app/ui/views/index.ts b/configs/app/ui/views/index.ts new file mode 100644 index 0000000000..4f1d77073b --- /dev/null +++ b/configs/app/ui/views/index.ts @@ -0,0 +1,4 @@ +export { default as address } from './address'; +export { default as block } from './block'; +export { default as nft } from './nft'; +export { default as tx } from './tx'; diff --git a/configs/app/ui/views/nft.ts b/configs/app/ui/views/nft.ts new file mode 100644 index 0000000000..b0d9f9b28c --- /dev/null +++ b/configs/app/ui/views/nft.ts @@ -0,0 +1,9 @@ +import type { NftMarketplaceItem } from 'types/views/nft'; + +import { getEnvValue, parseEnvJson } from 'configs/app/utils'; + +const config = Object.freeze({ + marketplaces: parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES')) || [], +}); + +export default config; diff --git a/configs/app/ui/views/tx.ts b/configs/app/ui/views/tx.ts new file mode 100644 index 0000000000..f725363504 --- /dev/null +++ b/configs/app/ui/views/tx.ts @@ -0,0 +1,41 @@ +import type { TxAdditionalFieldsId, TxFieldsId } from 'types/views/tx'; +import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from 'types/views/tx'; + +import { getEnvValue, parseEnvJson } from 'configs/app/utils'; + +const hiddenFields = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS')) || []; + + if (!Array.isArray(parsedValue)) { + return undefined; + } + + const result = TX_FIELDS_IDS.reduce((result, item) => { + result[item] = parsedValue.includes(item); + return result; + }, {} as Record); + + return result; +})(); + +const additionalFields = (() => { + const parsedValue = parseEnvJson>(getEnvValue('NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS')) || []; + + if (!Array.isArray(parsedValue)) { + return undefined; + } + + const result = TX_ADDITIONAL_FIELDS_IDS.reduce((result, item) => { + result[item] = parsedValue.includes(item); + return result; + }, {} as Record); + + return result; +})(); + +const config = Object.freeze({ + hiddenFields, + additionalFields, +}); + +export default config; diff --git a/configs/app/utils.ts b/configs/app/utils.ts new file mode 100644 index 0000000000..97a9afd05d --- /dev/null +++ b/configs/app/utils.ts @@ -0,0 +1,48 @@ +import isBrowser from 'lib/isBrowser'; +import * as regexp from 'lib/regexp'; + +export const replaceQuotes = (value: string | undefined) => value?.replaceAll('\'', '"'); + +export const getEnvValue = (envName: string) => { + // eslint-disable-next-line no-restricted-properties + const envs = isBrowser() ? window.__envs : process.env; + + if (isBrowser() && envs.NEXT_PUBLIC_APP_INSTANCE === 'pw') { + const storageValue = localStorage.getItem(envName); + + if (typeof storageValue === 'string') { + return storageValue; + } + } + + return replaceQuotes(envs[envName]); +}; + +export const parseEnvJson = (env: string | undefined): DataType | null => { + try { + return JSON.parse(env || 'null') as DataType | null; + } catch (error) { + return null; + } +}; + +export const getExternalAssetFilePath = (envName: string) => { + const parsedValue = getEnvValue(envName); + + if (!parsedValue) { + return; + } + + return buildExternalAssetFilePath(envName, parsedValue); +}; + +export const buildExternalAssetFilePath = (name: string, value: string) => { + try { + const fileName = name.replace(/^NEXT_PUBLIC_/, '').replace(/_URL$/, '').toLowerCase(); + const url = new URL(value); + const fileExtension = url.pathname.match(regexp.FILE_EXTENSION)?.[1]; + return `/assets/configs/${ fileName }.${ fileExtension }`; + } catch (error) { + return; + } +}; diff --git a/configs/envs/.env.arbitrum b/configs/envs/.env.arbitrum new file mode 100644 index 0000000000..a1ec2fef59 --- /dev/null +++ b/configs/envs/.env.arbitrum @@ -0,0 +1,44 @@ +# Set of ENVs for Arbitrum One network explorer +# https://arbitrum.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=arbitrum" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=arbitrum.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x37c798810d49ba132b40efe7f4fdf6806a8fc58226bb5e185ddc91f896577abf +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(27, 74, 221, 1) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-arbitrum.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/arbitrum/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-one-icon-light.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-one-icon-dark.svg +NEXT_PUBLIC_NETWORK_ID=42161 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-one-logo-light.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-one-logo-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Arbitrum One +NEXT_PUBLIC_NETWORK_RPC_URL=https://arbitrum.llamarpc.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum One +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-one.png +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com +NEXT_PUBLIC_ROLLUP_TYPE=arbitrum +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.base b/configs/envs/.env.base new file mode 100644 index 0000000000..2c9cdf6699 --- /dev/null +++ b/configs/envs/.env.base @@ -0,0 +1,66 @@ +# Set of ENVs for Base Mainnet network explorer +# https://base.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=base" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "728301", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "728302", "width": "320", "height": "100" } +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=base.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'aerodrome'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/base-mainnet.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xfd5c5dae7b69fe29e61d19b9943e688aa0f1be1e983c4fba8fe985f90ff69d5f +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://basechain.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/0d18fc309ff499075127b364cc69306d/raw/2a51f961a8c1b9f884f2ab7eed79d4b69330e1ae/peanut_protocol_banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://base.blockscout.com/apps/peanut-protocol +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/base/pools'}},{'title':'L2scan','baseUrl':'https://base.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/base'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/base/transaction','address':'/base/address'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg +NEXT_PUBLIC_NETWORK_ID=8453 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg +NEXT_PUBLIC_NETWORK_NAME=Base Mainnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.base.org/ +NEXT_PUBLIC_NETWORK_SHORT_NAME=Mainnet +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://base.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.base.org/withdraw +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-base.safe.global +NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-base-mainnet.k8s-prod-1.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.celo_alfajores b/configs/envs/.env.celo_alfajores new file mode 100644 index 0000000000..2cbb790629 --- /dev/null +++ b/configs/envs/.env.celo_alfajores @@ -0,0 +1,39 @@ +# Set of ENVs for Celo Alfajores network explorer +# https://celo-alfajores.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=celo_alfajores" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=celo-alfajores.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_GAS_TRACKER_ENABLED=false +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(252, 255, 82, 1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(0, 0, 0, 1) +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=CELO +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=CELO +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-light.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-dark.svg +NEXT_PUBLIC_NETWORK_ID=44787 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-light.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Celo Alfajores +NEXT_PUBLIC_NETWORK_RPC_URL=https://alfajores-forno.celo-testnet.org +NEXT_PUBLIC_NETWORK_SHORT_NAME=Alfajores +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/celo.png +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth new file mode 100644 index 0000000000..1c1524cc72 --- /dev/null +++ b/configs/envs/.env.eth @@ -0,0 +1,64 @@ +# Set of ENVs for Ethereum network explorer +# https://eth.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=eth" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "728471", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "728470", "width": "320", "height": "100" } +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=eth.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/eth-mainnet.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xd01175f1efa23f36c5579b3c13e2bbd0885017643a7efef5cbcb6b474384dfa8 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap'] +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://ethereum-mainnet.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=gearbox-protocol +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/eth/pools'}},{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'blockchair','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/blockchair.png','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'sentio','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/sentio.png','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/0xPPL.png','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ] +NEXT_PUBLIC_NETWORK_ID=1 +NEXT_PUBLIC_NETWORK_NAME=Ethereum +NEXT_PUBLIC_NETWORK_RPC_URL=https://eth.llamarpc.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Ethereum +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/eth.jpg +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://eth.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global +NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s-prod-1.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com diff --git a/configs/envs/.env.eth_goerli b/configs/envs/.env.eth_goerli new file mode 100644 index 0000000000..5bf4167f20 --- /dev/null +++ b/configs/envs/.env.eth_goerli @@ -0,0 +1,59 @@ +# Set of ENVs for Goerli testnet network explorer +# https://eth-goerli.blockscout.com/ + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=Goerli +NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli +NEXT_PUBLIC_NETWORK_ID=5 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli +NEXT_PUBLIC_IS_TESTNET=true + +# api configuration +NEXT_PUBLIC_API_HOST=eth-goerli.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg +## footer +##views +NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}] +## misc +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] +NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true + +# app features +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C +NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout + +#meta +NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia new file mode 100644 index 0000000000..1304b37c72 --- /dev/null +++ b/configs/envs/.env.eth_sepolia @@ -0,0 +1,65 @@ +# Set of ENVs for Sepolia network explorer +# https://eth-sepolia.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=eth_sepolia" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" } +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(51, 53, 67, 1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(165, 252, 122, 1) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/sepolia-testnet/pools'}},{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png','baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}} ] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png +NEXT_PUBLIC_NETWORK_ID=11155111 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg +NEXT_PUBLIC_NETWORK_NAME=Sepolia +NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=Sepolia +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global +NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true +NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.garnet b/configs/envs/.env.garnet new file mode 100644 index 0000000000..a6763934bc --- /dev/null +++ b/configs/envs/.env.garnet @@ -0,0 +1,50 @@ +# Set of ENVs for Garnet (dev only) +# https://https://explorer.garnetchain.com// + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME="Garnet Testnet" +NEXT_PUBLIC_NETWORK_ID=17069 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_RPC_URL=https://partner-rpc.garnetchain.com/tireless-strand-dreamt-overcome + +# api configuration +NEXT_PUBLIC_API_HOST=explorer.garnetchain.com +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +## views +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +# app features +NEXT_PUBLIC_APP_INSTANCE=local +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/redstone-testnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/redstone.json +NEXT_PUBLIC_AD_BANNER_PROVIDER=none +## sidebar +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet.svg +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/garnet-dark.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/garnet-dark.svg +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(169, 31, 47) +NEXT_PUBLIC_OG_DESCRIPTION="Redstone is the home for onchain games, worlds, and other MUD applications" +# rollup +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-holesky.blockscout.com/ +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://garnet.qry.live/withdraw +NEXT_PUBLIC_HAS_MUD_FRAMEWORK=true \ No newline at end of file diff --git a/configs/envs/.env.gnosis b/configs/envs/.env.gnosis new file mode 100644 index 0000000000..fad2dde745 --- /dev/null +++ b/configs/envs/.env.gnosis @@ -0,0 +1,69 @@ +# Set of ENVs for Gnosis chain network explorer +# https://gnosis.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=gnosis" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'523705','width':'728','height':'90'} +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'539876','width':'320','height':'100'} +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=gnosis.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL=GNO +NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'},{'type':'amb','title':'Arbitrary Message Bridge','short_title':'AMB'}] +NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://eth.blockscout.com/token/'},{'id':'56','title':'Binance Smart Chain','short_title':'BSC','base_url':'https://bscscan.com/token/'},{'id':'99','title':'POA','short_title':'POA','base_url':'https://blockscout.com/poa/core/token/'}] +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/gnosis-chain-mainnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/gnosis.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x082762f95047d39d612daafec832f88163f3815fde4ddd8944f2a5198a396e0f +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'tvl'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(46, 74, 60) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://login.blockscout.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/0d18fc309ff499075127b364cc69306d/raw/2a51f961a8c1b9f884f2ab7eed79d4b69330e1ae/peanut_protocol_banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://gnosis.blockscout.com/apps/peanut-protocol +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=XDAI +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=XDAI +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/xdai/pools'}},{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/gnosis-chain'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/gnosis-chain/transaction','address':'/gnosis-chain/address'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/gnosis.svg +NEXT_PUBLIC_NETWORK_ID=100 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/gnosis.svg +NEXT_PUBLIC_NETWORK_NAME=Gnosis chain +NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO +NEXT_PUBLIC_NETWORK_SHORT_NAME=Gnosis chain +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/gnosis-chain-mainnet.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://gnosis.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-gnosis-chain.safe.global +NEXT_PUBLIC_STATS_API_HOST=https://stats-gnosis-mainnet.k8s-prod-1.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.jest b/configs/envs/.env.jest new file mode 100644 index 0000000000..e1f80c7e75 --- /dev/null +++ b/configs/envs/.env.jest @@ -0,0 +1,53 @@ +# Set of ENVs for Jest unit tests + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=Blockscout +NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout +NEXT_PUBLIC_NETWORK_ID=1 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://localhost:1111 +NEXT_PUBLIC_IS_TESTNET=true + +# api configuration +NEXT_PUBLIC_API_HOST=localhost +NEXT_PUBLIC_API_PORT=3003 +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] +NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND= +## sidebar +NEXT_PUBLIC_NETWORK_LOGO= +NEXT_PUBLIC_NETWORK_LOGO_DARK= +NEXT_PUBLIC_NETWORK_ICON= +NEXT_PUBLIC_NETWORK_ICON_DARK= +NEXT_PUBLIC_FEATURED_NETWORKS= +## footer +NEXT_PUBLIC_FOOTER_LINKS= +NEXT_PUBLIC_GIT_TAG=v1.0.11 +## misc +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] + +# app features +NEXT_PUBLIC_APP_INSTANCE=jest +NEXT_PUBLIC_APP_ENV=testing +NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3100 +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx +NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx diff --git a/configs/envs/.env.localhost b/configs/envs/.env.localhost new file mode 100644 index 0000000000..3956c0d11c --- /dev/null +++ b/configs/envs/.env.localhost @@ -0,0 +1,39 @@ +# Set of ENVs for local network explorer +# frontend app URL - https://localhost:3000/ +# API URL - https://localhost:3001/ + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=POA +NEXT_PUBLIC_NETWORK_SHORT_NAME=POA +NEXT_PUBLIC_NETWORK_ID=99 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network + +# api configuration +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=localhost +NEXT_PUBLIC_API_PROTOCOL=http +NEXT_PUBLIC_API_PORT=3001 + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] +## sidebar +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json +## footer +## misc +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}] + +# app features +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout diff --git a/configs/envs/.env.main b/configs/envs/.env.main new file mode 100644 index 0000000000..d33f7ec621 --- /dev/null +++ b/configs/envs/.env.main @@ -0,0 +1,68 @@ +# Set of ENVs for Sepolia network explorer +# https://eth-sepolia.k8s-dev.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=main" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" } +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.k8s-dev.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(51, 53, 67, 1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(165, 252, 122, 1) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/test-configs/marketplace-security-report-mock.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps'] +NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png', 'baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}} ] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png +NEXT_PUBLIC_NETWORK_ID=11155111 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg +NEXT_PUBLIC_NETWORK_NAME=Sepolia +NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=Sepolia +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global +NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer-test.k8s-dev.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.optimism b/configs/envs/.env.optimism new file mode 100644 index 0000000000..f23300e95f --- /dev/null +++ b/configs/envs/.env.optimism @@ -0,0 +1,69 @@ +# Set of ENVs for OP Mainnet network explorer +# https://optimism.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "749780", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "749779", "width": "320", "height": "100" } +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=optimism.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'velodrome'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] +NEXT_PUBLIC_FAULT_PROOF_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97f34a4cf685e365460dd38dbe16e092d8e4cc4b6ac779e3abcf4c18df6b1329 +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap', 'secondary_coin_price'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://optimism-goerli.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/0d18fc309ff499075127b364cc69306d/raw/2a51f961a8c1b9f884f2ab7eed79d4b69330e1ae/peanut_protocol_banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://optimism.blockscout.com/apps/peanut-protocol +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/optimism/pools'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ID=10 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_NAME=OP Mainnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Mainnet +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-optimism.safe.global +NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-mainnet.k8s-prod-1.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask'] \ No newline at end of file diff --git a/configs/envs/.env.optimism_sepolia b/configs/envs/.env.optimism_sepolia new file mode 100644 index 0000000000..8636924480 --- /dev/null +++ b/configs/envs/.env.optimism_sepolia @@ -0,0 +1,54 @@ +# Set of ENVs for OP Sepolia network explorer +# https://optimism-sepolia.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism_sepolia" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=optimism-sepolia.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_FAULT_PROOF_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-sepolia.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x59d26836041ab35169bdce431d68d070b7b8acb589fa52e126e6c828b6ece5e9 +NEXT_PUBLIC_HAS_USER_OPS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://optimism-goerli.us.auth0.com/v2/logout +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=

Build faster with the Superchain Dev Console: Get testnet ETH and tools to help you build, launch, and grow your app on the Superchain

+NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic-sepolia'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg +NEXT_PUBLIC_NETWORK_ID=11155420 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg +NEXT_PUBLIC_NETWORK_NAME=OP Sepolia +NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia.optimism.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Sepolia +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/ +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-sepolia.k8s.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask'] \ No newline at end of file diff --git a/configs/envs/.env.polygon b/configs/envs/.env.polygon new file mode 100644 index 0000000000..4fe0631ac0 --- /dev/null +++ b/configs/envs/.env.polygon @@ -0,0 +1,47 @@ +# Set of ENVs for Polygon Mainnet network explorer +# https://polygon.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=polygon" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" } +NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=polygon.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9 +NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1) +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=MATIC +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=MATIC +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/polygon_pos/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg +NEXT_PUBLIC_NETWORK_ID=137 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg +NEXT_PUBLIC_NETWORK_NAME=Polygon Mainnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://polygon.blockpi.network/v1/rpc/public +NEXT_PUBLIC_NETWORK_SHORT_NAME=Polygon +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/polygon-mainnet.png +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-polygon.safe.global +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask'] \ No newline at end of file diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw new file mode 100644 index 0000000000..7840c51b42 --- /dev/null +++ b/configs/envs/.env.pw @@ -0,0 +1,56 @@ +# Set of ENVs for Playwright components tests + +# app configuration +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3100 + +# blockchain parameters +NEXT_PUBLIC_NETWORK_NAME=Blockscout +NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout +NEXT_PUBLIC_NETWORK_ID=1 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_RPC_URL=https://localhost:1111 +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation + +# api configuration +NEXT_PUBLIC_API_PROTOCOL=http +NEXT_PUBLIC_API_HOST=localhost +NEXT_PUBLIC_API_PORT=3003 +NEXT_PUBLIC_API_BASE_PATH=/ + +# ui config +## homepage +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] +NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true +## sidebar +## footer +NEXT_PUBLIC_GIT_TAG=v1.0.11 +## views +NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'}] +## misc +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/%23address={hash}&blockscout=eth-goerli.blockscout.com'}] +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE= + +# app features +NEXT_PUBLIC_APP_ENV=testing +NEXT_PUBLIC_APP_INSTANCE=pw +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://localhost:3000/marketplace-suggest-ideas-form +NEXT_PUBLIC_AD_BANNER_PROVIDER=slise +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_AUTH_URL=http://localhost:3100 +NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout +NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx +NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004 +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx diff --git a/configs/envs/.env.rootstock_testnet b/configs/envs/.env.rootstock_testnet new file mode 100644 index 0000000000..2c3f426912 --- /dev/null +++ b/configs/envs/.env.rootstock_testnet @@ -0,0 +1,45 @@ +# Set of ENVs for Rootstock Testnet network explorer +# https://rootstock-testnet.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=rootstock_testnet" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=rootstock-testnet.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-testnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/rootstock.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x98b25020fa6551a439dfee58fb16ca11d9e93d4cdf15f3f07b697cf08cf11643 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgb(255, 145, 0) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255, 255, 255) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://rootstock.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=tRBTC +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=tRBTC +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/rsk-testnet'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/rootstock-short.svg +NEXT_PUBLIC_NETWORK_ID=31 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/rootstock.svg +NEXT_PUBLIC_NETWORK_NAME=Rootstock Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://public-node.testnet.rsk.co +NEXT_PUBLIC_NETWORK_SHORT_NAME=Rootstock +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/rootstock-testnet.png +NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward','nonce'] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.stability_testnet b/configs/envs/.env.stability_testnet new file mode 100644 index 0000000000..c057fda27c --- /dev/null +++ b/configs/envs/.env.stability_testnet @@ -0,0 +1,55 @@ +# Set of ENVs for Stability Testnet network explorer +# https://stability-testnet.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=stability_testnet" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=stability-testnet.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com/ +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/stability-testnet.json +NEXT_PUBLIC_GAS_TRACKER_ENABLED=false +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x38125475465a4113a216448af2c9570d0e2c25ef313f8cfbef74f1daad7a97b5 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(46, 51, 81, 1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(122, 235, 246, 1) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_LOGOUT_URL=https://blockscout-stability.us.auth0.com/v2/logout +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=FREE +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=FREE +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/stability-short-dark.svg +NEXT_PUBLIC_NETWORK_ID=20180427 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/stability-dark.svg +NEXT_PUBLIC_NETWORK_NAME=Stability Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://free.testnet.stabilityprotocol.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Stability Testnet +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/stability.png +NEXT_PUBLIC_STATS_API_HOST=https://stats-stability-testnet.k8s.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability +NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts'] +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward'] +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas'] +NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','gas_fees','burnt_fees'] +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/configs/envs/.env.zkevm b/configs/envs/.env.zkevm new file mode 100644 index 0000000000..8b7dc2e566 --- /dev/null +++ b/configs/envs/.env.zkevm @@ -0,0 +1,48 @@ +# Set of ENVs for Polygon zkEVM network explorer +# https://zkevm.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=zkevm" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" } +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" } +NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=zkevm.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zkevm.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x25fcb396fc8652dcd0040f677a1dcc6fecff390ecafc815894379a3f254f1aa9 +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1) +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=MATIC +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=MATIC +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/polygon-zkevm/pools'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg +NEXT_PUBLIC_NETWORK_ID=1101 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg +NEXT_PUBLIC_NETWORK_NAME=Polygon zkEVM +NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=zkEVM +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_ROLLUP_TYPE=zkEvm +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-zkevm.safe.global +NEXT_PUBLIC_STATS_API_HOST=https://stats-polygon-zkevm.k8s-prod-1.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask'] \ No newline at end of file diff --git a/configs/envs/.env.zksync b/configs/envs/.env.zksync new file mode 100644 index 0000000000..56e104488b --- /dev/null +++ b/configs/envs/.env.zksync @@ -0,0 +1,50 @@ +# Set of ENVs for ZkSync Era network explorer +# https://zksync.blockscout.com +# This is an auto-generated file. To update all values, run "yarn preset:sync --name=zksync" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=zksync.blockscout.com +NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zksync.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/zksync.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x79c7802ccdf3be5a49c47cc751aad351b0027e8275f6f54878eda50ee559a648 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(53, 103, 246, 1) +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(255, 255, 255, 1) +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://zksync.us.auth0.com/v2/logout +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/zksync/pools'}},{'title':'L2scan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/zksync.png','baseUrl':'https://zksync-era.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zksync-short-dark.svg +NEXT_PUBLIC_NETWORK_ID=324 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zksync-dark.svg +NEXT_PUBLIC_NETWORK_NAME=ZkSync Era +NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.era.zksync.io +NEXT_PUBLIC_NETWORK_SHORT_NAME=ZkSync Era +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zksync.png +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://zksync.drpc.org?ref=559183','text':'Public RPC'}] +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/ +NEXT_PUBLIC_ROLLUP_TYPE=zkSync +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-zksync.safe.global +NEXT_PUBLIC_STATS_API_HOST=https://stats-zksync-era-mainnet.k8s-prod-2.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/decs.d.ts b/decs.d.ts new file mode 100644 index 0000000000..434d031e55 --- /dev/null +++ b/decs.d.ts @@ -0,0 +1 @@ +declare module 'react-identicons' diff --git a/deploy/helmfile.yaml b/deploy/helmfile.yaml new file mode 100644 index 0000000000..bad95f0a82 --- /dev/null +++ b/deploy/helmfile.yaml @@ -0,0 +1,68 @@ +environments: + {{ .Environment.Name }}: +--- +helmDefaults: + timeout: 600 + kubeContext: k8s-dev + wait: true + recreatePods: false + +repositories: + - name: blockscout-ci-cd + url: https://blockscout.github.io/blockscout-ci-cd + - name: blockscout + url: https://blockscout.github.io/helm-charts + - name: bedag + url: https://bedag.github.io/helm-charts + +releases: + # Deploy review L2 + - name: reg-secret + chart: bedag/raw + namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + labels: + app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + values: + - resources: + - apiVersion: v1 + data: + .dockerconfigjson: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/dockerRegistryCreds + kind: Secret + metadata: + name: regcred + type: kubernetes.io/dockerconfigjson + - name: bs-stack + chart: blockscout/blockscout-stack + version: 1.2.* + namespace: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + labels: + app: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + values: + - values/review-l2/values.yaml.gotmpl + - global: + env: "review" + # Deploy review + - name: reg-secret + chart: bedag/raw + namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + labels: + app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + values: + - resources: + - apiVersion: v1 + data: + .dockerconfigjson: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/dockerRegistryCreds + kind: Secret + metadata: + name: regcred + type: kubernetes.io/dockerconfigjson + - name: bs-stack + chart: blockscout/blockscout-stack + version: 1.2.* + namespace: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + labels: + app: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + values: + - values/review/values.yaml.gotmpl + - global: + env: "review" \ No newline at end of file diff --git a/deploy/scripts/collect_envs.sh b/deploy/scripts/collect_envs.sh new file mode 100755 index 0000000000..d53dc989e4 --- /dev/null +++ b/deploy/scripts/collect_envs.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Check if the number of arguments provided is correct +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +input_file="$1" +prefix="NEXT_PUBLIC_" + +# Function to make the environment variables registry file based on documentation file ENVS.md +# It will read the input file, extract all prefixed string and use them as variables names +# This variables will have dummy values assigned to them +make_registry_file() { + output_file=".env.registry" + + # Check if file already exists and empty its content if it does + if [ -f "$output_file" ]; then + > "$output_file" + fi + + grep -oE "${prefix}[[:alnum:]_]+" "$input_file" | sort -u | while IFS= read -r var_name; do + echo "$var_name=__" >> "$output_file" + done +} + +# Function to save build-time environment variables to .env file +save_build-time_envs() { + output_file=".env" + + # Check if file already exists and empty its content if it does or create a new one + if [ -f "$output_file" ]; then + > "$output_file" + else + touch "$output_file" + fi + + env | grep "^${prefix}" | while IFS= read -r line; do + echo "$line" >> "$output_file" + done +} + +make_registry_file +save_build-time_envs \ No newline at end of file diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh new file mode 100755 index 0000000000..747b7269c9 --- /dev/null +++ b/deploy/scripts/download_assets.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +echo +echo "⬇️ Downloading external assets..." + +# Check if the number of arguments provided is correct +if [ "$#" -ne 1 ]; then + echo "🛑 Error: incorrect amount of arguments. Usage: $0 ." + exit 1 +fi + +# Define the directory to save the downloaded assets +ASSETS_DIR="$1" + +# Define a list of environment variables containing URLs of external assets +ASSETS_ENVS=( + "NEXT_PUBLIC_MARKETPLACE_CONFIG_URL" + "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" + "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL" + "NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL" + "NEXT_PUBLIC_FEATURED_NETWORKS" + "NEXT_PUBLIC_FOOTER_LINKS" + "NEXT_PUBLIC_NETWORK_LOGO" + "NEXT_PUBLIC_NETWORK_LOGO_DARK" + "NEXT_PUBLIC_NETWORK_ICON" + "NEXT_PUBLIC_NETWORK_ICON_DARK" + "NEXT_PUBLIC_OG_IMAGE_URL" +) + +# Create the assets directory if it doesn't exist +mkdir -p "$ASSETS_DIR" + +# Function to determine the target file name based on the environment variable +get_target_filename() { + local env_var="$1" + local url="${!env_var}" + + # Extract the middle part of the variable name (between "NEXT_PUBLIC_" and "_URL") in lowercase + local name_prefix="${env_var#NEXT_PUBLIC_}" + local name_suffix="${name_prefix%_URL}" + local name_lc="$(echo "$name_suffix" | tr '[:upper:]' '[:lower:]')" + + # Check if the URL starts with "file://" + if [[ "$url" == file://* ]]; then + # Extract the local file path + local file_path="${url#file://}" + # Get the filename from the local file path + local filename=$(basename "$file_path") + # Extract the extension from the filename + local extension="${filename##*.}" + else + # Remove query parameters from the URL and get the filename + local filename=$(basename "${url%%\?*}") + # Extract the extension from the filename + local extension="${filename##*.}" + fi + + # Convert the extension to lowercase + extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]') + + # Construct the custom file name + echo "$name_lc.$extension" +} + +# Function to download and save an asset +download_and_save_asset() { + local env_var="$1" + local url="$2" + local filename="$3" + local destination="$ASSETS_DIR/$filename" + + # Check if the environment variable is set + if [ -z "${!env_var}" ]; then + echo " [.] $env_var: Variable is not set. Skipping download." + return 1 + fi + + # Check if the URL starts with "file://" + if [[ "$url" == file://* ]]; then + # Copy the local file to the destination + cp "${url#file://}" "$destination" + else + # Download the asset using curl + curl -s -o "$destination" "$url" + fi + + # Check if the download was successful + if [ $? -eq 0 ]; then + echo " [+] $env_var: Successfully saved file from $url to $destination." + return 0 + else + echo " [-] $env_var: Failed to save file from $url." + return 1 + fi +} + +# Iterate through the list and download assets +for env_var in "${ASSETS_ENVS[@]}"; do + url="${!env_var}" + filename=$(get_target_filename "$env_var") + download_and_save_asset "$env_var" "$url" "$filename" +done + +echo "✅ Done." +echo diff --git a/deploy/scripts/entrypoint.sh b/deploy/scripts/entrypoint.sh new file mode 100755 index 0000000000..298303c7a0 --- /dev/null +++ b/deploy/scripts/entrypoint.sh @@ -0,0 +1,63 @@ +#!/bin/bash + + +export_envs_from_preset() { + if [ -z "$ENVS_PRESET" ]; then + return + fi + + if [ "$ENVS_PRESET" = "none" ]; then + return + fi + + local preset_file="./configs/envs/.env.$ENVS_PRESET" + + if [ ! -f "$preset_file" ]; then + return + fi + + local blacklist=( + "NEXT_PUBLIC_APP_PROTOCOL" + "NEXT_PUBLIC_APP_HOST" + "NEXT_PUBLIC_APP_PORT" + "NEXT_PUBLIC_APP_ENV" + "NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL" + ) + + while IFS='=' read -r name value; do + name="${name#"${name%%[![:space:]]*}"}" # Trim leading whitespace + if [[ -n $name && $name == "NEXT_PUBLIC_"* && ! "${blacklist[*]}" =~ "$name" ]]; then + export "$name"="$value" + fi + done < <(grep "^[^#;]" "$preset_file") +} + +# If there is a preset, load the environment variables from the its file +export_envs_from_preset + +# Download external assets +./download_assets.sh ./public/assets/configs + +# Check run-time ENVs values +./validate_envs.sh +if [ $? -ne 0 ]; then + exit 1 +fi + +# Generate favicons bundle +./favicon_generator.sh +if [ $? -ne 0 ]; then + echo "👎 Unable to generate favicons bundle." +else + echo "👍 Favicons bundle successfully generated." +fi +echo + +# Create envs.js file with run-time environment variables for the client app +./make_envs_script.sh + +# Print list of enabled features +node ./feature-reporter.js + +echo "Starting Next.js application" +exec "$@" \ No newline at end of file diff --git a/deploy/scripts/favicon_generator.sh b/deploy/scripts/favicon_generator.sh new file mode 100755 index 0000000000..0cfc9f7f8a --- /dev/null +++ b/deploy/scripts/favicon_generator.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +master_url="${FAVICON_MASTER_URL:-$NEXT_PUBLIC_NETWORK_ICON}" +export MASTER_URL="$master_url" + +cd ./deploy/tools/favicon-generator +./script.sh +if [ $? -ne 0 ]; then + cd ../../../ + exit 1 +else + cd ../../../ + favicon_folder="./public/assets/favicon/" + + echo "⏳ Replacing default favicons with freshly generated pack..." + if [ -d "$favicon_folder" ]; then + rm -r "$favicon_folder" + fi + mkdir -p "$favicon_folder" + cp -r ./deploy/tools/favicon-generator/output/* "$favicon_folder" +fi \ No newline at end of file diff --git a/deploy/scripts/make_envs_script.sh b/deploy/scripts/make_envs_script.sh new file mode 100755 index 0000000000..0634296c45 --- /dev/null +++ b/deploy/scripts/make_envs_script.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +echo "🌀 Creating client script with ENV values..." + +# Define the output file name +output_file="${1:-./public/assets/envs.js}" + +touch $output_file; +truncate -s 0 $output_file; + +# Check if the .env file exists and load ENVs from it +if [ -f .env ]; then + source .env + export $(cut -d= -f1 .env) +fi + +echo "window.__envs = {" >> $output_file; + +# Iterate through all environment variables +for var in $(env | grep '^NEXT_PUBLIC_' | cut -d= -f1); do + # Get the value of the variable + value="${!var}" + + # Replace double quotes with single quotes + value="${value//\"/\'}" + + # Write the variable name and value to the output file + echo "${var}: \"${value}\"," >> "$output_file" +done + +echo "}" >> $output_file; + +echo "✅ Done." diff --git a/deploy/scripts/validate_envs.sh b/deploy/scripts/validate_envs.sh new file mode 100755 index 0000000000..55211d1c59 --- /dev/null +++ b/deploy/scripts/validate_envs.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Check if the .env file exists +if [ -f .env ]; then + # Load the environment variables from .env + source .env +fi + +# Check run-time ENVs values integrity +node "$(dirname "$0")/envs-validator.js" "$input" +if [ $? != 0 ]; then + exit 1 +fi \ No newline at end of file diff --git a/deploy/tools/affected-tests/.gitignore b/deploy/tools/affected-tests/.gitignore new file mode 100644 index 0000000000..30bc162798 --- /dev/null +++ b/deploy/tools/affected-tests/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/deploy/tools/affected-tests/index.js b/deploy/tools/affected-tests/index.js new file mode 100644 index 0000000000..f2fc450eb6 --- /dev/null +++ b/deploy/tools/affected-tests/index.js @@ -0,0 +1,208 @@ +/* eslint-disable no-console */ +const { execSync } = require('child_process'); +const dependencyTree = require('dependency-tree'); +const fs = require('fs'); +const path = require('path'); + +const ROOT_DIR = path.resolve(__dirname, '../../../'); + +const TARGET_FILE = path.resolve(ROOT_DIR, './playwright/affected-tests.txt'); + +const NON_EXISTENT_DEPS = []; + +const DIRECTORIES_WITH_TESTS = [ + path.resolve(ROOT_DIR, './ui'), +]; +const VISITED = {}; + +function getAllPwFilesInDirectory(directory) { + const files = fs.readdirSync(directory, { recursive: true }); + return files + .filter((file) => file.endsWith('.pw.tsx')) + .map((file) => path.join(directory, file)); +} + +function getFileDeps(filename, changedNpmModules) { + return dependencyTree.toList({ + filename, + directory: ROOT_DIR, + filter: (path) => { + if (path.indexOf('node_modules') === -1) { + return true; + } + + if (changedNpmModules.some((module) => path.startsWith(module))) { + return true; + } + + return false; + }, + tsConfig: path.resolve(ROOT_DIR, './tsconfig.json'), + nonExistent: NON_EXISTENT_DEPS, + visited: VISITED, + }); +} + +async function getChangedFiles() { + const command = process.env.CI ? + `git diff --name-only origin/${ process.env.GITHUB_BASE_REF } ${ process.env.GITHUB_SHA } -- ${ ROOT_DIR }` : + `git diff --name-only main $(git branch --show-current) -- ${ ROOT_DIR }`; + + console.log('Executing command: ', command); + const files = execSync(command) + .toString() + .trim() + .split('\n') + .filter(Boolean); + + return files.map((file) => path.join(ROOT_DIR, file)); +} + +function checkChangesInChakraTheme(changedFiles) { + const themeDir = path.resolve(ROOT_DIR, './theme'); + return changedFiles.some((file) => file.startsWith(themeDir)); +} + +function checkChangesInSvgSprite(changedFiles) { + const iconDir = path.resolve(ROOT_DIR, './icons'); + const areIconsChanged = changedFiles.some((file) => file.startsWith(iconDir)); + + if (!areIconsChanged) { + return false; + } + + const svgNamesFile = path.resolve(ROOT_DIR, './public/icons/name.d.ts'); + const areSvgNamesChanged = changedFiles.some((file) => file === svgNamesFile); + + if (!areSvgNamesChanged) { + // If only the icons have changed and not the names in the SVG file, we will need to run all tests. + // This is because we cannot correctly identify the test files that depend on these changes. + return true; + } + + // If the icon names have changed, then there should be changes in the components that use them. + // Otherwise, typescript would complain about that. + return false; +} + +function createTargetFile(content) { + fs.writeFileSync(TARGET_FILE, content); +} + +function getPackageJsonUpdatedProps(packageJsonFile) { + const command = process.env.CI ? + `git diff --unified=0 origin/${ process.env.GITHUB_BASE_REF } ${ process.env.GITHUB_SHA } -- ${ packageJsonFile }` : + `git diff --unified=0 main $(git branch --show-current) -- ${ packageJsonFile }`; + + console.log('Executing command: ', command); + const changedLines = execSync(command) + .toString() + .trim() + .split('\n') + .filter(Boolean) + .filter((line) => line.startsWith('+ ') || line.startsWith('- ')); + + const changedProps = [ ...new Set( + changedLines + .map((line) => line.replaceAll(' ', '').replaceAll('+', '').replaceAll('-', '')) + .map((line) => line.split(':')[0].replaceAll('"', '')), + ) ]; + + return changedProps; +} + +function getUpdatedNpmModules(changedFiles) { + const packageJsonFile = path.resolve(ROOT_DIR, './package.json'); + + if (!changedFiles.includes(packageJsonFile)) { + return []; + } + + try { + const packageJsonContent = JSON.parse(fs.readFileSync(packageJsonFile, 'utf-8')); + const usedNpmModules = [ + ...Object.keys(packageJsonContent.dependencies || {}), + ...Object.keys(packageJsonContent.devDependencies || {}), + ]; + const updatedProps = getPackageJsonUpdatedProps(packageJsonFile); + + return updatedProps.filter((prop) => usedNpmModules.includes(prop)); + } catch (error) {} +} + +async function run() { + // NOTES: + // - The absence of TARGET_FILE implies that all tests should be run. + // - The empty TARGET_FILE implies that no tests should be run. + + const start = Date.now(); + + fs.unlink(TARGET_FILE, () => {}); + + const changedFiles = await getChangedFiles(); + + if (!changedFiles.length) { + createTargetFile(''); + console.log('No changed files found. Exiting...'); + return; + } + + console.log('Changed files in the branch: ', changedFiles); + + if (checkChangesInChakraTheme(changedFiles)) { + console.log('Changes in Chakra theme detected. It is advisable to run all test suites. Exiting...'); + return; + } + + if (checkChangesInSvgSprite(changedFiles)) { + console.log('There are some changes in the SVG sprite that cannot be linked to a specific component. It is advisable to run all test suites. Exiting...'); + return; + } + + let changedNpmModules = getUpdatedNpmModules(changedFiles); + + if (!changedNpmModules) { + console.log('Some error occurred while detecting changed NPM modules. It is advisable to run all test suites. Exiting...'); + return; + } + + console.log('Changed NPM modules in the branch: ', changedNpmModules); + + changedNpmModules = [ + ...changedNpmModules, + ...changedNpmModules.map((module) => `@types/${ module }`), // there are some deps that are resolved to .d.ts files + ].map((module) => path.resolve(ROOT_DIR, `./node_modules/${ module }`)); + + const allTestFiles = DIRECTORIES_WITH_TESTS.reduce((acc, dir) => { + return acc.concat(getAllPwFilesInDirectory(dir)); + }, []); + + const isDepChanged = (dep) => changedFiles.includes(dep) || changedNpmModules.some((module) => dep.startsWith(module)); + + const testFilesToRun = allTestFiles + .map((file) => ({ file, deps: getFileDeps(file, changedNpmModules) })) + .filter(({ deps }) => deps.some(isDepChanged)); + const testFileNamesToRun = testFilesToRun.map(({ file }) => path.relative(ROOT_DIR, file)); + + if (!testFileNamesToRun.length) { + createTargetFile(''); + console.log('No tests to run. Exiting...'); + return; + } + + createTargetFile(testFileNamesToRun.join('\n')); + + const end = Date.now(); + + const testFilesToRunWithFilteredDeps = testFilesToRun.map(({ file, deps }) => ({ + file, + deps: deps.filter(isDepChanged), + })); + + console.log('Total time: ', ((end - start) / 1_000).toLocaleString()); + console.log('Total test to run: ', testFileNamesToRun.length); + console.log('Tests to run with changed deps: ', testFilesToRunWithFilteredDeps); + console.log('Non existent deps: ', NON_EXISTENT_DEPS); +} + +run(); diff --git a/deploy/tools/affected-tests/package.json b/deploy/tools/affected-tests/package.json new file mode 100644 index 0000000000..bfba5734fa --- /dev/null +++ b/deploy/tools/affected-tests/package.json @@ -0,0 +1,10 @@ +{ + "name": "affected-tests", + "version": "1.0.0", + "main": "index.js", + "author": "Vasilii (tom) Goriunov ", + "license": "MIT", + "dependencies": { + "dependency-tree": "10.0.9" + } +} diff --git a/deploy/tools/affected-tests/yarn.lock b/deploy/tools/affected-tests/yarn.lock new file mode 100644 index 0000000000..385918d425 --- /dev/null +++ b/deploy/tools/affected-tests/yarn.lock @@ -0,0 +1,716 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.21.8": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + +"@dependents/detective-less@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@dependents/detective-less/-/detective-less-4.1.0.tgz#4a979ee7a6a79eb33602862d6a1263e30f98002e" + integrity sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^6.0.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@^5.59.5": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + +app-module-path@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5" + integrity sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +ast-module-types@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-5.0.0.tgz#32b2b05c56067ff38e95df66f11d6afd6c9ba16b" + integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +color-name@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +dependency-tree@10.0.9: + version "10.0.9" + resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-10.0.9.tgz#0c6c0dbeb0c5ec2cf83bf755f30e9cb12e7b4ac7" + integrity sha512-dwc59FRIsht+HfnTVM0BCjJaEWxdq2YAvEDy4/Hn6CwS3CBWMtFnL3aZGAkQn3XCYxk/YcTDE4jX2Q7bFTwCjA== + dependencies: + commander "^10.0.1" + filing-cabinet "^4.1.6" + precinct "^11.0.5" + typescript "^5.0.4" + +detective-amd@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/detective-amd/-/detective-amd-5.0.2.tgz#579900f301c160efe037a6377ec7e937434b2793" + integrity sha512-XFd/VEQ76HSpym80zxM68ieB77unNuoMwopU2TFT/ErUk5n4KvUTwW4beafAVUugrjV48l4BmmR0rh2MglBaiA== + dependencies: + ast-module-types "^5.0.0" + escodegen "^2.0.0" + get-amd-module-type "^5.0.1" + node-source-walk "^6.0.1" + +detective-cjs@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/detective-cjs/-/detective-cjs-5.0.1.tgz#836ad51c6de4863efc7c419ec243694f760ff8b2" + integrity sha512-6nTvAZtpomyz/2pmEmGX1sXNjaqgMplhQkskq2MLrar0ZAIkHMrDhLXkRiK2mvbu9wSWr0V5/IfiTrZqAQMrmQ== + dependencies: + ast-module-types "^5.0.0" + node-source-walk "^6.0.0" + +detective-es6@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-4.0.1.tgz#38d5d49a6d966e992ef8f2d9bffcfe861a58a88a" + integrity sha512-k3Z5tB4LQ8UVHkuMrFOlvb3GgFWdJ9NqAa2YLUU/jTaWJIm+JJnEh4PsMc+6dfT223Y8ACKOaC0qcj7diIhBKw== + dependencies: + node-source-walk "^6.0.1" + +detective-postcss@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/detective-postcss/-/detective-postcss-6.1.3.tgz#51a2d4419327ad85d0af071c7054c79fafca7e73" + integrity sha512-7BRVvE5pPEvk2ukUWNQ+H2XOq43xENWbH0LcdCE14mwgTBEAMoAx+Fc1rdp76SmyZ4Sp48HlV7VedUnP6GA1Tw== + dependencies: + is-url "^1.2.4" + postcss "^8.4.23" + postcss-values-parser "^6.0.2" + +detective-sass@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/detective-sass/-/detective-sass-5.0.3.tgz#63e54bc9b32f4bdbd9d5002308f9592a3d3a508f" + integrity sha512-YsYT2WuA8YIafp2RVF5CEfGhhyIVdPzlwQgxSjK+TUm3JoHP+Tcorbk3SfG0cNZ7D7+cYWa0ZBcvOaR0O8+LlA== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^6.0.1" + +detective-scss@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-4.0.3.tgz#79758baa0158f72bfc4481eb7e21cc3b5f1ea6eb" + integrity sha512-VYI6cHcD0fLokwqqPFFtDQhhSnlFWvU614J42eY6G0s8c+MBhi9QAWycLwIOGxlmD8I/XvGSOUV1kIDhJ70ZPg== + dependencies: + gonzales-pe "^4.3.0" + node-source-walk "^6.0.1" + +detective-stylus@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detective-stylus/-/detective-stylus-4.0.0.tgz#ce97b6499becdc291de7b3c11df8c352c1eee46e" + integrity sha512-TfPotjhszKLgFBzBhTOxNHDsutIxx9GTWjrL5Wh7Qx/ydxKhwUrlSFeLIn+ZaHPF+h0siVBkAQSuy6CADyTxgQ== + +detective-typescript@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/detective-typescript/-/detective-typescript-11.1.0.tgz#2deea5364cae1f0d9d3688bc596e662b049438cc" + integrity sha512-Mq8egjnW2NSCkzEb/Az15/JnBI/Ryyl6Po0Y+0mABTFvOS6DAyUGRZqz1nyhu4QJmWWe0zaGs/ITIBeWkvCkGw== + dependencies: + "@typescript-eslint/typescript-estree" "^5.59.5" + ast-module-types "^5.0.0" + node-source-walk "^6.0.1" + typescript "^5.0.4" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +enhanced-resolve@^5.14.1: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +escodegen@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +eslint-visitor-keys@^3.3.0: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.0.tgz#ca5e1a90b5e68f97fc8b61330d5819b82f5fab03" + integrity sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w== + dependencies: + reusify "^1.0.4" + +filing-cabinet@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/filing-cabinet/-/filing-cabinet-4.1.6.tgz#8d6d12cf3a84365bbd94e1cbf07d71c113420dd2" + integrity sha512-C+HZbuQTER36sKzGtUhrAPAoK6+/PrrUhYDBQEh3kBRdsyEhkLbp1ML8S0+6e6gCUrUlid+XmubxJrhvL2g/Zw== + dependencies: + app-module-path "^2.2.0" + commander "^10.0.1" + enhanced-resolve "^5.14.1" + is-relative-path "^1.0.2" + module-definition "^5.0.1" + module-lookup-amd "^8.0.5" + resolve "^1.22.3" + resolve-dependency-path "^3.0.2" + sass-lookup "^5.0.1" + stylus-lookup "^5.0.1" + tsconfig-paths "^4.2.0" + typescript "^5.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-amd-module-type@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-5.0.1.tgz#bef38ea3674e1aa1bda9c59c8b0da598582f73f2" + integrity sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw== + dependencies: + ast-module-types "^5.0.0" + node-source-walk "^6.0.1" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gonzales-pe@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== + dependencies: + minimist "^1.2.5" + +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +ignore@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== + +is-relative-path@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-relative-path/-/is-relative-path-1.0.2.tgz#091b46a0d67c1ed0fe85f1f8cfdde006bb251d46" + integrity sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA== + +is-url-superb@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2" + integrity sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA== + +is-url@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +module-definition@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-5.0.1.tgz#62d1194e5d5ea6176b7dc7730f818f466aefa32f" + integrity sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA== + dependencies: + ast-module-types "^5.0.0" + node-source-walk "^6.0.1" + +module-lookup-amd@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/module-lookup-amd/-/module-lookup-amd-8.0.5.tgz#aaeea41979105b49339380ca3f7d573db78c32a5" + integrity sha512-vc3rYLjDo5Frjox8NZpiyLXsNWJ5BWshztc/5KSOMzpg9k5cHH652YsJ7VKKmtM4SvaxuE9RkrYGhiSjH3Ehow== + dependencies: + commander "^10.0.1" + glob "^7.2.3" + requirejs "^2.3.6" + requirejs-config-file "^4.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-6.0.2.tgz#ba81bc4bc0f6f05559b084bea10be84c3f87f211" + integrity sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag== + dependencies: + "@babel/parser" "^7.21.8" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss-values-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz#636edc5b86c953896f1bb0d7a7a6615df00fb76f" + integrity sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw== + dependencies: + color-name "^1.1.4" + is-url-superb "^4.0.0" + quote-unquote "^1.0.0" + +postcss@^8.4.23: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +precinct@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/precinct/-/precinct-11.0.5.tgz#3e15b3486670806f18addb54b8533e23596399ff" + integrity sha512-oHSWLC8cL/0znFhvln26D14KfCQFFn4KOLSw6hmLhd+LQ2SKt9Ljm89but76Pc7flM9Ty1TnXyrA2u16MfRV3w== + dependencies: + "@dependents/detective-less" "^4.1.0" + commander "^10.0.1" + detective-amd "^5.0.2" + detective-cjs "^5.0.1" + detective-es6 "^4.0.1" + detective-postcss "^6.1.3" + detective-sass "^5.0.3" + detective-scss "^4.0.3" + detective-stylus "^4.0.0" + detective-typescript "^11.1.0" + module-definition "^5.0.1" + node-source-walk "^6.0.2" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quote-unquote@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b" + integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg== + +requirejs-config-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc" + integrity sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw== + dependencies: + esprima "^4.0.0" + stringify-object "^3.2.1" + +requirejs@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" + integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== + +resolve-dependency-path@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-3.0.2.tgz#012816717bcbe8b846835da11af9d2beb5acef50" + integrity sha512-Tz7zfjhLfsvR39ADOSk9us4421J/1ztVBo4rWUkF38hgHK5m0OCZ3NxFVpqHRkjctnwVa15igEUHFJp8MCS7vA== + +resolve@^1.22.3: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +sass-lookup@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-5.0.1.tgz#1f01d7ff21e09d8c9dcf8d05b3fca28f2f96e6ed" + integrity sha512-t0X5PaizPc2H4+rCwszAqHZRtr4bugo4pgiCvrBFvIX0XFxnr29g77LJcpyj9A0DcKf7gXMLcgvRjsonYI6x4g== + dependencies: + commander "^10.0.1" + +semver@^7.3.7: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +stringify-object@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +stylus-lookup@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-5.0.1.tgz#3c4d116c3b1e8e1a8169c0d9cd20e608595560f4" + integrity sha512-tLtJEd5AGvnVy4f9UHQMw4bkJJtaAcmo54N+ovQBjDY3DuWyK9Eltxzr5+KG0q4ew6v2EHyuWWNnHeiw/Eo7rQ== + dependencies: + commander "^10.0.1" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +typescript@^5.0.4: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/deploy/tools/envs-validator/.gitignore b/deploy/tools/envs-validator/.gitignore new file mode 100644 index 0000000000..caba20fba5 --- /dev/null +++ b/deploy/tools/envs-validator/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/public +.env +.env.registry +.env.secrets +index.js \ No newline at end of file diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts new file mode 100644 index 0000000000..9770ea6399 --- /dev/null +++ b/deploy/tools/envs-validator/index.ts @@ -0,0 +1,137 @@ +/* eslint-disable no-console */ +import fs from 'fs'; +import path from 'path'; +import type { ValidationError } from 'yup'; + +import { buildExternalAssetFilePath } from '../../../configs/app/utils'; +import schema from './schema'; + +const silent = process.argv.includes('--silent'); + +run(); + +async function run() { + !silent && console.log(); + try { + const appEnvs = Object.entries(process.env) + .filter(([ key ]) => key.startsWith('NEXT_PUBLIC_')) + .reduce((result, [ key, value ]) => { + result[key] = value || ''; + return result; + }, {} as Record); + + await checkPlaceholdersCongruity(appEnvs); + await validateEnvs(appEnvs); + + } catch (error) { + process.exit(1); + } +} + +async function validateEnvs(appEnvs: Record) { + !silent && console.log(`🌀 Validating ENV variables values...`); + + try { + // replace ENVs with external JSON files content + const envsWithJsonConfig = [ + 'NEXT_PUBLIC_FEATURED_NETWORKS', + 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', + 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL', + 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', + 'NEXT_PUBLIC_FOOTER_LINKS', + ]; + + for await (const envName of envsWithJsonConfig) { + if (appEnvs[envName]) { + appEnvs[envName] = await getExternalJsonContent(envName) || '[]'; + } + } + + await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false }); + !silent && console.log('👍 All good!'); + } catch (_error) { + if (typeof _error === 'object' && _error !== null && 'errors' in _error) { + console.log('🚨 ENVs validation failed with the following errors:'); + (_error as ValidationError).errors.forEach((error) => { + console.log(' ', error); + }); + } else { + console.log('🚨 Unexpected error occurred during validation.'); + console.error(_error); + } + + throw _error; + } + + !silent && console.log(); +} + +async function getExternalJsonContent(envName: string): Promise { + return new Promise((resolve, reject) => { + const fileName = `./public${ buildExternalAssetFilePath(envName, 'https://foo.bar/baz.json') }`; + + fs.readFile(path.resolve(__dirname, fileName), 'utf8', (err, data) => { + if (err) { + console.log(`🚨 Unable to read file: ${ fileName }`); + reject(err); + return; + } + + resolve(data); + }); + }); +} + +async function checkPlaceholdersCongruity(envsMap: Record) { + try { + !silent && console.log(`🌀 Checking environment variables and their placeholders congruity...`); + + const runTimeEnvs = await getEnvsPlaceholders(path.resolve(__dirname, '.env.registry')); + const buildTimeEnvs = await getEnvsPlaceholders(path.resolve(__dirname, '.env')); + const envs = Object.keys(envsMap).filter((env) => !buildTimeEnvs.includes(env)); + + const inconsistencies: Array = []; + for (const env of envs) { + const hasPlaceholder = runTimeEnvs.includes(env); + if (!hasPlaceholder) { + inconsistencies.push(env); + } + } + + if (inconsistencies.length > 0) { + console.log('🚸 For the following environment variables placeholders were not generated at build-time:'); + inconsistencies.forEach((env) => { + console.log(` ${ env }`); + }); + console.log(` They are either deprecated or running the app with them may lead to unexpected behavior. + Please check the documentation for more details - https://github.com/blockscout/frontend/blob/main/docs/ENVS.md + `); + throw new Error(); + } + + !silent && console.log('👍 All good!\n'); + } catch (error) { + console.log('🚨 Congruity check failed.\n'); + throw error; + } +} + +function getEnvsPlaceholders(filePath: string): Promise> { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + console.log(`🚨 Unable to read placeholders file.`); + reject(err); + return; + } + + const lines = data.split('\n'); + const variables = lines.map(line => { + const variable = line.split('=')[0]; + return variable.trim(); + }); + + resolve(variables.filter(Boolean)); + }); + }); +} diff --git a/deploy/tools/envs-validator/package.json b/deploy/tools/envs-validator/package.json new file mode 100644 index 0000000000..1cc1803950 --- /dev/null +++ b/deploy/tools/envs-validator/package.json @@ -0,0 +1,21 @@ +{ + "name": "envs-validator", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "build": "yarn webpack-cli -c ./webpack.config.js", + "validate": "node ./index.js", + "test": "./test.sh" + }, + "dependencies": { + "ts-loader": "^9.4.4", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "yup": "^1.2.0" + }, + "devDependencies": { + "dotenv-cli": "^7.2.1", + "tsconfig-paths-webpack-plugin": "^4.1.0" + } +} diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts new file mode 100644 index 0000000000..a4daf66371 --- /dev/null +++ b/deploy/tools/envs-validator/schema.ts @@ -0,0 +1,751 @@ +/* eslint-disable max-len */ +declare module 'yup' { + interface StringSchema { + // Yup's URL validator is not perfect so we made our own + // https://github.com/jquense/yup/pull/1859 + url(): never; + } +} + +import * as yup from 'yup'; + +import type { AdButlerConfig } from '../../../types/client/adButlerConfig'; +import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders'; +import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders'; +import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract'; +import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown'; +import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig'; +import { GAS_UNITS } from '../../../types/client/gasTracker'; +import type { GasUnit } from '../../../types/client/gasTracker'; +import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace'; +import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig'; +import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation'; +import type { NavItemExternal, NavigationLinkId, NavigationLayout } from '../../../types/client/navigation'; +import { ROLLUP_TYPES } from '../../../types/client/rollup'; +import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token'; +import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation'; +import { VALIDATORS_CHAIN_TYPE } from '../../../types/client/validators'; +import type { ValidatorsChainType } from '../../../types/client/validators'; +import type { WalletType } from '../../../types/client/wallets'; +import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; +import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; +import { CHAIN_INDICATOR_IDS } from '../../../types/homepage'; +import type { ChainIndicatorId } from '../../../types/homepage'; +import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; +import { COLOR_THEME_IDS } from '../../../types/settings'; +import type { AddressViewId } from '../../../types/views/address'; +import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address'; +import { BLOCK_FIELDS_IDS } from '../../../types/views/block'; +import type { BlockFieldId } from '../../../types/views/block'; +import type { NftMarketplaceItem } from '../../../types/views/nft'; +import type { TxAdditionalFieldsId, TxFieldsId } from '../../../types/views/tx'; +import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from '../../../types/views/tx'; + +import { replaceQuotes } from '../../../configs/app/utils'; +import * as regexp from '../../../lib/regexp'; +import type { IconName } from '../../../ui/shared/IconSvg'; + +const protocols = [ 'http', 'https' ]; + +const urlTest: yup.TestConfig = { + name: 'url', + test: (value: unknown) => { + if (!value) { + return true; + } + + try { + if (typeof value === 'string') { + new URL(value); + return true; + } + } catch (error) {} + + return false; + }, + message: '${path} is not a valid URL', + exclusive: true, +}; + +const marketplaceAppSchema: yup.ObjectSchema = yup + .object({ + id: yup.string().required(), + external: yup.boolean(), + title: yup.string().required(), + logo: yup.string().test(urlTest).required(), + logoDarkMode: yup.string().test(urlTest), + shortDescription: yup.string().required(), + categories: yup.array().of(yup.string().required()).required(), + url: yup.string().test(urlTest).required(), + author: yup.string().required(), + description: yup.string().required(), + site: yup.string().test(urlTest), + twitter: yup.string().test(urlTest), + telegram: yup.string().test(urlTest), + github: yup.lazy(value => + Array.isArray(value) ? + yup.array().of(yup.string().required().test(urlTest)) : + yup.string().test(urlTest), + ), + discord: yup.string().test(urlTest), + internalWallet: yup.boolean(), + priority: yup.number(), + }); + +const issueSeverityDistributionSchema: yup.ObjectSchema = yup + .object({ + critical: yup.number().required(), + gas: yup.number().required(), + high: yup.number().required(), + informational: yup.number().required(), + low: yup.number().required(), + medium: yup.number().required(), + }); + +const solidityscanReportSchema: yup.ObjectSchema = yup + .object({ + contractname: yup.string().required(), + scan_status: yup.string().required(), + scan_summary: yup + .object({ + issue_severity_distribution: issueSeverityDistributionSchema.required(), + lines_analyzed_count: yup.number().required(), + scan_time_taken: yup.number().required(), + score: yup.string().required(), + score_v2: yup.string().required(), + threat_score: yup.string().required(), + }) + .required(), + scanner_reference_url: yup.string().test(urlTest).required(), + }); + +const contractDataSchema: yup.ObjectSchema = yup + .object({ + address: yup.string().required(), + isVerified: yup.boolean().required(), + solidityScanReport: solidityscanReportSchema.nullable().notRequired(), + }); + +const chainsDataSchema = yup.lazy((objValue) => { + let schema = yup.object(); + Object.keys(objValue).forEach((key) => { + schema = schema.shape({ + [key]: yup.object({ + overallInfo: yup.object({ + verifiedNumber: yup.number().required(), + totalContractsNumber: yup.number().required(), + solidityScanContractsNumber: yup.number().required(), + securityScore: yup.number().required(), + issueSeverityDistribution: issueSeverityDistributionSchema.required(), + }).required(), + contractsData: yup.array().of(contractDataSchema).required(), + }), + }); + }); + return schema; +}); + +const securityReportSchema: yup.ObjectSchema = yup + .object({ + appName: yup.string().required(), + chainsData: chainsDataSchema, + }); + +const marketplaceSchema = yup + .object() + .shape({ + NEXT_PUBLIC_MARKETPLACE_ENABLED: yup.boolean(), + NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: yup + .array() + .json() + .of(marketplaceAppSchema) + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: yup + .array() + .json() + .of(yup.string()) + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema.test(urlTest).required(), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema.test(urlTest), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: yup + .array() + .json() + .of(securityReportSchema) + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_FEATURED_APP cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema.test(urlTest), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema.test(urlTest), + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID: yup + .string() + .when('NEXT_PUBLIC_MARKETPLACE_ENABLED', { + is: true, + then: (schema) => schema, + // eslint-disable-next-line max-len + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), + }), + }); + +const beaconChainSchema = yup + .object() + .shape({ + NEXT_PUBLIC_HAS_BEACON_CHAIN: yup.boolean(), + NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL: yup + .string() + .when('NEXT_PUBLIC_HAS_BEACON_CHAIN', { + is: (value: boolean) => value, + then: (schema) => schema.min(1).optional(), + otherwise: (schema) => schema.max( + -1, + 'NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL cannot not be used if NEXT_PUBLIC_HAS_BEACON_CHAIN is not set to "true"', + ), + }), + }); + +const rollupSchema = yup + .object() + .shape({ + NEXT_PUBLIC_ROLLUP_TYPE: yup.string().oneOf(ROLLUP_TYPES), + NEXT_PUBLIC_ROLLUP_L1_BASE_URL: yup + .string() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value, + then: (schema) => schema.test(urlTest).required(), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined'), + }), + NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL: yup + .string() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: (value: string) => value === 'optimistic', + then: (schema) => schema.test(urlTest).required(), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL can be used only if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' '), + }), + }); + +const adButlerConfigSchema = yup + .object() + .transform(replaceQuotes) + .json() + .when('NEXT_PUBLIC_AD_BANNER_PROVIDER', { + is: (value: AdBannerProviders) => value === 'adbutler', + then: (schema) => schema + .shape({ + id: yup.string().required(), + width: yup.number().positive().required(), + height: yup.number().positive().required(), + }) + .required(), + }) + .when('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER', { + is: (value: AdBannerProviders) => value === 'adbutler', + then: (schema) => schema + .shape({ + id: yup.string().required(), + width: yup.number().positive().required(), + height: yup.number().positive().required(), + }) + .required(), + }); + +const adsBannerSchema = yup + .object() + .shape({ + NEXT_PUBLIC_AD_BANNER_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_PROVIDERS), + NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS), + NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema, + NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema, + }); + +const sentrySchema = yup + .object() + .shape({ + NEXT_PUBLIC_SENTRY_DSN: yup.string().test(urlTest), + SENTRY_CSP_REPORT_URI: yup + .string() + .when('NEXT_PUBLIC_SENTRY_DSN', { + is: (value: string) => Boolean(value), + then: (schema) => schema.test(urlTest), + otherwise: (schema) => schema.max(-1, 'SENTRY_CSP_REPORT_URI cannot not be used without NEXT_PUBLIC_SENTRY_DSN'), + }), + NEXT_PUBLIC_SENTRY_ENABLE_TRACING: yup + .boolean() + .when('NEXT_PUBLIC_SENTRY_DSN', { + is: (value: string) => Boolean(value), + then: (schema) => schema, + }), + NEXT_PUBLIC_APP_INSTANCE: yup + .string() + .when('NEXT_PUBLIC_SENTRY_DSN', { + is: (value: string) => Boolean(value), + then: (schema) => schema, + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_APP_INSTANCE cannot not be used without NEXT_PUBLIC_SENTRY_DSN'), + }), + NEXT_PUBLIC_APP_ENV: yup + .string() + .when('NEXT_PUBLIC_SENTRY_DSN', { + is: (value: string) => Boolean(value), + then: (schema) => schema, + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_APP_ENV cannot not be used without NEXT_PUBLIC_SENTRY_DSN'), + }), + }); + +const accountSchema = yup + .object() + .shape({ + NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(), + NEXT_PUBLIC_AUTH0_CLIENT_ID: yup + .string() + .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', { + is: (value: boolean) => value, + then: (schema) => schema.required(), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_AUTH0_CLIENT_ID cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'), + }), + NEXT_PUBLIC_AUTH_URL: yup + .string() + .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', { + is: (value: boolean) => value, + then: (schema) => schema.test(urlTest), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_AUTH_URL cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'), + }), + NEXT_PUBLIC_LOGOUT_URL: yup + .string() + .when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', { + is: (value: boolean) => value, + then: (schema) => schema.test(urlTest).required(), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_LOGOUT_URL cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not set to "true"'), + }), + }); + +const featuredNetworkSchema: yup.ObjectSchema = yup + .object() + .shape({ + title: yup.string().required(), + url: yup.string().test(urlTest).required(), + group: yup.string().oneOf(NETWORK_GROUPS).required(), + icon: yup.string().test(urlTest), + isActive: yup.boolean(), + invertIconInDarkMode: yup.boolean(), + }); + +const navItemExternalSchema: yup.ObjectSchema = yup + .object({ + text: yup.string().required(), + url: yup.string().test(urlTest).required(), + }); + +const footerLinkSchema: yup.ObjectSchema = yup + .object({ + text: yup.string().required(), + url: yup.string().test(urlTest).required(), + }); + +const footerLinkGroupSchema: yup.ObjectSchema = yup + .object({ + title: yup.string().required(), + links: yup + .array() + .of(footerLinkSchema) + .required(), + }); + +const networkExplorerSchema: yup.ObjectSchema = yup + .object({ + title: yup.string().required(), + logo: yup.string().test(urlTest), + baseUrl: yup.string().test(urlTest).required(), + paths: yup + .object() + .shape({ + tx: yup.string(), + address: yup.string(), + token: yup.string(), + block: yup.string(), + }), + }); + +const contractCodeIdeSchema: yup.ObjectSchema = yup + .object({ + title: yup.string().required(), + url: yup.string().test(urlTest).required(), + icon_url: yup.string().test(urlTest).required(), + }); + +const nftMarketplaceSchema: yup.ObjectSchema = yup + .object({ + name: yup.string().required(), + collection_url: yup.string().test(urlTest).required(), + instance_url: yup.string().test(urlTest).required(), + logo_url: yup.string().test(urlTest).required(), + }); + +const bridgedTokenChainSchema: yup.ObjectSchema = yup + .object({ + id: yup.string().required(), + title: yup.string().required(), + short_title: yup.string().required(), + base_url: yup.string().test(urlTest).required(), + }); + +const tokenBridgeSchema: yup.ObjectSchema = yup + .object({ + type: yup.string().required(), + title: yup.string().required(), + short_title: yup.string().required(), + }); + +const bridgedTokensSchema = yup + .object() + .shape({ + NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS: yup + .array() + .transform(replaceQuotes) + .json() + .of(bridgedTokenChainSchema), + NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES: yup + .array() + .transform(replaceQuotes) + .json() + .of(tokenBridgeSchema) + .when('NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', { + is: (value: Array) => value && value.length > 0, + then: (schema) => schema.required(), + otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES cannot not be used without NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS'), + }), + }); + +const deFiDropdownItemSchema: yup.ObjectSchema = yup + .object({ + text: yup.string().required(), + icon: yup.string().required(), + dappId: yup.string(), + url: yup.string().test(urlTest), + }) + .test('oneOfRequired', 'NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: Either dappId or url is required', function(value) { + return Boolean(value.dappId) || Boolean(value.url); + }) as yup.ObjectSchema; + +const schema = yup + .object() + .noUnknown(true, (params) => { + return `Unknown ENV variables were provided: ${ params.unknown }`; + }) + .shape({ + // I. Build-time ENVs + // ----------------- + NEXT_PUBLIC_GIT_TAG: yup.string(), + NEXT_PUBLIC_GIT_COMMIT_SHA: yup.string(), + + // II. Run-time ENVs + // ----------------- + // 1. App configuration + NEXT_PUBLIC_APP_HOST: yup.string().required(), + NEXT_PUBLIC_APP_PROTOCOL: yup.string().oneOf(protocols), + NEXT_PUBLIC_APP_PORT: yup.number().positive().integer(), + + // 2. Blockchain parameters + NEXT_PUBLIC_NETWORK_NAME: yup.string().required(), + NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(), + NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(), + NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest), + NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(), + NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), + NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), + NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(), + NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(), + NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES: yup.boolean(), + NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string().oneOf([ 'validation', 'mining' ]), + NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME: yup.string(), + NEXT_PUBLIC_IS_TESTNET: yup.boolean(), + + // 3. API configuration + NEXT_PUBLIC_API_PROTOCOL: yup.string().oneOf(protocols), + NEXT_PUBLIC_API_HOST: yup.string().required(), + NEXT_PUBLIC_API_PORT: yup.number().integer().positive(), + NEXT_PUBLIC_API_BASE_PATH: yup.string(), + NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL: yup.string().oneOf([ 'ws', 'wss' ]), + + // 4. UI configuration + // a. homepage + NEXT_PUBLIC_HOMEPAGE_CHARTS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)), + NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), + NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), + NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), + + // b. sidebar + NEXT_PUBLIC_FEATURED_NETWORKS: yup + .array() + .json() + .of(featuredNetworkSchema), + NEXT_PUBLIC_OTHER_LINKS: yup + .array() + .transform(replaceQuotes) + .json() + .of(navItemExternalSchema), + NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(NAVIGATION_LINK_IDS)), + NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string()), + NEXT_PUBLIC_NAVIGATION_LAYOUT: yup.string().oneOf([ 'horizontal', 'vertical' ]), + NEXT_PUBLIC_NETWORK_LOGO: yup.string().test(urlTest), + NEXT_PUBLIC_NETWORK_LOGO_DARK: yup.string().test(urlTest), + NEXT_PUBLIC_NETWORK_ICON: yup.string().test(urlTest), + NEXT_PUBLIC_NETWORK_ICON_DARK: yup.string().test(urlTest), + + // c. footer + NEXT_PUBLIC_FOOTER_LINKS: yup + .array() + .json() + .of(footerLinkGroupSchema), + + // d. views + NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(BLOCK_FIELDS_IDS)), + NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: yup.string().oneOf(IDENTICON_TYPES), + NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(ADDRESS_VIEWS_IDS)), + NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: yup.boolean(), + NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS: yup + .mixed() + .test( + 'shape', + 'Invalid schema were provided for NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS, it should be either array of method ids or "none" string literal', + (data) => { + const isNoneSchema = yup.string().oneOf([ 'none' ]); + const isArrayOfMethodsSchema = yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(SMART_CONTRACT_EXTRA_VERIFICATION_METHODS)); + + return isNoneSchema.isValidSync(data) || isArrayOfMethodsSchema.isValidSync(data); + }), + NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(TX_FIELDS_IDS)), + NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(TX_ADDITIONAL_FIELDS_IDS)), + NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: yup + .array() + .transform(replaceQuotes) + .json() + .of(nftMarketplaceSchema), + + // e. misc + NEXT_PUBLIC_NETWORK_EXPLORERS: yup + .array() + .transform(replaceQuotes) + .json() + .of(networkExplorerSchema), + NEXT_PUBLIC_CONTRACT_CODE_IDES: yup + .array() + .transform(replaceQuotes) + .json() + .of(contractCodeIdeSchema), + NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: yup.boolean(), + NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(), + NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(), + NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(), + NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS), + + // 5. Features configuration + NEXT_PUBLIC_API_SPEC_URL: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_API_SPEC_URL, it should be either URL-string or "none" string literal', (data) => { + const isNoneSchema = yup.string().oneOf([ 'none' ]); + const isUrlStringSchema = yup.string().test(urlTest); + + return isNoneSchema.isValidSync(data) || isUrlStringSchema.isValidSync(data); + }), + NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_STATS_API_BASE_PATH: yup.string(), + NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(), + NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GRAPHIQL_TRANSACTION, it should be either Hex-string or "none" string literal', (data) => { + const isNoneSchema = yup.string().oneOf([ 'none' ]); + const isHashStringSchema = yup.string().matches(regexp.HEX_REGEXP); + + return isNoneSchema.isValidSync(data) || isHashStringSchema.isValidSync(data); + }), + NEXT_PUBLIC_WEB3_WALLETS: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_WEB3_WALLETS, it should be either array or "none" string literal', (data) => { + const isNoneSchema = yup.string().equals([ 'none' ]); + const isArrayOfWalletsSchema = yup + .array() + .transform(replaceQuotes) + .json() + .of(yup.string().oneOf(SUPPORTED_WALLETS)); + + return isNoneSchema.isValidSync(data) || isArrayOfWalletsSchema.isValidSync(data); + }), + NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: yup.boolean(), + NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: yup.string().oneOf(TX_INTERPRETATION_PROVIDERS), + NEXT_PUBLIC_AD_TEXT_PROVIDER: yup.string().oneOf(SUPPORTED_AD_TEXT_PROVIDERS), + NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), + NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), + NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), + NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: yup.boolean(), + NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED: yup.boolean(), + NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest), + NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), + NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), + NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(), + NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG, it should have name and url template', (data) => { + const isUndefined = data === undefined; + const valueSchema = yup.object().transform(replaceQuotes).json().shape({ + name: yup.string().required(), + url_template: yup.string().required(), + logo: yup.string(), + dapp_id: yup.string(), + }); + + return isUndefined || valueSchema.isValidSync(data); + }), + NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG, it should have name and url template', (data) => { + const isUndefined = data === undefined; + const valueSchema = yup.object().transform(replaceQuotes).json().shape({ + name: yup.string().required(), + url_template: yup.string().required(), + logo: yup.string(), + dapp_id: yup.string(), + }); + + return isUndefined || valueSchema.isValidSync(data); + }), + NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string().oneOf(VALIDATORS_CHAIN_TYPE), + NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(), + NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string().oneOf(GAS_UNITS)), + NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: yup.boolean(), + NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: yup + .array() + .transform(replaceQuotes) + .json() + .of(deFiDropdownItemSchema), + NEXT_PUBLIC_FAULT_PROOF_ENABLED: yup.boolean() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: 'optimistic', + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_FAULT_PROOF_ENABLED can only be used with NEXT_PUBLIC_ROLLUP_TYPE=optimistic', + value => value === undefined, + ), + }), + NEXT_PUBLIC_HAS_MUD_FRAMEWORK: yup.boolean() + .when('NEXT_PUBLIC_ROLLUP_TYPE', { + is: 'optimistic', + then: (schema) => schema, + otherwise: (schema) => schema.test( + 'not-exist', + 'NEXT_PUBLIC_HAS_MUD_FRAMEWORK can only be used with NEXT_PUBLIC_ROLLUP_TYPE=optimistic', + value => value === undefined, + ), + }), + + // 6. External services envs + NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), + NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), + NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), + NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(), + + // Misc + NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(), + }) + .concat(accountSchema) + .concat(adsBannerSchema) + .concat(marketplaceSchema) + .concat(rollupSchema) + .concat(beaconChainSchema) + .concat(bridgedTokensSchema) + .concat(sentrySchema); + +export default schema; diff --git a/deploy/tools/envs-validator/test.sh b/deploy/tools/envs-validator/test.sh new file mode 100755 index 0000000000..46d3ea3fca --- /dev/null +++ b/deploy/tools/envs-validator/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +test_folder="./test" +common_file="${test_folder}/.env.common" + +# Generate ENV registry file +export NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) +export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) +../../scripts/collect_envs.sh ../../../docs/ENVS.md + +# Copy test assets +mkdir -p "./public/assets/configs" +cp -r ${test_folder}/assets ./public/ + +# Build validator script +yarn build + +validate_file() { + local test_file="$1" + + echo + echo "🧿 Validating file '$test_file'..." + + dotenv \ + -e $test_file \ + -e $common_file \ + yarn run validate -- --silent + + if [ $? -eq 0 ]; then + echo "👍 All good!" + return 0 + else + echo "🛑 The file is invalid. Please fix errors and run script again." + echo + return 1 + fi +} + +test_files=($(find "$test_folder" -maxdepth 1 -type f | grep -vE '\/\.env\.common$')) + +for file in "${test_files[@]}"; do + validate_file "$file" + if [ $? -eq 1 ]; then + exit 1 + fi +done diff --git a/deploy/tools/envs-validator/test/.env.adbutler b/deploy/tools/envs-validator/test/.env.adbutler new file mode 100644 index 0000000000..7877a7740c --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.adbutler @@ -0,0 +1,3 @@ +NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'123456','width':'728','height':'90'} +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'654321','width':'300','height':'100'} \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.adbutler_add b/deploy/tools/envs-validator/test/.env.adbutler_add new file mode 100644 index 0000000000..7f1968e4bb --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.adbutler_add @@ -0,0 +1,4 @@ +NEXT_PUBLIC_AD_BANNER_PROVIDER='slise' +NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER='adbutler' +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'123456','width':'728','height':'90'} +NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'654321','width':'300','height':'100'} \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.alt b/deploy/tools/envs-validator/test/.env.alt new file mode 100644 index 0000000000..62183782bf --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.alt @@ -0,0 +1,3 @@ +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none +NEXT_PUBLIC_API_SPEC_URL=none +NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base new file mode 100644 index 0000000000..607ff38d36 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.base @@ -0,0 +1,83 @@ +NEXT_PUBLIC_SENTRY_DSN=https://sentry.io +NEXT_PUBLIC_AUTH_URL=https://example.com +NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true +NEXT_PUBLIC_LOGOUT_URL=https://example.com +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X +NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx +NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx +NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx +FAVICON_GENERATOR_API_KEY=xxx +NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx +NEXT_PUBLIC_AD_TEXT_PROVIDER=coinzilla +NEXT_PUBLIC_AD_BANNER_PROVIDER=slise +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_SPEC_URL=https://example.com +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://example.com'}] +NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}] +NEXT_PUBLIC_COLOR_THEME_DEFAULT=dim +NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}] +NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com +NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true +NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com +NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps'] +NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal +NEXT_PUBLIC_FOOTER_LINKS=https://example.com +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d +NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false +NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff' +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)' +NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true +NEXT_PUBLIC_GAS_TRACKER_ENABLED=true +NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei'] +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='Hello' +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com +NEXT_PUBLIC_METASUITES_ENABLED=true +NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=['eth_rpc_api','rpc_api'] +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH +NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] +NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO +NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true +NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png +NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png +NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://example.com/logo.png +NEXT_PUBLIC_NETWORK_RPC_URL=https://example.com +NEXT_PUBLIC_NETWORK_SHORT_NAME=Test +NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation +NEXT_PUBLIC_OG_DESCRIPTION='Hello world!' +NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}] +NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true +NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global +NEXT_PUBLIC_STATS_API_HOST=https://example.com +NEXT_PUBLIC_STATS_API_BASE_PATH=/ +NEXT_PUBLIC_USE_NEXT_JS_PROXY=false +NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar +NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts'] +NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry'] +NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward'] +NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'NFT Marketplace','collection_url':'https://example.com/{hash}','instance_url':'https://example.com/{hash}/{id}','logo_url':'https://example.com/logo.png'}] +NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS=['fee_per_gas'] +NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS=['value','fee_currency','gas_price','tx_fee','gas_fees','burnt_fees'] +NEXT_PUBLIC_VISUALIZE_API_HOST=https://example.com +NEXT_PUBLIC_VISUALIZE_API_BASE_PATH=https://example.com +NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false +NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket'] +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability +NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}] +NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} +NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'dapp_id': 'smol-refuel', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&utm_medium=address&disableBridges=true', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'} diff --git a/deploy/tools/envs-validator/test/.env.beacon_chain b/deploy/tools/envs-validator/test/.env.beacon_chain new file mode 100644 index 0000000000..f0800e9af3 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.beacon_chain @@ -0,0 +1,2 @@ +NEXT_PUBLIC_HAS_BEACON_CHAIN=true +NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL=aETH \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.common b/deploy/tools/envs-validator/test/.env.common new file mode 100644 index 0000000000..5788f392d3 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.common @@ -0,0 +1,4 @@ +NEXT_PUBLIC_API_HOST=blockscout.com +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_NETWORK_ID=1 +NEXT_PUBLIC_NETWORK_NAME=Testnet diff --git a/deploy/tools/envs-validator/test/.env.marketplace b/deploy/tools/envs-validator/test/.env.marketplace new file mode 100644 index 0000000000..6cc6b1f839 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.marketplace @@ -0,0 +1,12 @@ +NEXT_PUBLIC_MARKETPLACE_ENABLED=true +NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://example.com +NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://example.com +NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://example.com +NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://example.com +NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://example.com +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://example.com +NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave +NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html +NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=test +NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=test diff --git a/deploy/tools/envs-validator/test/.env.rollup b/deploy/tools/envs-validator/test/.env.rollup new file mode 100644 index 0000000000..e7cacfb086 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.rollup @@ -0,0 +1,4 @@ +NEXT_PUBLIC_ROLLUP_TYPE=optimistic +NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com +NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com +NEXT_PUBLIC_FAULT_PROOF_ENABLED=true \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.sentry b/deploy/tools/envs-validator/test/.env.sentry new file mode 100644 index 0000000000..c34c4d4f26 --- /dev/null +++ b/deploy/tools/envs-validator/test/.env.sentry @@ -0,0 +1,5 @@ +NEXT_PUBLIC_SENTRY_DSN=https://sentry.io +SENTRY_CSP_REPORT_URI=https://sentry.io +NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true +NEXT_PUBLIC_APP_ENV=production +NEXT_PUBLIC_APP_INSTANCE=duck \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/assets/configs/featured_networks.json b/deploy/tools/envs-validator/test/assets/configs/featured_networks.json new file mode 100644 index 0000000000..83b9088843 --- /dev/null +++ b/deploy/tools/envs-validator/test/assets/configs/featured_networks.json @@ -0,0 +1,22 @@ +[ + { + "title": "Ethereum", + "url": "https://eth.blockscout.com/", + "group": "Mainnets", + "icon": "https://example.com/logo.svg" + }, + { + "title": "Goerli", + "url": "https://eth-goerli.blockscout.com/", + "group": "Testnets", + "isActive": true, + "icon": "https://example.com/logo.svg", + "invertIconInDarkMode": true + }, + { + "title": "POA Sokol", + "url": "https://blockscout.com/poa/sokol", + "group": "Other", + "icon": "https://example.com/logo.svg" + } +] \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/assets/configs/footer_links.json b/deploy/tools/envs-validator/test/assets/configs/footer_links.json new file mode 100644 index 0000000000..d3f7e7d437 --- /dev/null +++ b/deploy/tools/envs-validator/test/assets/configs/footer_links.json @@ -0,0 +1,28 @@ +[ + { + "title": "Foo", + "links": [ + { + "text": "Home", + "url": "https://example.com" + }, + { + "text": "Brand", + "url": "https://example.com" + } + ] + }, + { + "title": "Developers", + "links": [ + { + "text": "Develop", + "url": "https://example.com" + }, + { + "text": "Grants", + "url": "https://example.com" + } + ] + } + ] \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json b/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json new file mode 100644 index 0000000000..15b31a5557 --- /dev/null +++ b/deploy/tools/envs-validator/test/assets/configs/marketplace_categories.json @@ -0,0 +1,5 @@ +[ + "Swaps", + "Bridges", + "NFT" +] diff --git a/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json b/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json new file mode 100644 index 0000000000..0b142b7a24 --- /dev/null +++ b/deploy/tools/envs-validator/test/assets/configs/marketplace_config.json @@ -0,0 +1,25 @@ +[ + { + "author": "Hop", + "id": "hop-exchange", + "title": "Hop", + "logo": "https://example.com/logo.svg", + "categories": ["Bridge"], + "shortDescription": "Hop is a scalable rollup-to-rollup general token bridge.", + "site": "https://example.com", + "description": "Hop is a scalable rollup-to-rollup general token bridge.", + "external": true, + "url": "https://example.com" + }, + { + "author": "Blockscout", + "id": "token-approval-tracker", + "title": "Token Approval Tracker", + "logo": "https://example.com/logo.svg", + "categories": ["Infra & Dev tooling"], + "shortDescription": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.", + "site": "https://example.com", + "description": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.", + "url": "https://example.com" + } + ] diff --git a/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json b/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json new file mode 100644 index 0000000000..cf0f481ae3 --- /dev/null +++ b/deploy/tools/envs-validator/test/assets/configs/marketplace_security_reports.json @@ -0,0 +1,1073 @@ +[ + { + "appName": "paraswap", + "doc": "https://developers.paraswap.network/smart-contracts", + "chainsData": { + "1": { + "overallInfo": { + "verifiedNumber": 4, + "totalContractsNumber": 4, + "solidityScanContractsNumber": 4, + "securityScore": 77.41749999999999, + "issueSeverityDistribution": { + "critical": 5, + "gas": 58, + "high": 9, + "informational": 27, + "low": 41, + "medium": 5 + } + }, + "contractsData": [ + { + "address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contractname": "AugustusSwapper", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 8, + "high": 4, + "informational": 7, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 180, + "scan_time_taken": 1, + "score": "3.61", + "score_v2": "72.22", + "threat_score": "73.68" + } + } + }, + { + "address": "0x216b4b4ba9f3e719726886d34a177484278bfcae", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x216b4b4ba9f3e719726886d34a177484278bfcae", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x216b4b4ba9f3e719726886d34a177484278bfcae", + "contractname": "TokenTransferProxy", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x216b4b4ba9f3e719726886d34a177484278bfcae/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 1, + "gas": 29, + "high": 5, + "informational": 14, + "low": 21, + "medium": 3 + }, + "lines_analyzed_count": 553, + "scan_time_taken": 1, + "score": "3.92", + "score_v2": "78.48", + "threat_score": "78.95" + } + } + }, + { + "address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xa68bEA62Dc4034A689AA0F58A76681433caCa663", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xa68bEA62Dc4034A689AA0F58A76681433caCa663", + "contractname": "AugustusRegistry", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xa68bEA62Dc4034A689AA0F58A76681433caCa663/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 3, + "high": 0, + "informational": 5, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 103, + "scan_time_taken": 0, + "score": "4.22", + "score_v2": "84.47", + "threat_score": "88.89" + } + } + }, + { + "address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7", + "contractname": "FeeClaimer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xeF13101C5bbD737cFb2bF00Bbd38c626AD6952F7/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 18, + "high": 0, + "informational": 1, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 149, + "scan_time_taken": 0, + "score": "3.72", + "score_v2": "74.50", + "threat_score": "94.74" + } + } + } + ] + }, + "10": { + "overallInfo": { + "verifiedNumber": 3, + "totalContractsNumber": 4, + "solidityScanContractsNumber": 3, + "securityScore": 75.44333333333333, + "issueSeverityDistribution": { + "critical": 4, + "gas": 29, + "high": 4, + "informational": 20, + "low": 20, + "medium": 2 + } + }, + "contractsData": [ + { + "address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", + "contractname": "AugustusSwapper", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 8, + "high": 4, + "informational": 7, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 180, + "scan_time_taken": 1, + "score": "3.61", + "score_v2": "72.22", + "threat_score": "73.68" + } + } + }, + { + "address": "0x216B4B4Ba9F3e719726886d34a177484278Bfcae", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x6e7bE86000dF697facF4396efD2aE2C322165dC3", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x6e7bE86000dF697facF4396efD2aE2C322165dC3", + "contractname": "AugustusRegistry", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x6e7bE86000dF697facF4396efD2aE2C322165dC3/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 3, + "high": 0, + "informational": 5, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 102, + "scan_time_taken": 0, + "score": "4.22", + "score_v2": "84.31", + "threat_score": "88.89" + } + } + }, + { + "address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xA7465CCD97899edcf11C56D2d26B49125674e45F", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xA7465CCD97899edcf11C56D2d26B49125674e45F", + "contractname": "FeeClaimer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xA7465CCD97899edcf11C56D2d26B49125674e45F/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 18, + "high": 0, + "informational": 8, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 149, + "scan_time_taken": 1, + "score": "3.49", + "score_v2": "69.80", + "threat_score": "94.74" + } + } + } + ] + }, + "8453": { + "overallInfo": { + "verifiedNumber": 1, + "totalContractsNumber": 4, + "solidityScanContractsNumber": 1, + "securityScore": 73.33, + "issueSeverityDistribution": { + "critical": 4, + "gas": 8, + "high": 4, + "informational": 5, + "low": 8, + "medium": 1 + } + }, + "contractsData": [ + { + "address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52", + "contractname": "AugustusSwapper", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x59C7C832e96D2568bea6db468C1aAdcbbDa08A52/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 8, + "high": 4, + "informational": 5, + "low": 8, + "medium": 1 + }, + "lines_analyzed_count": 180, + "scan_time_taken": 1, + "score": "3.67", + "score_v2": "73.33", + "threat_score": "73.68" + } + } + }, + { + "address": "0x93aAAe79a53759cD164340E4C8766E4Db5331cD7", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x7e31b336f9e8ba52ba3c4ac861b033ba90900bb3", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x9aaB4B24541af30fD72784ED98D8756ac0eFb3C7", + "isVerified": false, + "solidityScanReport": null + } + ] + } + } + }, + { + "appName": "mean-finance", + "doc": "https://docs.mean.finance/guides/smart-contract-registry", + "chainsData": { + "1": { + "overallInfo": { + "verifiedNumber": 4, + "totalContractsNumber": 6, + "solidityScanContractsNumber": 4, + "securityScore": 61.36750000000001, + "issueSeverityDistribution": { + "critical": 6, + "gas": 25, + "high": 1, + "informational": 10, + "low": 20, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923", + "contractname": "DCAPermissionsManager", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 2, + "gas": 22, + "high": 0, + "informational": 8, + "low": 11, + "medium": 3 + }, + "lines_analyzed_count": 314, + "scan_time_taken": 1, + "score": "3.87", + "score_v2": "77.39", + "threat_score": "88.89" + } + } + }, + { + "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contractname": "DCAHubPositionDescriptor", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 1, + "informational": 2, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 280, + "scan_time_taken": 1, + "score": "4.77", + "score_v2": "95.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 11, + "scan_time_taken": 0, + "score": "1.82", + "score_v2": "36.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 11, + "scan_time_taken": 0, + "score": "1.82", + "score_v2": "36.36", + "threat_score": "100.00" + } + } + } + ] + }, + "10": { + "overallInfo": { + "verifiedNumber": 5, + "totalContractsNumber": 6, + "solidityScanContractsNumber": 5, + "securityScore": 66.986, + "issueSeverityDistribution": { + "critical": 6, + "gas": 26, + "high": 1, + "informational": 10, + "low": 23, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contractname": "DCAHub", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 23, + "scan_time_taken": 0, + "score": "3.48", + "score_v2": "69.57", + "threat_score": "94.44" + } + } + }, + { + "address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923", + "contractname": "DCAPermissionsManager", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 2, + "gas": 22, + "high": 0, + "informational": 8, + "low": 11, + "medium": 3 + }, + "lines_analyzed_count": 314, + "scan_time_taken": 1, + "score": "3.87", + "score_v2": "77.39", + "threat_score": "88.89" + } + } + }, + { + "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 16, + "scan_time_taken": 0, + "score": "2.81", + "score_v2": "56.25", + "threat_score": "100.00" + } + } + }, + { + "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contractname": "DCAHubPositionDescriptor", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 1, + "informational": 2, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 280, + "scan_time_taken": 1, + "score": "4.77", + "score_v2": "95.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contract_chain": "optimism", + "contract_platform": "blockscout", + "contract_url": "https://optimism.blockscout.com/address/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9/blockscout/optimism?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 11, + "scan_time_taken": 0, + "score": "1.82", + "score_v2": "36.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "isVerified": false, + "solidityScanReport": null + } + ] + }, + "8453": { + "overallInfo": { + "verifiedNumber": 4, + "totalContractsNumber": 6, + "solidityScanContractsNumber": 4, + "securityScore": 74.88, + "issueSeverityDistribution": { + "critical": 6, + "gas": 25, + "high": 1, + "informational": 7, + "low": 20, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345", + "contractname": "DCAHub", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xA5AdC5484f9997fBF7D405b9AA62A7d88883C345/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 23, + "scan_time_taken": 0, + "score": "3.48", + "score_v2": "69.57", + "threat_score": "94.44" + } + } + }, + { + "address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x20bdAE1413659f47416f769a4B27044946bc9923", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0x20bdAE1413659f47416f769a4B27044946bc9923", + "contractname": "DCAPermissionsManager", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x20bdAE1413659f47416f769a4B27044946bc9923/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 2, + "gas": 22, + "high": 0, + "informational": 5, + "low": 11, + "medium": 3 + }, + "lines_analyzed_count": 314, + "scan_time_taken": 1, + "score": "3.92", + "score_v2": "78.34", + "threat_score": "88.89" + } + } + }, + { + "address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contract_chain": "base", + "contract_platform": "blockscout", + "contract_url": "https://base.blockscout.com/address/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE", + "contractname": "DCAHubCompanion", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xDf0dbc66f85979a1d54671c4D9e439F306Be27EE/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 0, + "informational": 0, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 16, + "scan_time_taken": 0, + "score": "2.81", + "score_v2": "56.25", + "threat_score": "100.00" + } + } + }, + { + "address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b", + "contractname": "DCAHubPositionDescriptor", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x4ACd4BC402bc8e6BA8aBDdcA639d8011ef0b8a4b/blockscout/base?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 1, + "high": 1, + "informational": 2, + "low": 3, + "medium": 0 + }, + "lines_analyzed_count": 280, + "scan_time_taken": 1, + "score": "4.77", + "score_v2": "95.36", + "threat_score": "100.00" + } + } + }, + { + "address": "0x49c590F6a2dfB0f809E82B9e2BF788C0Dd1c31f9", + "isVerified": false, + "solidityScanReport": null + }, + { + "address": "0x5ad2fED59E8DF461c6164c31B4267Efb7cBaF9C0", + "isVerified": false, + "solidityScanReport": null + } + ] + } + } + }, + { + "appName": "cow-swap", + "doc": "https://docs.cow.fi/cow-protocol/reference/contracts/core#deployments", + "chainsData": { + "1": { + "overallInfo": { + "verifiedNumber": 3, + "totalContractsNumber": 3, + "solidityScanContractsNumber": 3, + "securityScore": 87.60000000000001, + "issueSeverityDistribution": { + "critical": 4, + "gas": 18, + "high": 0, + "informational": 13, + "low": 14, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contractname": "GPv2Settlement", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 16, + "high": 0, + "informational": 7, + "low": 5, + "medium": 3 + }, + "lines_analyzed_count": 493, + "scan_time_taken": 1, + "score": "4.57", + "score_v2": "91.48", + "threat_score": "94.74" + } + } + }, + { + "address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contractname": "EIP173Proxy", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 0, + "high": 0, + "informational": 4, + "low": 5, + "medium": 0 + }, + "lines_analyzed_count": 94, + "scan_time_taken": 0, + "score": "4.26", + "score_v2": "85.11", + "threat_score": "88.89" + } + } + }, + { + "address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contractname": "GPv2VaultRelayer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/eth?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 2, + "high": 0, + "informational": 2, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 87, + "scan_time_taken": 0, + "score": "4.31", + "score_v2": "86.21", + "threat_score": "94.74" + } + } + } + ] + }, + "100": { + "overallInfo": { + "verifiedNumber": 3, + "totalContractsNumber": 3, + "solidityScanContractsNumber": 3, + "securityScore": 87.60000000000001, + "issueSeverityDistribution": { + "critical": 4, + "gas": 18, + "high": 0, + "informational": 13, + "low": 14, + "medium": 3 + } + }, + "contractsData": [ + { + "address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contract_chain": "gnosis", + "contract_platform": "blockscout", + "contract_url": "https://gnosis.blockscout.com/address/0x9008D19f58AAbD9eD0D60971565AA8510560ab41", + "contractname": "GPv2Settlement", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x9008D19f58AAbD9eD0D60971565AA8510560ab41/blockscout/gnosis?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 16, + "high": 0, + "informational": 7, + "low": 5, + "medium": 3 + }, + "lines_analyzed_count": 493, + "scan_time_taken": 1, + "score": "4.57", + "score_v2": "91.48", + "threat_score": "94.74" + } + } + }, + { + "address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE", + "contractname": "EIP173Proxy", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0x2c4c28DDBdAc9C5E7055b4C863b72eA0149D8aFE/blockscout/gnosis?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 0, + "high": 0, + "informational": 4, + "low": 5, + "medium": 0 + }, + "lines_analyzed_count": 94, + "scan_time_taken": 0, + "score": "4.26", + "score_v2": "85.11", + "threat_score": "88.89" + } + } + }, + { + "address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "isVerified": true, + "solidityScanReport": { + "connection_id": "", + "contract_address": "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contract_chain": "eth", + "contract_platform": "blockscout", + "contract_url": "https://eth.blockscout.com/address/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", + "contractname": "GPv2VaultRelayer", + "is_quick_scan": true, + "node_reference_id": null, + "request_type": "threat_scan", + "scanner_reference_url": "https://solidityscan.com/quickscan/0xC92E8bdf79f0507f65a392b0ab4667716BFE0110/blockscout/gnosis?ref=blockscout", + "scan_status": "scan_done", + "scan_summary": { + "issue_severity_distribution": { + "critical": 0, + "gas": 2, + "high": 0, + "informational": 2, + "low": 4, + "medium": 0 + }, + "lines_analyzed_count": 87, + "scan_time_taken": 0, + "score": "4.31", + "score_v2": "86.21", + "threat_score": "94.74" + } + } + } + ] + } + } + } +] diff --git a/deploy/tools/envs-validator/tsconfig.json b/deploy/tools/envs-validator/tsconfig.json new file mode 100644 index 0000000000..a169faac5c --- /dev/null +++ b/deploy/tools/envs-validator/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "target": "es2016", + "module": "CommonJS", + "paths": { + "nextjs-routes": ["./nextjs/nextjs-routes.d.ts"], + } + }, + "include": [ + "../../../types/**/*.ts", + "../../../configs/app/**/*.ts", + "../../../global.d.ts", + "./index.ts", + "./schema.ts" + ], + "tsc-alias": { + "verbose": true, + "resolveFullPaths": true, + } +} + \ No newline at end of file diff --git a/deploy/tools/envs-validator/webpack.config.js b/deploy/tools/envs-validator/webpack.config.js new file mode 100644 index 0000000000..b62c19d076 --- /dev/null +++ b/deploy/tools/envs-validator/webpack.config.js @@ -0,0 +1,25 @@ +const path = require('path'); +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); + +module.exports = { + mode: 'production', + target: 'node', + entry: path.resolve(__dirname) + '/index.ts', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js' ], + plugins: [ new TsconfigPathsPlugin({ configFile: './tsconfig.json' }) ], + }, + output: { + filename: 'index.js', + path: path.resolve(__dirname), + }, +}; diff --git a/deploy/tools/envs-validator/yarn.lock b/deploy/tools/envs-validator/yarn.lock new file mode 100644 index 0000000000..6ea4de04cf --- /dev/null +++ b/deploy/tools/envs-validator/yarn.lock @@ -0,0 +1,980 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.44.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.0.tgz#55818eabb376e2272f77fbf5c96c43137c3c1e53" + integrity sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/node@*": + version "20.4.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" + integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw== + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +braces@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.14.5: + version "4.21.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" + integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== + dependencies: + caniuse-lite "^1.0.30001503" + electron-to-chromium "^1.4.431" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +caniuse-lite@^1.0.30001503: + version "1.0.30001517" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8" + integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +dotenv-cli@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.2.1.tgz#e595afd9ebfb721df9da809a435b9aa966c92062" + integrity sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@^16.0.0: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + +electron-to-chromium@^1.4.431: + version "1.4.467" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.467.tgz#b0660bf644baff7eedea33b8c742fb53ec60e3c2" + integrity sha512-2qI70O+rR4poYeF2grcuS/bCps5KJh6y1jtZMDDEteyKJQrzLOEhFyXCLcHW6DTBjKjWkk26JhWoAi+Ux9A0fg== + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.10.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" + integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== + +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-core-module@^2.11.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.12: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +property-expr@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" + integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@^7.3.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.19.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" + integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + +ts-loader@^9.4.4: + version "9.4.4" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.4.tgz#6ceaf4d58dcc6979f84125335904920884b7cee4" + integrity sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +tsconfig-paths-webpack-plugin@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" + integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.1.2" + +tsconfig-paths@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-merge@^5.7.3: + version "5.9.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" + integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.88.2: + version "5.88.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" + integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yup@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.2.0.tgz#9e51af0c63bdfc9be0fdc6c10aa0710899d8aff6" + integrity sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0" diff --git a/deploy/tools/favicon-generator/.gitignore b/deploy/tools/favicon-generator/.gitignore new file mode 100755 index 0000000000..09af2dfe51 --- /dev/null +++ b/deploy/tools/favicon-generator/.gitignore @@ -0,0 +1,4 @@ +/output +config.json +response.json +favicon_package** \ No newline at end of file diff --git a/deploy/tools/favicon-generator/config.template.json b/deploy/tools/favicon-generator/config.template.json new file mode 100755 index 0000000000..4d032e8bb9 --- /dev/null +++ b/deploy/tools/favicon-generator/config.template.json @@ -0,0 +1,41 @@ +{ + "favicon_generation": { + "api_key": "", + "master_picture": { + "type": "url", + "url": "" + }, + "files_location": { + "type": "path", + "path": "/favicons" + }, + "favicon_design": { + "desktop_browser": {}, + "ios": { + "picture_aspect": "no_change", + "assets": { + "ios6_and_prior_icons": false, + "ios7_and_later_icons": true, + "precomposed_icons": false, + "declare_only_default_icon": true + } + }, + "safari_pinned_tab": { + "picture_aspect": "black_and_white", + "threshold": 60 + } + }, + "settings": { + "compression": "3", + "scaling_algorithm": "Mitchell", + "error_on_image_too_small": true, + "readme_file": false, + "html_code_file": false, + "use_path_as_is": false + }, + "versioning": { + "param_name": "ver", + "param_value": "15Zd8" + } + } +} \ No newline at end of file diff --git a/deploy/tools/favicon-generator/script.sh b/deploy/tools/favicon-generator/script.sh new file mode 100755 index 0000000000..69145399ed --- /dev/null +++ b/deploy/tools/favicon-generator/script.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +echo "🌀 Generating favicons bundle..." + +# Check if MASTER_URL is provided +if [ -z "$MASTER_URL" ]; then + echo "🛑 Error: MASTER_URL variable is not provided." + exit 1 +fi + +# Check if FAVICON_GENERATOR_API_KEY is provided +if [ -z "$FAVICON_GENERATOR_API_KEY" ]; then + echo "🛑 Error: FAVICON_GENERATOR_API_KEY variable is not provided." + exit 1 +fi + +# Mask the FAVICON_GENERATOR_API_KEY to display only the first 8 characters +API_KEY_MASKED="${FAVICON_GENERATOR_API_KEY:0:8}***" +echo "🆗 The following variables are provided:" +echo " MASTER_URL: $MASTER_URL" +echo " FAVICON_GENERATOR_API_KEY: $API_KEY_MASKED" +echo + +# RealFaviconGenerator API endpoint URL +API_URL="https://realfavicongenerator.net/api/favicon" + +# Target folder for the downloaded assets +TARGET_FOLDER="./output" + +# Path to the config JSON template file +CONFIG_TEMPLATE_FILE="config.template.json" + +# Path to the generated config JSON file +CONFIG_FILE="config.json" + +# Replace and placeholders in the JSON template file +API_KEY_VALUE="$FAVICON_GENERATOR_API_KEY" +sed -e "s||$API_KEY_VALUE|" -e "s||$MASTER_URL|" "$CONFIG_TEMPLATE_FILE" > "$CONFIG_FILE" + +# Make the API POST request with JSON data from the config file +echo "⏳ Making request to API..." +API_RESPONSE=$(curl -s -X POST -H "Content-Type: application/json" -d @"$CONFIG_FILE" "$API_URL") + +# Create the response.json file with the API response +echo "$API_RESPONSE" > response.json + +# Check if the API response is valid JSON and contains success status +if ! jq -e '.favicon_generation_result.result.status == "success"' <<< "$API_RESPONSE" >/dev/null; then + echo "🛑 Error: API response does not contain the expected structure or has an error status." + ERROR_MESSAGE=$(echo "$API_RESPONSE" | jq -r '.favicon_generation_result.result.error_message' | tr -d '\\') + if [ -n "$ERROR_MESSAGE" ]; then + echo "🛑 $ERROR_MESSAGE" + fi + exit 1 +fi +echo "🆗 API responded with success status." + +# Parse the JSON response to extract the file URL and remove backslashes +FILE_URL=$(echo "$API_RESPONSE" | jq -r '.favicon_generation_result.favicon.package_url' | tr -d '\\') +PREVIEW_URL=$(echo "$API_RESPONSE" | jq -r '.favicon_generation_result.preview_picture_url' | tr -d '\\') + +# Check if FILE_URL is empty +if [ -z "$FILE_URL" ]; then + echo "🛑 File URL not found in JSON response." + exit 1 +fi + +echo "🆗 Found following file URL in the response: $FILE_URL" +echo "🆗 Favicon preview URL: $PREVIEW_URL" +echo + +# Generate a filename based on the URL +FILE_NAME=$(basename "$FILE_URL") + +# Check if the target folder exists and clear its contents if it does +if [ -d "$TARGET_FOLDER" ]; then + rm -r "$TARGET_FOLDER" +fi +mkdir -p "$TARGET_FOLDER" + +# Download the file +echo "⏳ Trying to download the file..." +curl -s -L "$FILE_URL" -o "$FILE_NAME" + +# Check if the download was successful +if [ $? -eq 0 ]; then + echo "🆗 File downloaded successfully." + echo +else + echo "🛑 Error: Failed to download the file." + exit 1 +fi + +# Unzip the downloaded file to the target folder +echo "⏳ Unzipping the file..." +unzip -q "$FILE_NAME" -d "$TARGET_FOLDER" + +# Check if the unzip operation was successful +if [ $? -eq 0 ]; then + echo "🆗 File unzipped successfully." + echo +else + echo "🛑 Failed to unzip the file." + exit 1 +fi + +# Clean up - remove the JSON response file and temporary JSON config file +rm response.json "$CONFIG_FILE" + +echo "✅ Done." \ No newline at end of file diff --git a/deploy/tools/feature-reporter/.gitignore b/deploy/tools/feature-reporter/.gitignore new file mode 100644 index 0000000000..cefc90f67f --- /dev/null +++ b/deploy/tools/feature-reporter/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/build +index.js \ No newline at end of file diff --git a/deploy/tools/feature-reporter/dev.sh b/deploy/tools/feature-reporter/dev.sh new file mode 100755 index 0000000000..183698a810 --- /dev/null +++ b/deploy/tools/feature-reporter/dev.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +rm -rf ./build +yarn compile_config +yarn build +dotenv -e ../../../configs/envs/.env.main -e ../../../configs/envs/.env.secrets yarn print_report \ No newline at end of file diff --git a/deploy/tools/feature-reporter/entry.js b/deploy/tools/feature-reporter/entry.js new file mode 100644 index 0000000000..01a2989035 --- /dev/null +++ b/deploy/tools/feature-reporter/entry.js @@ -0,0 +1,23 @@ +/* eslint-disable no-console */ +const config = require('./build/configs/app').default; + +run(); + +async function run() { + console.log(); + try { + console.log(`📋 Here is the list of the features enabled for the running instance. +To adjust their configuration, please refer to the documentation - https://github.com/blockscout/frontend/blob/main/docs/ENVS.md#app-features + `); + Object.entries(config.features) + .forEach(([ , feature ]) => { + const mark = feature.isEnabled ? 'v' : ' '; + console.log(` [${ mark }] ${ feature.title }`); + }); + + } catch (error) { + console.log('🚨 An error occurred while generating the feature report.'); + process.exit(1); + } + console.log(); +} diff --git a/deploy/tools/feature-reporter/package.json b/deploy/tools/feature-reporter/package.json new file mode 100644 index 0000000000..1368868318 --- /dev/null +++ b/deploy/tools/feature-reporter/package.json @@ -0,0 +1,22 @@ +{ + "name": "feature-reporter", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "compile_config": "yarn tsc -p ./tsconfig.json && yarn tsc-alias -p ./tsconfig.json", + "build": "yarn webpack-cli -c ./webpack.config.js", + "print_report": "node ./index.js", + "dev": "./dev.sh" + }, + "dependencies": { + "tsc": "^2.0.4", + "tsc-alias": "^1.8.7", + "typescript": "5.1", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "devDependencies": { + "dotenv-cli": "^7.2.1" + } +} diff --git a/deploy/tools/feature-reporter/tsconfig.json b/deploy/tools/feature-reporter/tsconfig.json new file mode 100644 index 0000000000..1fefb24f4c --- /dev/null +++ b/deploy/tools/feature-reporter/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "module": "CommonJS", + "outDir": "./build", + "paths": { + "nextjs-routes": ["./nextjs/nextjs-routes.d.ts"], + } + }, + "include": [ "../../../configs/app/index.ts", "../../../global.d.ts" ], + "tsc-alias": { + "verbose": true, + "resolveFullPaths": true, + } +} diff --git a/deploy/tools/feature-reporter/webpack.config.js b/deploy/tools/feature-reporter/webpack.config.js new file mode 100644 index 0000000000..41363dfeef --- /dev/null +++ b/deploy/tools/feature-reporter/webpack.config.js @@ -0,0 +1,13 @@ +const path = require('path'); +module.exports = { + mode: 'production', + target: 'node', + entry: path.resolve(__dirname, '/entry.js'), + resolve: { + extensions: [ '.js' ], + }, + output: { + filename: 'index.js', + path: path.resolve(__dirname), + }, +}; diff --git a/deploy/tools/feature-reporter/yarn.lock b/deploy/tools/feature-reporter/yarn.lock new file mode 100644 index 0000000000..502e97507e --- /dev/null +++ b/deploy/tools/feature-reporter/yarn.lock @@ -0,0 +1,1069 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.44.2" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.2.tgz#0d21c505f98a89b8dd4d37fa162b09da6089199a" + integrity sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + +"@types/json-schema@*", "@types/json-schema@^7.0.8": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/node@*": + version "20.4.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" + integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +braces@^3.0.2, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.14.5: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +caniuse-lite@^1.0.30001517: + version "1.0.30001519" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601" + integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg== + +chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^9.0.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dotenv-cli@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.2.1.tgz#e595afd9ebfb721df9da809a435b9aa966c92062" + integrity sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@^16.0.0: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + +electron-to-chromium@^1.4.477: + version "1.4.487" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.487.tgz#e2ef8b15f2791bf68fa6f38f2656f1a551d360ae" + integrity sha512-XbCRs/34l31np/p33m+5tdBrdXu9jJkZxSbNxj5I0H1KtV2ZMSB+i/HYqDiRzHaFx2T5EdytjoBRe8QRJE2vQg== + +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.10.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13" + integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== + +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +globby@^11.0.4: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mylas@^2.1.9: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4" + integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +plimit-lit@^1.2.6: + version "1.5.0" + resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-1.5.0.tgz#f66df8a7041de1e965c4f1c0697ab486968a92a5" + integrity sha512-Eb/MqCb1Iv/ok4m1FqIXqvUKPISufcjZ605hl3KM/n8GaX8zfhtgdLwZU3vKjuHGh2O9Rjog/bHTq8ofIShdng== + dependencies: + queue-lit "^1.5.0" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +queue-lit@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/queue-lit/-/queue-lit-1.5.0.tgz#8197fdafda1edd615c8a0fc14c48353626e5160a" + integrity sha512-IslToJ4eiCEE9xwMzq3viOO5nH8sUWUCwoElrhNMozzr9IIt2qqvB4I+uHu/zJTQVqc9R5DFwok4ijNK1pU3fA== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^5.16.8: + version "5.19.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" + integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tsc-alias@^1.8.7: + version "1.8.7" + resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.7.tgz#4f8721b031a31345fa9f1fa8d3cf209d925abb88" + integrity sha512-59Q/zUQa3miTf99mLbSqaW0hi1jt4WoG8Uhe5hSZJHQpSoFW9eEwvW7jlKMHXWvT+zrzy3SN9PE/YBhQ+WVydA== + dependencies: + chokidar "^3.5.3" + commander "^9.0.0" + globby "^11.0.4" + mylas "^2.1.9" + normalize-path "^3.0.0" + plimit-lit "^1.2.6" + +tsc@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/tsc/-/tsc-2.0.4.tgz#5f6499146abea5dca4420b451fa4f2f9345238f5" + integrity sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q== + +typescript@5.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-merge@^5.7.3: + version "5.9.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" + integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.88.2: + version "5.88.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" + integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl new file mode 100644 index 0000000000..a30300e8a1 --- /dev/null +++ b/deploy/values/review-l2/values.yaml.gotmpl @@ -0,0 +1,83 @@ +fullNameOverride: bs-stack +nameOverride: bs-stack +imagePullSecrets: + - name: regcred +config: + network: + id: 420 + name: "Base" + shortname: Base + currency: + name: Ether + symbol: ETH + decimals: 18 + account: + enabled: true + testnet: true +blockscout: + enabled: false +stats: + enabled: false +frontend: + enabled: true + replicaCount: 1 + image: + tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + pullPolicy: Always + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: internal-and-public + nginx.ingress.kubernetes.io/proxy-body-size: 500m + nginx.ingress.kubernetes.io/client-max-body-size: "500M" + nginx.ingress.kubernetes.io/proxy-buffering: "on" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-send-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "8" + cert-manager.io/cluster-issuer: "zerossl-prod" + hostname: review-l2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com + + resources: + limits: + memory: 768Mi + cpu: "1" + requests: + memory: 384Mi + cpu: 250m + env: + NEXT_PUBLIC_APP_ENV: development + NEXT_PUBLIC_APP_INSTANCE: review_L2 + NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation + NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg + NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg + NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json + NEXT_PUBLIC_API_HOST: base.blockscout.com + NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout + NEXT_PUBLIC_STATS_API_HOST: https://stats-l2-base-mainnet.k8s-prod-1.blockscout.com + NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)" + NEXT_PUBLIC_NETWORK_RPC_URL: https://mainnet.base.org + NEXT_PUBLIC_WEB3_WALLETS: "['coinbase']" + NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: true + NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs']" + NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer.services.blockscout.com + NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info.services.blockscout.com + NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs.services.blockscout.com + NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens.services.blockscout.com + NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata.services.blockscout.com + NEXT_PUBLIC_ROLLUP_TYPE: optimistic + NEXT_PUBLIC_ROLLUP_L1_BASE_URL: https://eth.blockscout.com + NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62 + NEXT_PUBLIC_USE_NEXT_JS_PROXY: true + NEXT_PUBLIC_NAVIGATION_LAYOUT: horizontal + NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/blocks','/name-domains']" + envFromSecret: + NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN + SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI + NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID + NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY + NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID + FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY + NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl new file mode 100644 index 0000000000..c37f4ee6c6 --- /dev/null +++ b/deploy/values/review/values.yaml.gotmpl @@ -0,0 +1,99 @@ +fullNameOverride: bs-stack +nameOverride: bs-stack +imagePullSecrets: + - name: regcred +config: + network: + id: 11155111 + name: Blockscout + shortname: Blockscout + currency: + name: Ether + symbol: ETH + decimals: 18 + account: + enabled: true + testnet: true +blockscout: + enabled: false +stats: + enabled: false +frontend: + enabled: true + replicaCount: 1 + image: + tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + pullPolicy: Always + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: internal-and-public + nginx.ingress.kubernetes.io/proxy-body-size: 500m + nginx.ingress.kubernetes.io/client-max-body-size: "500M" + nginx.ingress.kubernetes.io/proxy-buffering: "on" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-send-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "15m" + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "8" + cert-manager.io/cluster-issuer: "zerossl-prod" + hostname: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }}.k8s-dev.blockscout.com + + resources: + limits: + memory: 768Mi + cpu: "1" + requests: + memory: 384Mi + cpu: 250m + env: + NEXT_PUBLIC_APP_ENV: development + NEXT_PUBLIC_APP_INSTANCE: review + NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation + NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-sepolia.json + NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg + NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png + NEXT_PUBLIC_API_HOST: eth-sepolia.k8s-dev.blockscout.com + NEXT_PUBLIC_STATS_API_HOST: https://stats-sepolia.k8s-dev.blockscout.com/ + NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ + NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com + NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com + NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com + NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata-test.k8s-dev.blockscout.com + NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com + NEXT_PUBLIC_MARKETPLACE_ENABLED: true + NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C + NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form + NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json + NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout + NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" + NEXT_PUBLIC_NETWORK_RPC_URL: https://eth-sepolia.public.blastapi.io + NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]" + NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json + NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: zkbob-wallet + NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL: https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html + NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL: https://www.basename.app + NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d + NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" + NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar + NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: true + NEXT_PUBLIC_USE_NEXT_JS_PROXY: true + NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" + NEXT_PUBLIC_HAS_USER_OPS: true + NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" + NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: blockscout + NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true + NEXT_PUBLIC_AD_BANNER_PROVIDER: slise + NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true + NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']" + envFromSecret: + NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN + SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI + NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID + NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY + NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID + FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY + NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY + NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN + NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY: ref+vault://deployment-values/blockscout/dev/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY diff --git a/docs/BUILD-TIME_ENVS.md b/docs/BUILD-TIME_ENVS.md new file mode 100644 index 0000000000..7f0db07fc2 --- /dev/null +++ b/docs/BUILD-TIME_ENVS.md @@ -0,0 +1,9 @@ +# Build-time environment variables + +These variables are passed to the app during the image build process. They cannot be re-defined during the run-time. + +| Variable | Type | Description | Optional | Example value | +| --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GIT_COMMIT_SHA | `string` | SHA of the latest commit in the branch from which image is built | false | `29d0613e` | +| NEXT_PUBLIC_GIT_TAG | `string` | Git tag on the latest commit in the branch from which image is built | true | `v1.0.0` | +| NEXT_OPEN_TELEMETRY_ENABLED | `boolean` | Enables OpenTelemetry SDK | true | `true` | diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..0284e6e3cf --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,207 @@ +# Contribution guide + +Thanks for showing interest to contribute to Blockscout. The following steps will get you up and running. + +  + +## Project setup + +1. Fork the repo by clicking Fork button at the top of the repo main page and name it appropriately + +2. Clone your fork locally + ```sh + git clone https://github.com//.git + cd + ``` + +3. Make sure you're running Node.js 20+ and NPM 10+; if not, upgrade it accordingly, for example using [nvm](https://github.com/nvm-sh/nvm). + ```sh + node -v + npm -v + ``` + +4. Install dependencies + ```sh + yarn + ``` + +  + +## Toolkit + +We are using following technology stack in the project +- [Yarn](https://yarnpkg.com/) as package manager +- [ReactJS](https://reactjs.org/) as UI library +- [Next.js](https://nextjs.org/) as application framework +- [Chakra](https://chakra-ui.com/) as component library; our theme customization can be found in `/theme` folder +- [TanStack Query](https://tanstack.com/query/v4/docs/react/overview/) for fetching, caching and updating data from the API +- [Jest](https://jestjs.io/) as JavaScript testing framework +- [Playwright](https://playwright.dev/) as a tool for components visual testing + +And of course our premier language is [Typescript](https://www.typescriptlang.org/). + +  + +## Local development + +To develop locally, follow one of the two paths outlined below: + +A. Custom configuration: + +1. Create `.env.local` file in the root folder and include all required environment variables from the [list](./ENVS.md) +2. Optionally, clone `.env.example` and name it `.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need. +3. Use `yarn dev` command to start the dev server. +4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`). + +B. Pre-defined configuration: + +1. Optionally, clone `.env.example` file into `configs/envs/.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need. +2. Choose one of the predefined configurations located in the `/configs/envs` folder. +3. Start your local dev server using the `yarn dev:preset ` command. +4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`). + + +  + +## Adding new dependencies +For all types of dependencies: +- **Do not add** a dependency if the desired functionality is easily implementable +- If adding a dependency is necessary, please be sure that is is well-maintained and trustworthy + +  + +## Adding new ENV variable + +*Note*, if the variable should be exposed to the browser don't forget to add prefix `NEXT_PUBLIC_` to its name. + +These are the steps that you have to follow to make everything work: +1. First and foremost, document variable in the [/docs/ENVS.md](./ENVS.md) file; provide short description, its expected type, requirement flag, default and example value; **do not skip this step** otherwise the app will not receive variable value at run-time +2. Make sure that you have added a property to React app config (`configs/app/index.ts`) in appropriate section that is associated with this variable; do not use ENV variable values directly in the application code; decide where this variable belongs to and place it under the certain section: + - `app` - the front-end app itself + - `api` - the main API configuration + - `chain` - the Blockchain parameters + - `UI` - the app UI customization + - `meta` - SEO and meta-tags customization + - `features` - the particular feature of the app + - `services` - some 3rd party service integration which is not related to one particular feature +3. If a new variable is meant to store the URL of an external API service, remember to include its value in the Content-Security-Policy document header. Refer to `nextjs/csp/policies/app.ts` for details. +4. For local development purposes add the variable with its appropriate values to pre-defined ENV configs `configs/envs` where it is needed +5. Add the variable to CI configs where it is needed + - `deploy/values/review/values.yaml.gotmpl` - review development environment + - `deploy/values/review-l2/values.yaml.gotmpl` - review development environment for L2 networks +6. If your variable is meant to receive a link to some external resource (image or JSON-config file), extend the array `ASSETS_ENVS` in `deploy/scripts/download_assets.sh` with your variable name +7. Add validation schema for the new variable into the file `deploy/tools/envs-validator/schema.ts` +8. Check if modified validation schema is valid by doing the following steps: + - change your current directory to `deploy/tools/envs-validator` + - install deps with `yarn` command + - add your variable into `./test/.env.base` test preset or create a new test preset if needed + - if your variable contains a link to the external JSON config file: + - add example of file content into `./test/assets` directory; the file name should be constructed by stripping away prefix `NEXT_PUBLIC_` and postfix `_URL` if any, and converting the remaining string to lowercase (for example, `NEXT_PUBLIC_MARKETPLACE_CONFIG_URL` will become `marketplace_config.json`) + - in the main script `index.ts` extend array `envsWithJsonConfig` with your variable name + - run `yarn test` command to see the validation result +9. Don't forget to mention in the PR notes that new ENV variable was added + +  + +## Writing & Running Tests + +Every feature or bugfix should be accompanied by tests, either unit tests or component visual tests, or both, except from trivial fixes (for example, typo fix). All commands for running tests you can find [below](./CONTRIBUTING.md#command-list). + +### Jest unit tests + +If your changes only related to the logic of the app and not to its visual presentation, then try to write unit tests using [Jest](https://jestjs.io/) framework and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). In general these tests are "cheaper" and faster than Playwright ones. Use them for testing your utilities and React hooks, as well as the whole components logic. + +Place your test suites in `.test.ts` or `.test.tsx` files. You can find or add some mocks or other helpful utilities for these tests purposes in the `/jest` folder. + +*Note*, that we are using custom renderer and wrapper in all test for React components, so please do not import package `@testing-library/react` directly in your test suites, instead use imports from `jest/lib` utility. + +### Playwright components tests + +For changes associated with the UI itself write components visual tests using [Playwright](https://playwright.dev/) framework and its *experimental* [Components test library](https://playwright.dev/docs/test-components). Please be aware of known [issues and limitations](https://playwright.dev/docs/test-components#known-issues-and-limitations) of this library. + +Your tests files should have `.pw.tsx` extension. All configs, mocks, fixtures and other utilities for these tests live in `/playwright` folder. + +We have 3 pre-configured projects. You can run your test with the desired project by simply adding its [tag](https://playwright.dev/docs/test-annotations#tag-tests) to the test name: +- `default` - default project for all test, uses desktop Chrome desktop device; don't need to specify its tag, instead use `-@default` tag to skip test run with this project +- `mobile` - project for testing on mobile devices, uses Safari mobile browser; add tag `+@mobile` to run test with this project +- `dark-color-mode` - project for testing app in the dark color mode, uses desktop Chrome desktop device with forced dark color mode; add tag `+@dark-mode` to run test with this project. + +*Note* that, since we are developing not on the same operating system as our CI system, we have to use Docker to generate or update the screenshots. In order to do that use `yarn test:pw:docker --update-snapshots` command. Please **do not commit** any screenshots generated via `yarn test:pw:local` command, their associated tests will fail in the CI run. + +  + +## Making a Pull Request + +### Steps to PR + +1. Make sure that you fork and clone repo; check if the main branch has all recent changes from the original repo + + > Tip: Keep your `main` branch pointing at the original repository and make pull + > requests from branches on your fork. To do this, run: + > + > ``` + > git remote add upstream https://github.com/blockscout/frontend.git + > git fetch upstream + > git branch --set-upstream-to=upstream/main main + > ``` + > + > This will add the original repository as a "remote" called "upstream," Then + > fetch the git information from that remote, then set your local `main` branch + > to use the upstream main branch whenever you run `git pull`. Then you can make + > all of your pull request branches based on this `main` branch. Whenever you + > want to update your version of `main`, do a regular `git pull`. + +2. Create a branch for your PR with `git checkout -b `; we do not follow any branch name convention just yet +3. Commit your changes. Commits should be one logical change that still allows all tests to pass. Prefer smaller commits if there could be two levels of logic grouping. The goal is to allow contributors in the future (including your future self) to determine your reasoning for making changes and to allow them to cherry-pick, patch or port those changes in isolation to other branches or forks. Again, there is no strict commit message convention, but make sure that it clear and fully describes all changes that were made +4. If during your PR you reveal a pre-existing bug, try to isolate the bug and fix it on an independent branch and PR it first +5. Where possible, please provide unit tests that demonstrate new functionality or bug fix is working + +### Opening PR and getting it accepted + +1. Push your changes and create a Pull Request. If you are still working on the task, please use "Draft Pull Request" option, so we know that it is not ready yet. In addition, you can add label "skip checks" to your PR, so all CI checks will not be triggered. +2. Once you finish your work, remove label "skip checks" from PR, if it was added before, and publish PR if it was in the draft state +3. Make sure that all code checks and tests are successfully passed +4. Add description to your Pull Request and link an existing issue(s) that it is fixing +5. Request review from one or all core team members: @tom2drum, @isstuev. Our core team is committed to reviewing patches in a timely manner. +6. After code review is done, we merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface. + +*Note*, if you Pull Request contains any changes that are not backwards compatible with the previous versions of the app, please specify them in PR description and add label ["breaking changes"](https://github.com/blockscout/frontend/labels/breaking%20changes) to it. + +  + +## Commands list + +| Command | Description | +| --- | --- | +| **Running and building** | +| `yarn dev` | run local dev server with user's configuration | +| `yarn dev:preset ` | run local dev server with predefined configuration | +| `yarn build:docker` | build a docker image locally | +| `yarn start:docker:local` | start an application from previously built local docker image with user's configuration | +| `yarn start:docker:preset ` | start an application from previously built local docker image with predefined configuration | +| **Linting and formatting** | +| `yarn lint:eslint` | lint project files with ESLint | +| `yarn lint:eslint:fix` | lint project files with ESLint and automatically fix problems | +| `yarn lint:tsc` | compile project typescript files using TypeScript Compiler | +| `yarn svg:format` | format and optimize SVG icons in the `/icons` folder using SVGO tool | +| `yarn svg:build-sprite` | build SVG icons sprite | +| **Testing** | +| `yarn test:jest` | run all Jest unit tests | +| `yarn test:jest:watch` | run all Jest unit tests in watch mode | +| `yarn test:pw:local` | run Playwright component tests locally | +| `yarn test:pw:docker` | run Playwright component tests in docker container | +| `yarn test:pw:ci` | run Playwright component tests in CI | + +  + +## Tips & Tricks + +### Code Editor + +#### VSCode + +There are some predefined tasks for all commands described above. You can see its full list by pressing cmd + shift + P and using command `Task: Run task` + +Also there is a Jest test launch configuration for debugging and running current test file in the watch mode. + +And you may find the Dev Container setup useful too. diff --git a/docs/CUSTOM_BUILD.md b/docs/CUSTOM_BUILD.md new file mode 100644 index 0000000000..9142e12846 --- /dev/null +++ b/docs/CUSTOM_BUILD.md @@ -0,0 +1,10 @@ +# Building and running your own docker image + +You are free to clone the repo and make any changes to the application code that you want, adding your own customization and features. After that you can build a docker image by running `yarn build:docker` or alternatively run `docker build` and pass your own args that is necessary. + +For running app container from freshly built image do +```sh +docker run -p 3000:3000 --env-file +``` + +*Disclaimer* Do not try to generate production build of the app on your local machine (outside the docker). The app will not work as you would expect. diff --git a/docs/DEPRECATED_ENVS.md b/docs/DEPRECATED_ENVS.md new file mode 100644 index 0000000000..0cf27ae106 --- /dev/null +++ b/docs/DEPRECATED_ENVS.md @@ -0,0 +1,12 @@ +# Deprecated environment variables + +| Variable | Type | Description | Compulsoriness | Default value | Example value | Introduced in version | Deprecated in version | Comment | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY | `string` | RealFaviconGenerator [API key](https://realfavicongenerator.net/api/) | Required | - | `` | v1.11.0 | v1.16.0 | Replaced FAVICON_GENERATOR_API_KEY | +| NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK | `boolean` | Set to true for optimistic L2 solutions | Required | - | `true` | v1.17.0 | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE | +| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.17.0 | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE | +| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.17.0 | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | +| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | - | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL | +| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | - | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED | +| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | +| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | diff --git a/docs/ENVS.md b/docs/ENVS.md new file mode 100644 index 0000000000..830252673c --- /dev/null +++ b/docs/ENVS.md @@ -0,0 +1,745 @@ +# Run-time environment variables + +The app instance can be customized by passing the following variables to the Node.js environment at runtime. Some of these variables have been deprecated, and their full list can be found in the [file](./DEPRECATED_ENVS.md). + +**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted. If it contains a hash (`#`) or a dollar-sign (`$`) the whole value should be wrapped in single quotes as well (see `dotenv` [readme](https://github.com/bkeepers/dotenv#variable-substitution) for the reference) + +## Disclaimer about using variables + +Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will be exposed to the browser. So any user can obtain its values. Make sure that for all 3rd-party services keys (e.g., Sentri, Auth0, WalletConnect, etc.) in the services administration panel you have created a whitelist of allowed origins and have added your app domain into it. That will help you prevent using your key by unauthorized app, if someone gets its value. + +  + +## Table of contents +- [App configuration](ENVS.md#app-configuration) +- [Blockchain parameters](ENVS.md#blockchain-parameters) +- [API configuration](ENVS.md#api-configuration) +- [UI configuration](ENVS.md#ui-configuration) + - [Homepage](ENVS.md#homepage) + - [Navigation](ENVS.md#navigation) + - [Footer](ENVS.md#footer) + - [Favicon](ENVS.md#favicon) + - [Meta](ENVS.md#meta) + - [Views](ENVS.md#views) + - [Block](ENVS.md#block-views) + - [Address](ENVS.md#address-views) + - [Transaction](ENVS.md#transaction-views) + - [NFT](ENVS.md#nft-views) + - [Misc](ENVS.md#misc) +- [App features](ENVS.md#app-features) + - [My account](ENVS.md#my-account) + - [Gas tracker](ENVS.md#gas-tracker) + - [Address verification](ENVS.md#address-verification-in-my-account) in "My account" + - [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.) + - [Banner ads](ENVS.md#banner-ads) + - [Text ads](ENVS.md#text-ads) + - [Beacon chain](ENVS.md#beacon-chain) + - [User operations](ENVS.md#user-operations-erc-4337) + - [Rollup chain](ENVS.md#rollup-chain) + - [Export data to CSV file](ENVS.md#export-data-to-csv-file) + - [Google analytics](ENVS.md#google-analytics) + - [Mixpanel analytics](ENVS.md#mixpanel-analytics) + - [GrowthBook feature flagging and A/B testing](ENVS.md#growthbook-feature-flagging-and-ab-testing) + - [GraphQL API documentation](ENVS.md#graphql-api-documentation) + - [REST API documentation](ENVS.md#rest-api-documentation) + - [Marketplace](ENVS.md#marketplace) + - [Solidity to UML diagrams](ENVS.md#solidity-to-uml-diagrams) + - [Blockchain statistics](ENVS.md#blockchain-statistics) + - [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet) + - [Transaction interpretation](ENVS.md#transaction-interpretation) + - [Verified tokens info](ENVS.md#verified-tokens-info) + - [Name service integration](ENVS.md#name-service-integration) + - [Metadata service integration](ENVS.md#metadata-service-integration) + - [Public tag submission](ENVS.md#public-tag-submission) + - [Data availability](ENVS.md#data-availability) + - [Bridged tokens](ENVS.md#bridged-tokens) + - [Safe{Core} address tags](ENVS.md#safecore-address-tags) + - [SUAVE chain](ENVS.md#suave-chain) + - [MetaSuites extension](ENVS.md#metasuites-extension) + - [Validators list](ENVS.md#validators-list) + - [Sentry error monitoring](ENVS.md#sentry-error-monitoring) + - [OpenTelemetry](ENVS.md#opentelemetry) + - [Swap button](ENVS.md#defi-dropdown) + - [Multichain balance button](ENVS.md#multichain-balance-button) + - [Get gas button](ENVS.md#get-gas-button) +- [3rd party services configuration](ENVS.md#external-services-configuration) + +  + +## App configuration + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` | App url schema | - | `https` | `http` | v1.0.x+ | +| NEXT_PUBLIC_APP_HOST | `string` | App host | Required | - | `blockscout.com` | v1.0.x+ | +| NEXT_PUBLIC_APP_PORT | `number` | Port where app is running | - | `3000` | `3001` | v1.0.x+ | +| NEXT_PUBLIC_USE_NEXT_JS_PROXY | `boolean` | Tells the app to proxy all APIs request through the NextJS app. **We strongly advise not to use it in the production environment**, since it can lead to performance issues of the NodeJS server | - | `false` | `true` | v1.8.0+ | + +  + +## Blockchain parameters + +*Note!* The `NEXT_PUBLIC_NETWORK_CURRENCY` variables represent the blockchain's native token used for paying transaction fees. `NEXT_PUBLIC_NETWORK_SECONDARY_COIN` variables refer to tokens like protocol-specific tokens (e.g., OP token on Optimism chain) or governance tokens (e.g., GNO on Gnosis chain). + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` | Used for SEO attributes (e.g, page description) | - | - | `OoG` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference | - | - | `https://core.poa.network` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | v1.23.0+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | v1.29.0+ | +| NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES | `boolean` | Set to `true` for networks where users can pay transaction fees in either the native coin or ERC-20 tokens. | - | `false` | `true` | v1.33.0+ | +| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME | `string` | Name of the standard for creating tokens | - | `ERC` | `BEP` | v1.31.0+ | +| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | v1.0.x+ | + +  + +## API configuration + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` | v1.0.x+ | +| NEXT_PUBLIC_API_HOST | `string` | Main API host | Required | - | `blockscout.com` | v1.0.x+ | +| NEXT_PUBLIC_API_PORT | `number` | Port where API is running on the host | - | - | `3001` | v1.0.x+ | +| NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` | v1.0.x+ | +| NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` | v1.0.x+ | + +  + +## UI configuration + +### Homepage + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ | +| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | v1.0.x+ | +| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ | +| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | + +  + +### Navigation + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_NETWORK_LOGO | `string` | Network logo; if not provided, placeholder will be shown; *Note* the logo height should be 24px and width less than 120px | - | - | `https://placekitten.com/240/40` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_LOGO_DARK | `string` | Network logo for dark color mode; if not provided, **inverted** regular logo will be used instead | - | - | `https://placekitten.com/240/40` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_ICON | `string` | Network icon; used as a replacement for regular network logo when nav bar is collapsed; if not provided, placeholder will be shown; *Note* the icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_ICON_DARK | `string` | Network icon for dark color mode; if not provided, **inverted** regular icon will be used instead | - | - | `https://placekitten.com/60/60` | v1.0.x+ | +| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` | v1.0.x+ | +| NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | v1.0.x+ | +| NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` | v1.16.0+ | +| NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES | `Array` | List of menu item routes that should have a lightning label | - | - | `['/accounts']` | v1.31.0+ | +| NEXT_PUBLIC_NAVIGATION_LAYOUT | `vertical \| horizontal` | Navigation menu layout type | - | `vertical` | `horizontal` | v1.32.0+ | + +#### Featured network configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| title | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | +| url | `string` | Network explorer main page url | Required | - | `https://blockscout.com/xdai/mainnet` | +| group | `Mainnets \| Testnets \| Other` | Indicates in which tab network appears in the menu | Required | - | `Mainnets` | +| icon | `string` | Network icon; if not provided, the common placeholder will be shown; *Note* that icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` | +| isActive | `boolean` | Pass `true` if item should be shown as active in the menu | - | - | `true` | +| invertIconInDarkMode | `boolean` | Pass `true` if icon colors should be inverted in dark mode | - | - | `true` | + +  + +### Footer + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_FOOTER_LINKS | `string` | URL of configuration file (`.json` format only) which contains list of link groups to be displayed in the footer. See [below](#footer-links-configuration-properties) list of available properties for particular group | - | - | `https://example.com/footer_links_config.json` | v1.1.1+ | + +The app version shown in the footer is derived from build-time ENV variables `NEXT_PUBLIC_GIT_TAG` and `NEXT_PUBLIC_GIT_COMMIT_SHA` and cannot be overwritten at run-time. + +#### Footer links configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| title | `string` | Title of link group | Required | - | `Company` | +| links | `Array<{'text':string;'url':string;}>` | list of links | Required | - | `[{'text':'Homepage','url':'https://www.blockscout.com'}]` | + +  + +### Favicon + +By default, the app has generic favicon. You can override this behavior by providing the following variables. Hence, the favicon assets bundle will be generated at the container start time and will be used instead of default one. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| FAVICON_GENERATOR_API_KEY | `string` | RealFaviconGenerator [API key](https://realfavicongenerator.net/api/) | Required | - | `` | v1.16.0+ | +| FAVICON_MASTER_URL | `string` | - | - | `NEXT_PUBLIC_NETWORK_ICON` | `https://placekitten.com/180/180` | v1.11.0+ | + +  + +### Meta + +Settings for meta tags, OG tags and SEO + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE | `boolean` | Set to `true` to promote Blockscout in meta and OG titles | - | `true` | `true` | v1.12.0+ | +| NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` | v1.12.0+ | +| NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` | v1.12.0+ | +| NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` | v1.29.0+ | +| NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to pre-render page titles (e.g Token page) on the server side and inject page h1-tag to the markup before it is sent to the browser. | - | `false` | `true` | v1.30.0+ | + +  + +### Views + +#### Block views + +| Variable | Type | Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS | `Array` | Array of the block fields ids that should be hidden. See below the list of the possible id values. | - | - | `'["burnt_fees","total_reward"]'` | v1.10.0+ | + + +##### Block fields list +| Id | Description | +| --- | --- | +| `burnt_fees` | Burnt fees | +| `total_reward` | Total block reward | +| `nonce` | Block nonce | +| `miner` | Address of block's miner or validator | +| `L1_status` | Short interpretation of the batch lifecycle (applicable for Rollup chains) | +| `batch` | Batch index (applicable for Rollup chains) | + +  + +#### Address views + +| Variable | Type | Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Default style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | v1.12.0+ | +| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | v1.15.0+ | +| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | v1.19.0+ | +| NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS | `Array<'solidity-hardhat' \| 'solidity-foundry'>` | Pass an array of additional methods from which users can choose while verifying a smart contract. Both methods are available by default, pass `'none'` string to disable them all. | - | - | `['solidity-hardhat']` | v1.33.0+ | + +##### Address views list +| Id | Description | +| --- | --- | +| `top_accounts` | Top accounts | + +  + +#### Transaction views + +| Variable | Type | Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS | `Array` | Array of the transaction fields ids that should be hidden. See below the list of the possible id values. | - | - | `'["value","tx_fee"]'` | v1.15.0+ | +| NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS | `Array` | Array of the additional fields ids that should be added to the transaction details. See below the list of the possible id values. | - | - | `'["fee_per_gas"]'` | v1.15.0+ | + +##### Transaction fields list +| Id | Description | +| --- | --- | +| `value` | Sent value | +| `fee_currency` | Fee currency | +| `gas_price` | Price per unit of gas | +| `tx_fee` | Total transaction fee | +| `gas_fees` | Gas fees breakdown | +| `burnt_fees` | Amount of native coin burnt for transaction | +| `L1_status` | Short interpretation of the batch lifecycle (applicable for Rollup chains) | +| `batch` | Batch index (applicable for Rollup chains) | + +##### Transaction additional fields list +| Id | Description | +| --- | --- | +| `fee_per_gas` | Amount of total fee divided by total amount of gas used by transaction | + +  + +#### NFT views + +| Variable | Type | Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES | `Array` where `NftMarketplace` can have following [properties](#nft-marketplace-properties) | Used to build up links to NFT collections and NFT instances in external marketplaces. | - | - | `[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'}]` | v1.15.0+ | + + +##### NFT marketplace properties +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| name | `string` | Displayed name of the marketplace | Required | - | `OpenSea` | +| collection_url | `string` | URL template for NFT collection | Required | - | `https://opensea.io/assets/ethereum/{hash}` | +| instance_url | `string` | URL template for NFT instance | Required | - | `https://opensea.io/assets/ethereum/{hash}/{id}` | +| logo_url | `string` | URL of marketplace logo | Required | - | `https://opensea.io/static/images/logos/opensea-logo.svg` | + +*Note* URL templates should contain placeholders of NFT hash (`{hash}`) and NFT id (`{id}`). This placeholders will be substituted with particular values for every collection or instance. + +  + +### Misc + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | v1.0.x+ | +| NEXT_PUBLIC_CONTRACT_CODE_IDES | `Array` where `ContractCodeIde` can have following [properties](#contract-code-ide-configuration-properties) | Used to build up links to IDEs with contract source code. | - | - | `[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]` | v1.23.0+ | +| NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS | `boolean` | Set to `true` to enable Submit Audit form on the contract page | - | `false` | `true` | v1.25.0+ | +| NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` | v1.17.0+ | +| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ | +| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ | +| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ | + +#### Network explorer configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| logo | `string` | URL to explorer logo file. Should be at least 40x40. | - | - | `'https://foo.app/icon.png'` | +| title | `string` | Displayed name of the explorer | Required | - | `Anyblock` | +| baseUrl | `string` | Base url of the explorer | Required | - | `https://explorer.anyblock.tools` | +| paths | `Record<'tx' \| 'block' \| 'address' \| 'token', string>` | Map of explorer entities and their paths | Required | - | `{'tx':'/ethereum/poa/core/tx'}` | + +*Note* The url of an entity will be constructed as `]>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/` + +#### Contract code IDE configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| title | `string` | Displayed name of the IDE | Required | - | `Remix IDE` | +| url | `string` | URL of the IDE with placeholders for contract hash (`{hash}`) and current domain (`{domain}`) | Required | - | `https://remix.blockscout.com/?address={hash}&blockscout={domain}` | +| icon_url | `string` | URL of the IDE icon | Required | - | `https://example.com/icon.svg` | + +  + +## App features + +*Note* The variables which are marked as required should be passed as described in order to enable the particular feature, but they are not required in the whole app context. + +### My account + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ | +| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | Required | - | `` | v1.0.x+ | +| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | Required | - | `https://blockscout.com` | v1.0.x+ | +| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | Required | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ | + +  + +### Gas tracker + +This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_TRACKER_ENABLED=false`. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GAS_TRACKER_ENABLED | `boolean` | Set to true to enable "Gas tracker" in the app | Required | `true` | `false` | v1.25.0+ | +| NEXT_PUBLIC_GAS_TRACKER_UNITS | Array<`usd` \| `gwei`> | Array of units for displaying gas prices on the Gas Tracker page, in the stats snippet on the Home page, and in the top bar. The first value in the array will take priority over the second one in all mentioned views. If only one value is provided, gas prices will be displayed only in that unit. | - | `[ 'usd', 'gwei' ]` | `[ 'gwei' ]` | v1.25.0+ | + +  + +### Address verification in "My account" + +*Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones: + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | Required | - | `https://contracts-info.services.blockscout.com` | v1.1.0+ | +| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | + +  + +### Blockchain interaction (writing to contract, etc.) + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://cloud.walletconnect.com/) integration | Required | - | `` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Gnosis Chain` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_ID | `number` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `99` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `Ether` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `ETH` | v1.0.x+ | +| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | - | `18` | `6` | v1.0.x+ | + +  + +### Banner ads + +This feature is **enabled by default** with the `slise` ads provider. To switch it off pass `NEXT_PUBLIC_AD_BANNER_PROVIDER=none`. +*Note* that the `getit` ad provider is temporary disabled. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `hype` \| `getit` \| `none` | Ads provider | - | `slise` | `coinzilla` | v1.0.x+ | +| NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER | `adbutler` | Additional ads provider to mix with the main one | - | - | `adbutler` | v1.28.0+ | +| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` | v1.3.0+ | +| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` | v1.3.0+ | + +  + +### Text ads + +This feature is **enabled by default** with the `coinzilla` ads provider. To switch it off pass `NEXT_PUBLIC_AD_TEXT_PROVIDER=none`. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_AD_TEXT_PROVIDER | `coinzilla` \| `none` | Ads provider | - | `coinzilla` | `none` | v1.0.x+ | + +  + +### Beacon chain + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_HAS_BEACON_CHAIN | `boolean` | Set to true for networks with the beacon chain | Required | - | `true` | v1.0.x+ | +| NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL | `string` | Beacon network currency symbol | - | `NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL` | `ETH` | v1.0.x+ | + +  + +### User operations (ERC-4337) + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_HAS_USER_OPS | `boolean` | Set to true to show user operations related data and pages | - | - | `true` | v1.23.0+ | + +  + +### Rollup chain + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ | +| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ | +| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ | +| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ | +| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | - | + +  + +### Export data to CSV file + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `` | v1.0.x+ | + +  + +### Google analytics + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | true | - | `UA-XXXXXX-X` | v1.0.x+ | + +  + +### Mixpanel analytics + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN | `string` | Project token for [Mixpanel](https://mixpanel.com/) analytics service | true | - | `` | v1.1.0+ | + +  + +### GrowthBook feature flagging and A/B testing + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY | `string` | Client SDK key for [GrowthBook](https://www.growthbook.io/) service | true | - | `` | v1.22.0+ | + +  + +### GraphQL API documentation + +This feature is **always enabled**, but you can disable it by passing `none` value to `NEXT_PUBLIC_GRAPHIQL_TRANSACTION` variable. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl playground page. Pass `none` to disable the feature. | - | - | `0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62` | v1.0.x+ | + +  + +### REST API documentation + +This feature is **always enabled**, but you can disable it by passing `none` value to `NEXT_PUBLIC_API_SPEC_URL` variable. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec to be displayed on `/api-docs` page. Pass `none` to disable the feature. | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | v1.0.x+ | + +  + +### Marketplace + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_MARKETPLACE_ENABLED | `boolean` | `true` means that the marketplace page will be enabled | Required | - | `true` | v1.24.1+ | +| NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app. Can be replaced with NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | Required | - | `https://example.com/marketplace_config.json` | v1.0.x+ | +| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | +| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | v1.0.x+ | +| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` | v1.24.0+ | +| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` | v1.0.x+ | +| NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL | `string` | URL of configuration file (`.json` format only) which contains the list of categories to be displayed on the marketplace page in the specified order. If no URL is provided, then the list of categories will be compiled based on the `categories` fields from the marketplace (apps) configuration file | - | - | `https://example.com/marketplace_categories.json` | v1.23.0+ | +| NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL | `string` | URL of configuration file (`.json` format only) which contains app security reports for displaying security scores on the Marketplace page | - | - | `https://example.com/marketplace_security_reports.json` | v1.28.0+ | +| NEXT_PUBLIC_MARKETPLACE_FEATURED_APP | `string` | ID of the featured application to be displayed on the banner on the Marketplace page | - | - | `uniswap` | v1.29.0+ | +| NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL | `string` | URL of the banner HTML content | - | - | `https://example.com/banner` | v1.29.0+ | +| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ | +| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ | +| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ | + +#### Marketplace app configuration properties + +| Property | Type | Description | Compulsoriness | Example value | +| --- | --- | --- | --- | --- | +| id | `string` | Used as slug for the app. Must be unique in the app list. | Required | `'app'` | +| external | `boolean` | `true` means that the application opens in a new window, but not in an iframe. | - | `true` | +| title | `string` | Displayed title of the app. | Required | `'The App'` | +| logo | `string` | URL to logo file. Should be at least 288x288. | Required | `'https://foo.app/icon.png'` | +| shortDescription | `string` | Displayed only in the app list. | Required | `'Awesome app'` | +| categories | `Array` | Displayed category. | Required | `['Security', 'Tools']` | +| author | `string` | Displayed author of the app | Required | `'Bob'` | +| url | `string` | URL of the app which will be launched in the iframe. | Required | `'https://foo.app/launch'` | +| description | `string` | Displayed only in the modal dialog with additional info about the app. | Required | `'The best app'` | +| site | `string` | Displayed site link | - | `'https://blockscout.com'` | +| twitter | `string` | Displayed twitter link | - | `'https://twitter.com/blockscoutcom'` | +| telegram | `string` | Displayed telegram link | - | `'https://t.me/poa_network'` | +| github | `string` | Displayed github link | - | `'https://github.com/blockscout'` | +| internalWallet | `boolean` | `true` means that the application can automatically connect to the Blockscout wallet. | - | `true` | +| priority | `number` | The higher the priority, the higher the app will appear in the list on the Marketplace page. | - | `7` | + +  + +### Solidity to UML diagrams + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | Required | - | `https://visualizer.services.blockscout.com` | v1.0.x+ | +| NEXT_PUBLIC_VISUALIZE_API_BASE_PATH | `string` | Base path for Visualize API endpoint url | - | - | `/poa/core` | v1.29.0+ | + +  + +### Blockchain statistics + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | Required | - | `https://stats.services.blockscout.com` | v1.0.x+ | +| NEXT_PUBLIC_STATS_API_BASE_PATH | `string` | Base path for Stats API endpoint url | - | - | `/poa/core` | v1.29.0+ | + +  + +### Web3 wallet integration (add token or network to the wallet) + +This feature is **enabled by default** with the `['metamask']` value. To switch it off pass `NEXT_PUBLIC_WEB3_WALLETS=none`. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_WEB3_WALLETS | `Array<'metamask' \| 'coinbase' \| 'token_pocket'>` | Array of Web3 wallets which will be used to add tokens or chain to. The first wallet which is enabled in user's browser will be shown. | - | `[ 'metamask' ]` | `[ 'coinbase' ]` | v1.10.0+ | +| NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET | `boolean`| Set to `true` to hide icon "Add to your wallet" next to token addresses | - | - | `true` | v1.0.x+ | + +  + +### Transaction interpretation + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER | `blockscout` \| `noves` \| `none` | Transaction interpretation provider that displays human readable transaction description | - | `none` | `blockscout` | v1.21.0+ | + +  + +### Verified tokens info + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | Required | - | `https://contracts-info.services.blockscout.com` | v1.0.x+ | + +  + +### Name service integration + +This feature allows resolving blockchain addresses using human-readable domain names. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_NAME_SERVICE_API_HOST | `string` | Name Service API endpoint url | Required | - | `https://bens.services.blockscout.com` | v1.22.0+ | + +  + +### Metadata service integration + +This feature allows name tags and other public tags for addresses. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ | + +  + +### Public tag submission + +This feature allows you to submit an application with a public address tag. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ | +| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | + +  + +### Data Availability + +This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED | `boolean` | Set to true to enable blob transactions views. | Required | - | `true` | v1.28.0+ | + +  + +### Bridged tokens + +This feature allows users to view tokens that have been bridged from other EVM chains. Additional tab "Bridged" will be added to the tokens page and the link to original token will be displayed on the token page. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS | `Array` where `BridgedTokenChain` can have following [properties](#bridged-token-chain-configuration-properties) | Used for displaying filter by the chain from which token where bridged. Also, used for creating links to original tokens in other explorers. | Required | - | `[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://eth.blockscout.com/token'}]` | v1.14.0+ | +| NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES | `Array` where `TokenBridge` can have following [properties](#token-bridge-configuration-properties) | Used for displaying text about bridges types on the tokens page. | Required | - | `[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}]` | v1.14.0+ | + +#### Bridged token chain configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| id | `string` | Base chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `1` | +| title | `string` | Displayed name of the chain | Required | - | `Ethereum` | +| short_title | `string` | Used for displaying chain name in the list view as tag | Required | - | `ETH` | +| base_url | `string` | Base url to original token in base chain explorer | Required | - | `https://eth.blockscout.com/token` | + +*Note* The url to original token will be constructed as `/`, e.g `https://eth.blockscout.com/token/` + +#### Token bridge configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| type | `string` | Bridge type; should be matched to `bridge_type` field in API response | Required | - | `omni` | +| title | `string` | Bridge title | Required | - | `OmniBridge` | +| short_title | `string` | Bridge short title for displaying in the tags | Required | - | `OMNI` | + +  + +### Safe{Core} address tags + +For the smart contract addresses which are [Safe{Core} accounts](https://safe.global/) public tag "Multisig: Safe" will be displayed in the address page header alongside to Safe logo. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_SAFE_TX_SERVICE_URL | `string` | The Safe transaction service URL. See full list of supported networks [here](https://docs.safe.global/api-supported-networks). | - | - | `uniswap` | v1.26.0+ | + +  + +### SUAVE chain + +For blockchains that implement SUAVE architecture additional fields will be shown on the transaction page ("Allowed peekers", "Kettle"). Users also will be able to see the list of all transactions for a particular Kettle in the separate view. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_IS_SUAVE_CHAIN | `boolean` | Set to true for blockchains with [SUAVE architecture](https://writings.flashbots.net/mevm-suave-centauri-and-beyond) | Required | - | `true` | v1.14.0+ | + +  + +### MetaSuites extension + +Enables [MetaSuites browser extension](https://github.com/blocksecteam/metasuites) to integrate with the app views. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_METASUITES_ENABLED | `boolean` | Set to true to enable integration | Required | - | `true` | v1.26.0+ | + +  + +### Validators list + +The feature enables the Validators page which provides detailed information about the validators of the PoS chains. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability'` | Chain type | Required | - | `'stability'` | v1.25.0+ | + +  + +### Sentry error monitoring + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | Required | - | `` | v1.0.x+ | +| SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `` | v1.0.x+ | +| NEXT_PUBLIC_SENTRY_ENABLE_TRACING | `boolean` | Enables tracing and performance monitoring in Sentry.io | - | `false` | `true` | v1.17.0+ | +| NEXT_PUBLIC_APP_ENV | `string` | App env (e.g development, review or production). Passed as `environment` property to Sentry config | - | `production` | `production` | v1.0.x+ | +| NEXT_PUBLIC_APP_INSTANCE | `string` | Name of app instance. Used as custom tag `app_instance` value in the main Sentry scope. If not provided, it will be constructed from `NEXT_PUBLIC_APP_HOST` | - | - | `wonderful_kepler` | v1.0.x+ | + +  + +### OpenTelemetry + +OpenTelemetry SDK for Node.js app could be enabled by passing `OTEL_SDK_ENABLED=true` variable. Configure the OpenTelemetry Protocol Exporter by using the generic environment variables described in the [OT docs](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options). Note that this Next.js feature is currently experimental. The Docker image should be built with the `NEXT_OPEN_TELEMETRY_ENABLED=true` argument to enable it. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| OTEL_SDK_ENABLED | `boolean` | Run-time flag to enable the feature | Required | `false` | `true` | v1.18.0+ | + +  + +### DeFi dropdown + +If the feature is enabled, a single button or a dropdown (if more than 1 item is provided) will be displayed at the top of the explorer page, which will take a user to the specified application in the marketplace or to an external site. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon: string; dappId?: string, url?: string }]` | An array of dropdown items containing the button text, icon name and dappId in DAppscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ | + +  + +### Multichain balance button + +If the feature is enabled, a Multichain balance button will be displayed on the address page, which will take you to the portfolio application in the marketplace or to an external site. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url_template: 'https://app.zerion.io/{address}/overview', logo: 'https://example.com/icon.svg'` | v1.31.0+ | + +  + +#### Multichain button configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| name | `string` | Multichain portfolio application name | Required | - | `zerion` | +| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` | +| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` | +| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` | + +  + +### Get gas button + +If the feature is enabled, a Get gas button will be displayed in the top bar, which will take you to the gas refuel application in the marketplace or to an external site. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Get gas button config. See [below](#get-gas-button-configuration-properties) | - | - | `{ name: 'Need gas?', dapp_id: 'smol-refuel', url_template: 'https://smolrefuel.com/?outboundChain={chainId}', logo: 'https://example.com/icon.png' }` | v1.33.0+ | + +  + +#### Get gas button configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| name | `string` | Text on the button | Required | - | `Need gas?` | +| url_template | `string` | Url template, may contain `{chainId}` variable | Required | - | `https://smolrefuel.com/?outboundChain={chainId}` | +| dapp_id | `string` | Set for open a Blockscout dapp page instead of opening external app page | - | - | `smol-refuel` | +| logo | `string` | Gas refuel application logo url | - | - | `https://example.com/icon.png` | + +  + +## External services configuration + +### Google ReCaptcha + +For obtaining the variables values please refer to [reCAPTCHA documentation](https://developers.google.com/recaptcha). + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key | - | - | `` | v1.0.x+ | diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..d4378762bf --- /dev/null +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +## Description and Related Issue(s) + +*[Provide a brief description of the changes or enhancements introduced by this pull request and explain motivation behind them. Cite any related issue(s) or bug(s) that it addresses using the [format](https://blog.github.com/2013-05-14-closing-issues-via-pull-requests/) `Fixes #123` or `Resolves #456`.]* + +### Proposed Changes +*[Specify the changes or additions made in this pull request. Please mention if any changes were made to the ENV variables]* + +### Breaking or Incompatible Changes +*[Describe any breaking or incompatible changes introduced by this pull request. Specify how users might need to modify their code or configurations to accommodate these changes.]* + +### Additional Information +*[Include any additional information, context, or screenshots that may be helpful for reviewers.]* + +## Checklist for PR author +- [ ] I have tested these changes locally. +- [ ] I added tests to cover any new functionality, following this [guide](./CONTRIBUTING.md#writing--running-tests) +- [ ] Whenever I fix a bug, I include a regression test to ensure that the bug does not reappear silently. +- [ ] If I have added, changed, renamed, or removed an environment variable + - I updated the list of environment variables in the [documentation](ENVS.md) + - I made the necessary changes to the validator script according to the [guide](./CONTRIBUTING.md#adding-new-env-variable) + - I added "ENVs" label to this pull request diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000000..1632505b52 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,31 @@ +import type { WalletProvider } from 'types/web3'; + +type CPreferences = { + zone: string; + width: string; + height: string; +} + +declare global { + export interface Window { + ethereum?: WalletProvider | undefined; + coinzilla_display: Array; + ga?: { + getAll: () => Array<{ get: (prop: string) => string }>; + }; + AdButler: { + ads: Array; + register: (...args: unknown) => void; + }; + abkw: string; + __envs: Record; + } + + namespace NodeJS { + interface ProcessEnv { + NODE_ENV: 'development' | 'production'; + } + } +} + +export {}; diff --git a/icons/ABI.svg b/icons/ABI.svg new file mode 100644 index 0000000000..9e5e27982d --- /dev/null +++ b/icons/ABI.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/ABI_slim.svg b/icons/ABI_slim.svg new file mode 100644 index 0000000000..89532207b1 --- /dev/null +++ b/icons/ABI_slim.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/icons/API.svg b/icons/API.svg new file mode 100644 index 0000000000..2fae957e81 --- /dev/null +++ b/icons/API.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/ENS.svg b/icons/ENS.svg new file mode 100644 index 0000000000..9832944dab --- /dev/null +++ b/icons/ENS.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/ENS_slim.svg b/icons/ENS_slim.svg new file mode 100644 index 0000000000..cd999b523a --- /dev/null +++ b/icons/ENS_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/MUD.svg b/icons/MUD.svg new file mode 100644 index 0000000000..8ab1229a71 --- /dev/null +++ b/icons/MUD.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/MUD_menu.svg b/icons/MUD_menu.svg new file mode 100644 index 0000000000..c30c571c47 --- /dev/null +++ b/icons/MUD_menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/RPC.svg b/icons/RPC.svg new file mode 100644 index 0000000000..7df2ba2195 --- /dev/null +++ b/icons/RPC.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/apps.svg b/icons/apps.svg new file mode 100644 index 0000000000..c0cdc1c4eb --- /dev/null +++ b/icons/apps.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/apps_list.svg b/icons/apps_list.svg new file mode 100644 index 0000000000..62cb5020d6 --- /dev/null +++ b/icons/apps_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/apps_slim.svg b/icons/apps_slim.svg new file mode 100644 index 0000000000..59e2f2d818 --- /dev/null +++ b/icons/apps_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/down-right.svg b/icons/arrows/down-right.svg new file mode 100644 index 0000000000..e02aa2f9a5 --- /dev/null +++ b/icons/arrows/down-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/east-mini.svg b/icons/arrows/east-mini.svg new file mode 100644 index 0000000000..1a9d93e401 --- /dev/null +++ b/icons/arrows/east-mini.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/east.svg b/icons/arrows/east.svg new file mode 100644 index 0000000000..c152a2f673 --- /dev/null +++ b/icons/arrows/east.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/north-east.svg b/icons/arrows/north-east.svg new file mode 100644 index 0000000000..59aa6a4b23 --- /dev/null +++ b/icons/arrows/north-east.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/south-east.svg b/icons/arrows/south-east.svg new file mode 100644 index 0000000000..16a7590f56 --- /dev/null +++ b/icons/arrows/south-east.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/up-down.svg b/icons/arrows/up-down.svg new file mode 100644 index 0000000000..8f45bb257d --- /dev/null +++ b/icons/arrows/up-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/arrows/up-head.svg b/icons/arrows/up-head.svg new file mode 100644 index 0000000000..375381a790 --- /dev/null +++ b/icons/arrows/up-head.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/beta.svg b/icons/beta.svg new file mode 100644 index 0000000000..bba1309f3a --- /dev/null +++ b/icons/beta.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/beta_xs.svg b/icons/beta_xs.svg new file mode 100644 index 0000000000..a6dc48ee4e --- /dev/null +++ b/icons/beta_xs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/blob.svg b/icons/blob.svg new file mode 100644 index 0000000000..9b40d72ecb --- /dev/null +++ b/icons/blob.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/blobs/image.svg b/icons/blobs/image.svg new file mode 100644 index 0000000000..be08dd269c --- /dev/null +++ b/icons/blobs/image.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/icons/blobs/raw.svg b/icons/blobs/raw.svg new file mode 100644 index 0000000000..8a97401ff5 --- /dev/null +++ b/icons/blobs/raw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/icons/blobs/text.svg b/icons/blobs/text.svg new file mode 100644 index 0000000000..08ec8801bf --- /dev/null +++ b/icons/blobs/text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/block.svg b/icons/block.svg new file mode 100644 index 0000000000..85c88a6bdf --- /dev/null +++ b/icons/block.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/block_countdown.svg b/icons/block_countdown.svg new file mode 100644 index 0000000000..0024e52ce9 --- /dev/null +++ b/icons/block_countdown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/block_slim.svg b/icons/block_slim.svg new file mode 100644 index 0000000000..63302e1d83 --- /dev/null +++ b/icons/block_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/brands/blockscout.svg b/icons/brands/blockscout.svg new file mode 100644 index 0000000000..0e3279de01 --- /dev/null +++ b/icons/brands/blockscout.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/brands/safe.svg b/icons/brands/safe.svg new file mode 100644 index 0000000000..8369513837 --- /dev/null +++ b/icons/brands/safe.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/brands/solidity_scan.svg b/icons/brands/solidity_scan.svg new file mode 100644 index 0000000000..ac5747c69a --- /dev/null +++ b/icons/brands/solidity_scan.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/icons/burger.svg b/icons/burger.svg new file mode 100644 index 0000000000..c6a84a911e --- /dev/null +++ b/icons/burger.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/certified.svg b/icons/certified.svg new file mode 100644 index 0000000000..088a866f8a --- /dev/null +++ b/icons/certified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/check.svg b/icons/check.svg new file mode 100644 index 0000000000..563e6e5a9a --- /dev/null +++ b/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/clock-light.svg b/icons/clock-light.svg new file mode 100644 index 0000000000..110cd4b797 --- /dev/null +++ b/icons/clock-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/clock.svg b/icons/clock.svg new file mode 100644 index 0000000000..14a8c94053 --- /dev/null +++ b/icons/clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/coins/bitcoin.svg b/icons/coins/bitcoin.svg new file mode 100644 index 0000000000..7f22b3139f --- /dev/null +++ b/icons/coins/bitcoin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/collection.svg b/icons/collection.svg new file mode 100644 index 0000000000..981040af5a --- /dev/null +++ b/icons/collection.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/contracts/proxy.svg b/icons/contracts/proxy.svg new file mode 100644 index 0000000000..1b75cb210f --- /dev/null +++ b/icons/contracts/proxy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/contracts/regular.svg b/icons/contracts/regular.svg new file mode 100644 index 0000000000..1bc8e0d3d9 --- /dev/null +++ b/icons/contracts/regular.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/contracts/regular_many.svg b/icons/contracts/regular_many.svg new file mode 100644 index 0000000000..1f0b62afd2 --- /dev/null +++ b/icons/contracts/regular_many.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/contracts/verified.svg b/icons/contracts/verified.svg new file mode 100644 index 0000000000..6dbb058433 --- /dev/null +++ b/icons/contracts/verified.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/contracts/verified_many.svg b/icons/contracts/verified_many.svg new file mode 100644 index 0000000000..2a004f596d --- /dev/null +++ b/icons/contracts/verified_many.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/copy.svg b/icons/copy.svg new file mode 100644 index 0000000000..778c59dcf1 --- /dev/null +++ b/icons/copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/cross.svg b/icons/cross.svg new file mode 100644 index 0000000000..44a66fcc75 --- /dev/null +++ b/icons/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/delete.svg b/icons/delete.svg new file mode 100644 index 0000000000..13b71cdffe --- /dev/null +++ b/icons/delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/docs.svg b/icons/docs.svg new file mode 100644 index 0000000000..71a628d93c --- /dev/null +++ b/icons/docs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/donate.svg b/icons/donate.svg new file mode 100644 index 0000000000..9ed7def2bf --- /dev/null +++ b/icons/donate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/dots.svg b/icons/dots.svg new file mode 100644 index 0000000000..1ea165f92f --- /dev/null +++ b/icons/dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/edit.svg b/icons/edit.svg new file mode 100644 index 0000000000..02a2737da2 --- /dev/null +++ b/icons/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/email-sent.svg b/icons/email-sent.svg new file mode 100644 index 0000000000..d31e30f244 --- /dev/null +++ b/icons/email-sent.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/email.svg b/icons/email.svg new file mode 100644 index 0000000000..4e184be0e5 --- /dev/null +++ b/icons/email.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/empty_search_result.svg b/icons/empty_search_result.svg new file mode 100644 index 0000000000..f4d62eff0e --- /dev/null +++ b/icons/empty_search_result.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/icons/error-pages/404.svg b/icons/error-pages/404.svg new file mode 100644 index 0000000000..296ff701c8 --- /dev/null +++ b/icons/error-pages/404.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/error-pages/422.svg b/icons/error-pages/422.svg new file mode 100644 index 0000000000..99225c6433 --- /dev/null +++ b/icons/error-pages/422.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/error-pages/429.svg b/icons/error-pages/429.svg new file mode 100644 index 0000000000..9ae110e192 --- /dev/null +++ b/icons/error-pages/429.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/error-pages/500.svg b/icons/error-pages/500.svg new file mode 100644 index 0000000000..43cd887fd8 --- /dev/null +++ b/icons/error-pages/500.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/explorer.svg b/icons/explorer.svg new file mode 100644 index 0000000000..dd9e6b57af --- /dev/null +++ b/icons/explorer.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/icons/files/csv.svg b/icons/files/csv.svg new file mode 100644 index 0000000000..119d58cdf0 --- /dev/null +++ b/icons/files/csv.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/files/image.svg b/icons/files/image.svg new file mode 100644 index 0000000000..9d2da4ee82 --- /dev/null +++ b/icons/files/image.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/files/json.svg b/icons/files/json.svg new file mode 100644 index 0000000000..06c437efbf --- /dev/null +++ b/icons/files/json.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/files/placeholder.svg b/icons/files/placeholder.svg new file mode 100644 index 0000000000..129e36a3bc --- /dev/null +++ b/icons/files/placeholder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/files/sol.svg b/icons/files/sol.svg new file mode 100644 index 0000000000..0efa5d0b39 --- /dev/null +++ b/icons/files/sol.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/files/yul.svg b/icons/files/yul.svg new file mode 100644 index 0000000000..03b88fbb3d --- /dev/null +++ b/icons/files/yul.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/filter.svg b/icons/filter.svg new file mode 100644 index 0000000000..c9c284e81b --- /dev/null +++ b/icons/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/flame.svg b/icons/flame.svg new file mode 100644 index 0000000000..ddc5f161e6 --- /dev/null +++ b/icons/flame.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/games.svg b/icons/games.svg new file mode 100644 index 0000000000..39c01ca56d --- /dev/null +++ b/icons/games.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/gas.svg b/icons/gas.svg new file mode 100644 index 0000000000..4334fe93f5 --- /dev/null +++ b/icons/gas.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/gas_xl.svg b/icons/gas_xl.svg new file mode 100644 index 0000000000..5a3913ac16 --- /dev/null +++ b/icons/gas_xl.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/gear.svg b/icons/gear.svg new file mode 100644 index 0000000000..32ec463e17 --- /dev/null +++ b/icons/gear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/gear_slim.svg b/icons/gear_slim.svg new file mode 100644 index 0000000000..abc14e6a78 --- /dev/null +++ b/icons/gear_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/globe-b.svg b/icons/globe-b.svg new file mode 100644 index 0000000000..0b185e8551 --- /dev/null +++ b/icons/globe-b.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/globe.svg b/icons/globe.svg new file mode 100644 index 0000000000..e3cea4b639 --- /dev/null +++ b/icons/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/graphQL.svg b/icons/graphQL.svg new file mode 100644 index 0000000000..9332276ac2 --- /dev/null +++ b/icons/graphQL.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/heart_filled.svg b/icons/heart_filled.svg new file mode 100644 index 0000000000..80926b1668 --- /dev/null +++ b/icons/heart_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/heart_outline.svg b/icons/heart_outline.svg new file mode 100644 index 0000000000..8bf7ce3e36 --- /dev/null +++ b/icons/heart_outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/hourglass.svg b/icons/hourglass.svg new file mode 100644 index 0000000000..7ebd6d78b2 --- /dev/null +++ b/icons/hourglass.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/info.svg b/icons/info.svg new file mode 100644 index 0000000000..0401005742 --- /dev/null +++ b/icons/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/integration/full.svg b/icons/integration/full.svg new file mode 100644 index 0000000000..0ac50a345b --- /dev/null +++ b/icons/integration/full.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/integration/partial.svg b/icons/integration/partial.svg new file mode 100644 index 0000000000..2f7a76969f --- /dev/null +++ b/icons/integration/partial.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/key.svg b/icons/key.svg new file mode 100644 index 0000000000..ae0834f529 --- /dev/null +++ b/icons/key.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/lightning.svg b/icons/lightning.svg new file mode 100644 index 0000000000..03fea73d75 --- /dev/null +++ b/icons/lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/lightning_navbar.svg b/icons/lightning_navbar.svg new file mode 100644 index 0000000000..9587a9c7a2 --- /dev/null +++ b/icons/lightning_navbar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/link.svg b/icons/link.svg new file mode 100644 index 0000000000..5d072572aa --- /dev/null +++ b/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/link_external.svg b/icons/link_external.svg new file mode 100644 index 0000000000..dbddf710bc --- /dev/null +++ b/icons/link_external.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/lock.svg b/icons/lock.svg new file mode 100644 index 0000000000..763d128cb7 --- /dev/null +++ b/icons/lock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/minus.svg b/icons/minus.svg new file mode 100644 index 0000000000..ba1cfe2409 --- /dev/null +++ b/icons/minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/monaco/file.svg b/icons/monaco/file.svg new file mode 100644 index 0000000000..38b4a10fc4 --- /dev/null +++ b/icons/monaco/file.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/monaco/folder-open.svg b/icons/monaco/folder-open.svg new file mode 100644 index 0000000000..02989af8d2 --- /dev/null +++ b/icons/monaco/folder-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/monaco/folder.svg b/icons/monaco/folder.svg new file mode 100644 index 0000000000..70e98efa46 --- /dev/null +++ b/icons/monaco/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/monaco/solidity.svg b/icons/monaco/solidity.svg new file mode 100644 index 0000000000..f1c51a4c69 --- /dev/null +++ b/icons/monaco/solidity.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/monaco/vyper.svg b/icons/monaco/vyper.svg new file mode 100644 index 0000000000..cd7b34ad9a --- /dev/null +++ b/icons/monaco/vyper.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/moon-with-star.svg b/icons/moon-with-star.svg new file mode 100644 index 0000000000..24c2085873 --- /dev/null +++ b/icons/moon-with-star.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/moon.svg b/icons/moon.svg new file mode 100644 index 0000000000..57c4ee6564 --- /dev/null +++ b/icons/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/networks.svg b/icons/networks.svg new file mode 100644 index 0000000000..cc62a35798 --- /dev/null +++ b/icons/networks.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/networks/icon-placeholder.svg b/icons/networks/icon-placeholder.svg new file mode 100644 index 0000000000..77414f1df4 --- /dev/null +++ b/icons/networks/icon-placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/networks/logo-placeholder.svg b/icons/networks/logo-placeholder.svg new file mode 100644 index 0000000000..6c7891fdbf --- /dev/null +++ b/icons/networks/logo-placeholder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/nft_shield.svg b/icons/nft_shield.svg new file mode 100644 index 0000000000..0d2a83e9a3 --- /dev/null +++ b/icons/nft_shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/open-link.svg b/icons/open-link.svg new file mode 100644 index 0000000000..d0fcc28ab4 --- /dev/null +++ b/icons/open-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/output_roots.svg b/icons/output_roots.svg new file mode 100644 index 0000000000..447e65743b --- /dev/null +++ b/icons/output_roots.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/payment_link.svg b/icons/payment_link.svg new file mode 100644 index 0000000000..f97128fff6 --- /dev/null +++ b/icons/payment_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/plus.svg b/icons/plus.svg new file mode 100644 index 0000000000..b86a621188 --- /dev/null +++ b/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/privattags.svg b/icons/privattags.svg new file mode 100644 index 0000000000..7e0cacaa07 --- /dev/null +++ b/icons/privattags.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/profile.svg b/icons/profile.svg new file mode 100644 index 0000000000..177eea9650 --- /dev/null +++ b/icons/profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/publictags.svg b/icons/publictags.svg new file mode 100644 index 0000000000..4c55542fc6 --- /dev/null +++ b/icons/publictags.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/publictags_slim.svg b/icons/publictags_slim.svg new file mode 100644 index 0000000000..4de98a2d1f --- /dev/null +++ b/icons/publictags_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/qr_code.svg b/icons/qr_code.svg new file mode 100644 index 0000000000..f31b981dae --- /dev/null +++ b/icons/qr_code.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/refresh.svg b/icons/refresh.svg new file mode 100644 index 0000000000..fef0346a50 --- /dev/null +++ b/icons/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/repeat.svg b/icons/repeat.svg new file mode 100644 index 0000000000..dcd2c7a374 --- /dev/null +++ b/icons/repeat.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/restAPI.svg b/icons/restAPI.svg new file mode 100644 index 0000000000..8a6faf3d5f --- /dev/null +++ b/icons/restAPI.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/icons/rocket.svg b/icons/rocket.svg new file mode 100644 index 0000000000..46523e1b05 --- /dev/null +++ b/icons/rocket.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/rocket_xl.svg b/icons/rocket_xl.svg new file mode 100644 index 0000000000..8b3f4ccdbf --- /dev/null +++ b/icons/rocket_xl.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/scope.svg b/icons/scope.svg new file mode 100644 index 0000000000..6337932af8 --- /dev/null +++ b/icons/scope.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/score/score-not-ok.svg b/icons/score/score-not-ok.svg new file mode 100644 index 0000000000..9b4533f6ac --- /dev/null +++ b/icons/score/score-not-ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/score/score-ok.svg b/icons/score/score-ok.svg new file mode 100644 index 0000000000..dbe11836fd --- /dev/null +++ b/icons/score/score-ok.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/search.svg b/icons/search.svg new file mode 100644 index 0000000000..1d7814fa05 --- /dev/null +++ b/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/canny.svg b/icons/social/canny.svg new file mode 100644 index 0000000000..8041cd2bf3 --- /dev/null +++ b/icons/social/canny.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/coingecko.svg b/icons/social/coingecko.svg new file mode 100644 index 0000000000..baf56eef3a --- /dev/null +++ b/icons/social/coingecko.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/social/coinmarketcap.svg b/icons/social/coinmarketcap.svg new file mode 100644 index 0000000000..0341e03c42 --- /dev/null +++ b/icons/social/coinmarketcap.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/defi_llama.svg b/icons/social/defi_llama.svg new file mode 100644 index 0000000000..f7d2cc51a2 --- /dev/null +++ b/icons/social/defi_llama.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/social/discord.svg b/icons/social/discord.svg new file mode 100644 index 0000000000..133a54e97a --- /dev/null +++ b/icons/social/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/discord_filled.svg b/icons/social/discord_filled.svg new file mode 100644 index 0000000000..691efc8a4e --- /dev/null +++ b/icons/social/discord_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/facebook_filled.svg b/icons/social/facebook_filled.svg new file mode 100644 index 0000000000..12afda1977 --- /dev/null +++ b/icons/social/facebook_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/git.svg b/icons/social/git.svg new file mode 100644 index 0000000000..caf39a4c95 --- /dev/null +++ b/icons/social/git.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/github_filled.svg b/icons/social/github_filled.svg new file mode 100644 index 0000000000..e134fcaf9a --- /dev/null +++ b/icons/social/github_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/linkedin_filled.svg b/icons/social/linkedin_filled.svg new file mode 100644 index 0000000000..6fcd9bedf9 --- /dev/null +++ b/icons/social/linkedin_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/medium_filled.svg b/icons/social/medium_filled.svg new file mode 100644 index 0000000000..60f2d6303c --- /dev/null +++ b/icons/social/medium_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/opensea_filled.svg b/icons/social/opensea_filled.svg new file mode 100644 index 0000000000..a1369318e9 --- /dev/null +++ b/icons/social/opensea_filled.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/social/reddit_filled.svg b/icons/social/reddit_filled.svg new file mode 100644 index 0000000000..a57d9f7979 --- /dev/null +++ b/icons/social/reddit_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/slack_filled.svg b/icons/social/slack_filled.svg new file mode 100644 index 0000000000..177f971226 --- /dev/null +++ b/icons/social/slack_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/stats.svg b/icons/social/stats.svg new file mode 100644 index 0000000000..cfc239f889 --- /dev/null +++ b/icons/social/stats.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/social/telega.svg b/icons/social/telega.svg new file mode 100644 index 0000000000..432bc5c763 --- /dev/null +++ b/icons/social/telega.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/social/telegram_filled.svg b/icons/social/telegram_filled.svg new file mode 100644 index 0000000000..87a2ebf52b --- /dev/null +++ b/icons/social/telegram_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/twitter.svg b/icons/social/twitter.svg new file mode 100644 index 0000000000..21e9812ff7 --- /dev/null +++ b/icons/social/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/social/twitter_filled.svg b/icons/social/twitter_filled.svg new file mode 100644 index 0000000000..0d73b850a0 --- /dev/null +++ b/icons/social/twitter_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/star_filled.svg b/icons/star_filled.svg new file mode 100644 index 0000000000..7b6312c876 --- /dev/null +++ b/icons/star_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/star_outline.svg b/icons/star_outline.svg new file mode 100644 index 0000000000..05286fa1d5 --- /dev/null +++ b/icons/star_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/stats.svg b/icons/stats.svg new file mode 100644 index 0000000000..127477d180 --- /dev/null +++ b/icons/stats.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/status/error.svg b/icons/status/error.svg new file mode 100644 index 0000000000..3f7dff92c3 --- /dev/null +++ b/icons/status/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/status/pending.svg b/icons/status/pending.svg new file mode 100644 index 0000000000..f9e5a88d53 --- /dev/null +++ b/icons/status/pending.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/status/success.svg b/icons/status/success.svg new file mode 100644 index 0000000000..ce76c56a49 --- /dev/null +++ b/icons/status/success.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/status/warning.svg b/icons/status/warning.svg new file mode 100644 index 0000000000..c177aea5a5 --- /dev/null +++ b/icons/status/warning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/sun.svg b/icons/sun.svg new file mode 100644 index 0000000000..f90bf90eab --- /dev/null +++ b/icons/sun.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/swap.svg b/icons/swap.svg new file mode 100644 index 0000000000..c1566be5fc --- /dev/null +++ b/icons/swap.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/testnet.svg b/icons/testnet.svg new file mode 100644 index 0000000000..ff01ccf7ad --- /dev/null +++ b/icons/testnet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/token-placeholder.svg b/icons/token-placeholder.svg new file mode 100644 index 0000000000..92307c47e4 --- /dev/null +++ b/icons/token-placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/token.svg b/icons/token.svg new file mode 100644 index 0000000000..933d69406f --- /dev/null +++ b/icons/token.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/icons/tokens.svg b/icons/tokens.svg new file mode 100644 index 0000000000..40c10466d6 --- /dev/null +++ b/icons/tokens.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/tokens/xdai.svg b/icons/tokens/xdai.svg new file mode 100644 index 0000000000..17028c76bf --- /dev/null +++ b/icons/tokens/xdai.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/icons/top-accounts.svg b/icons/top-accounts.svg new file mode 100644 index 0000000000..b74d07e854 --- /dev/null +++ b/icons/top-accounts.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/transactions.svg b/icons/transactions.svg new file mode 100644 index 0000000000..3cefc2141e --- /dev/null +++ b/icons/transactions.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/transactions_slim.svg b/icons/transactions_slim.svg new file mode 100644 index 0000000000..412f46aa31 --- /dev/null +++ b/icons/transactions_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/txn_batches.svg b/icons/txn_batches.svg new file mode 100644 index 0000000000..00dd273c27 --- /dev/null +++ b/icons/txn_batches.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/txn_batches_slim.svg b/icons/txn_batches_slim.svg new file mode 100644 index 0000000000..b10a430734 --- /dev/null +++ b/icons/txn_batches_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/uniswap.svg b/icons/uniswap.svg new file mode 100644 index 0000000000..7abbc79f08 --- /dev/null +++ b/icons/uniswap.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/user_op.svg b/icons/user_op.svg new file mode 100644 index 0000000000..02bc701401 --- /dev/null +++ b/icons/user_op.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/user_op_slim.svg b/icons/user_op_slim.svg new file mode 100644 index 0000000000..d8c64b52b8 --- /dev/null +++ b/icons/user_op_slim.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/validator.svg b/icons/validator.svg new file mode 100644 index 0000000000..e77bb0ba5d --- /dev/null +++ b/icons/validator.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/verification-steps/finalized.svg b/icons/verification-steps/finalized.svg new file mode 100644 index 0000000000..fbae66d631 --- /dev/null +++ b/icons/verification-steps/finalized.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/verification-steps/unfinalized.svg b/icons/verification-steps/unfinalized.svg new file mode 100644 index 0000000000..49fdbc7ddf --- /dev/null +++ b/icons/verification-steps/unfinalized.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/verified.svg b/icons/verified.svg new file mode 100644 index 0000000000..b92152ab44 --- /dev/null +++ b/icons/verified.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/wallet.svg b/icons/wallet.svg new file mode 100644 index 0000000000..f1765c246d --- /dev/null +++ b/icons/wallet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/wallets/coinbase.svg b/icons/wallets/coinbase.svg new file mode 100644 index 0000000000..9a75b44e6d --- /dev/null +++ b/icons/wallets/coinbase.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/wallets/metamask.svg b/icons/wallets/metamask.svg new file mode 100644 index 0000000000..ea6a6382a5 --- /dev/null +++ b/icons/wallets/metamask.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/icons/wallets/token-pocket.svg b/icons/wallets/token-pocket.svg new file mode 100644 index 0000000000..e9566f816d --- /dev/null +++ b/icons/wallets/token-pocket.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/icons/watchlist.svg b/icons/watchlist.svg new file mode 100644 index 0000000000..6570c04066 --- /dev/null +++ b/icons/watchlist.svg @@ -0,0 +1,3 @@ + + + diff --git a/instrumentation.node.ts b/instrumentation.node.ts new file mode 100644 index 0000000000..5cae612a92 --- /dev/null +++ b/instrumentation.node.ts @@ -0,0 +1,71 @@ +/* eslint-disable no-console */ +import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { Resource } from '@opentelemetry/resources'; +import { + PeriodicExportingMetricReader, + ConsoleMetricExporter, +} from '@opentelemetry/sdk-metrics'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); + +const traceExporter = new OTLPTraceExporter(); + +const sdk = new NodeSDK({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'blockscout_frontend', + [SemanticResourceAttributes.SERVICE_VERSION]: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || 'unknown_version', + [SemanticResourceAttributes.SERVICE_INSTANCE_ID]: + process.env.NEXT_PUBLIC_APP_INSTANCE || + process.env.NEXT_PUBLIC_APP_HOST?.replace('.blockscout.com', '').replaceAll('-', '_') || + 'unknown_app', + }), + spanProcessor: new SimpleSpanProcessor(traceExporter), + traceExporter, + metricReader: new PeriodicExportingMetricReader({ + exporter: + process.env.NODE_ENV === 'production' ? + new OTLPMetricExporter() : + new ConsoleMetricExporter(), + }), + instrumentations: [ + getNodeAutoInstrumentations({ + '@opentelemetry/instrumentation-http': { + ignoreIncomingRequestHook: (request) => { + try { + if (!request.url) { + return false; + } + const url = new URL(request.url, `http://${ request.headers.host }`); + if ( + url.pathname.startsWith('/_next/static/') || + url.pathname.startsWith('/_next/data/') || + url.pathname.startsWith('/assets/') || + url.pathname.startsWith('/static/') + ) { + return true; + } + } catch (error) {} + return false; + }, + }, + }), + ], +}); + +if (process.env.OTEL_SDK_ENABLED) { + sdk.start(); + + process.on('SIGTERM', () => { + sdk + .shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); + }); +} diff --git a/instrumentation.ts b/instrumentation.ts new file mode 100644 index 0000000000..c5384e36a7 --- /dev/null +++ b/instrumentation.ts @@ -0,0 +1,5 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./instrumentation.node'); + } +} diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000000..d87d585507 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,41 @@ +/* eslint-disable max-len */ +import type { JestConfigWithTsJest } from 'ts-jest'; + +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const config: JestConfigWithTsJest = { + clearMocks: true, + coverageProvider: 'v8', + globalSetup: '/jest/global-setup.ts', + moduleDirectories: [ + 'node_modules', + __dirname, + ], + moduleNameMapper: { + '^jest/(.*)': '/jest/$1', + }, + modulePathIgnorePatterns: [ + 'node_modules_linux', + ], + preset: 'ts-jest', + reporters: [ 'default', 'github-actions' ], + setupFiles: [ + '/jest/setup.ts', + ], + testEnvironment: 'jsdom', + transform: { + // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` + // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.jest.json', + }, + ], + }, +}; + +export default config; diff --git a/jest/global-setup.ts b/jest/global-setup.ts new file mode 100644 index 0000000000..1d17469be1 --- /dev/null +++ b/jest/global-setup.ts @@ -0,0 +1,5 @@ +import dotenv from 'dotenv'; + +export default async function globalSetup() { + dotenv.config({ path: './configs/envs/.env.jest' }); +} diff --git a/jest/lib.tsx b/jest/lib.tsx new file mode 100644 index 0000000000..deafc637c6 --- /dev/null +++ b/jest/lib.tsx @@ -0,0 +1,57 @@ +import { ChakraProvider } from '@chakra-ui/react'; +import { GrowthBookProvider } from '@growthbook/growthbook-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { RenderOptions } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import { AppContextProvider } from 'lib/contexts/app'; +import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; +import { SocketProvider } from 'lib/socket/context'; +import theme from 'theme'; + +import 'lib/setLocale'; + +const PAGE_PROPS = { + cookies: '', + referrer: '', + query: {}, + adBannerProvider: null, + apiData: null, +}; + +const TestApp = ({ children }: {children: React.ReactNode}) => { + const [ queryClient ] = React.useState(() => new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 0, + }, + }, + })); + + return ( + + + + + + + { children } + + + + + + + ); +}; + +const customRender = ( + ui: React.ReactElement, + options?: Omit, +) => render(ui, { wrapper: TestApp, ...options }); + +export * from '@testing-library/react'; +export { customRender as render }; +export { TestApp as wrapper }; diff --git a/jest/mocks/next-router.ts b/jest/mocks/next-router.ts new file mode 100644 index 0000000000..4213a7ab96 --- /dev/null +++ b/jest/mocks/next-router.ts @@ -0,0 +1,17 @@ +import type { NextRouter } from 'next/router'; + +export const router = { + query: {}, + push: jest.fn(() => Promise.resolve()), +}; + +export const useRouter = jest.fn>>(() => (router)); + +export const mockUseRouter = (params?: Partial) => { + return { + useRouter: jest.fn(() => ({ + ...router, + ...params, + })), + }; +}; diff --git a/jest/setup.ts b/jest/setup.ts new file mode 100644 index 0000000000..6ccc67ad4c --- /dev/null +++ b/jest/setup.ts @@ -0,0 +1,43 @@ +import dotenv from 'dotenv'; +import { TextEncoder, TextDecoder } from 'util'; + +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); + +const envs = dotenv.config({ path: './configs/envs/.env.jest' }); + +Object.assign(global, { TextDecoder, TextEncoder }); + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +Object.defineProperty(window, '__envs', { + writable: true, + value: envs.parsed || {}, +}); + +// eslint-disable-next-line no-console +const consoleError = console.error; + +global.console = { + ...console, + error: (...args) => { + // silence some irrelevant errors + if (args.some((arg) => typeof arg === 'string' && arg.includes('Using kebab-case for css properties'))) { + return; + } + consoleError(...args); + }, +}; diff --git a/jest/utils/flushPromises.ts b/jest/utils/flushPromises.ts new file mode 100644 index 0000000000..2910c7deba --- /dev/null +++ b/jest/utils/flushPromises.ts @@ -0,0 +1,7 @@ +const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout; + +export default function flushPromises() { + return new Promise(function(resolve) { + scheduler(resolve); + }); +} diff --git a/lib/address/parseMetaPayload.ts b/lib/address/parseMetaPayload.ts new file mode 100644 index 0000000000..e8b067b148 --- /dev/null +++ b/lib/address/parseMetaPayload.ts @@ -0,0 +1,42 @@ +import type { AddressMetadataTag } from 'types/api/addressMetadata'; +import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; + +type MetaParsed = NonNullable; + +export default function parseMetaPayload(meta: AddressMetadataTag['meta']): AddressMetadataTagFormatted['meta'] { + try { + const parsedMeta = JSON.parse(meta || ''); + + if (typeof parsedMeta !== 'object' || parsedMeta === null || Array.isArray(parsedMeta)) { + throw new Error('Invalid JSON'); + } + + const result: AddressMetadataTagFormatted['meta'] = {}; + + const stringFields: Array = [ + 'textColor', + 'bgColor', + 'tagIcon', + 'tagUrl', + 'tooltipIcon', + 'tooltipTitle', + 'tooltipDescription', + 'tooltipUrl', + 'appID', + 'appMarketplaceURL', + 'appLogoURL', + 'appActionButtonText', + 'warpcastHandle', + ]; + + for (const stringField of stringFields) { + if (stringField in parsedMeta && typeof parsedMeta[stringField as keyof typeof parsedMeta] === 'string') { + result[stringField] = parsedMeta[stringField as keyof typeof parsedMeta]; + } + } + + return result; + } catch (error) { + return null; + } +} diff --git a/lib/address/useAddressMetadataInfoQuery.ts b/lib/address/useAddressMetadataInfoQuery.ts new file mode 100644 index 0000000000..14c22ab676 --- /dev/null +++ b/lib/address/useAddressMetadataInfoQuery.ts @@ -0,0 +1,35 @@ +import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; + +import parseMetaPayload from './parseMetaPayload'; + +export default function useAddressMetadataInfoQuery(addresses: Array, isEnabled = true) { + + const resource = 'address_metadata_info'; + + return useApiQuery(resource, { + queryParams: { + addresses, + chainId: config.chain.id, + tagsLimit: '20', + }, + queryOptions: { + enabled: isEnabled && addresses.length > 0 && config.features.addressMetadata.isEnabled, + select: (data) => { + const addresses = Object.entries(data.addresses) + .map(([ address, { tags, reputation } ]) => { + const formattedTags: Array = tags.map((tag) => ({ ...tag, meta: parseMetaPayload(tag.meta) })); + return [ address.toLowerCase(), { tags: formattedTags, reputation } ] as const; + }) + .reduce((result, item) => { + result[item[0]] = item[1]; + return result; + }, {} as AddressMetadataInfoFormatted['addresses']); + + return { addresses }; + }, + }, + }); +} diff --git a/lib/api/buildUrl.test.ts b/lib/api/buildUrl.test.ts new file mode 100644 index 0000000000..29ea283a95 --- /dev/null +++ b/lib/api/buildUrl.test.ts @@ -0,0 +1,43 @@ +import buildUrl from './buildUrl'; + +test('builds URL for resource without path params', () => { + const url = buildUrl('config_backend_version'); + expect(url).toBe('https://localhost:3003/api/v2/config/backend-version'); +}); + +test('builds URL for resource with path params', () => { + const url = buildUrl('block', { height_or_hash: '42' }); + expect(url).toBe('https://localhost:3003/api/v2/blocks/42'); +}); + +describe('falsy query parameters', () => { + test('leaves "false" as query parameter', () => { + const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: false }); + expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=false'); + }); + + test('leaves "null" as query parameter', () => { + const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null }); + expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null'); + }); + + test('strips out empty string as query parameter', () => { + const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null, sort: '' }); + expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null'); + }); + + test('strips out "undefined" as query parameter', () => { + const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null, sort: undefined }); + expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null'); + }); +}); + +test('builds URL with array-like query parameters', () => { + const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: [ '0x11', '0x22' ], sort: 'asc' }); + expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=0x11%2C0x22&sort=asc'); +}); + +test('builds URL for resource with custom API endpoint', () => { + const url = buildUrl('token_verified_info', { chainId: '42', hash: '0x11' }); + expect(url).toBe('https://localhost:3005/api/v1/chains/42/token-infos/0x11'); +}); diff --git a/lib/api/buildUrl.ts b/lib/api/buildUrl.ts new file mode 100644 index 0000000000..07805dbd9c --- /dev/null +++ b/lib/api/buildUrl.ts @@ -0,0 +1,26 @@ +import { compile } from 'path-to-regexp'; + +import config from 'configs/app'; + +import isNeedProxy from './isNeedProxy'; +import { RESOURCES } from './resources'; +import type { ApiResource, ResourceName, ResourcePathParams } from './resources'; + +export default function buildUrl( + resourceName: R, + pathParams?: ResourcePathParams, + queryParams?: Record | number | boolean | null | undefined>, +): string { + const resource: ApiResource = RESOURCES[resourceName]; + const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint); + const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath; + const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path; + const url = new URL(compile(path)(pathParams), baseUrl); + + queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { + // there are some pagination params that can be null or false for the next page + value !== undefined && value !== '' && url.searchParams.append(key, String(value)); + }); + + return url.toString(); +} diff --git a/lib/api/isBodyAllowed.ts b/lib/api/isBodyAllowed.ts new file mode 100644 index 0000000000..aad52fedb7 --- /dev/null +++ b/lib/api/isBodyAllowed.ts @@ -0,0 +1,3 @@ +export default function isBodyAllowed(method: string | undefined | null) { + return method && ![ 'GET', 'HEAD' ].includes(method); +} diff --git a/lib/api/isNeedProxy.ts b/lib/api/isNeedProxy.ts new file mode 100644 index 0000000000..a7705091e4 --- /dev/null +++ b/lib/api/isNeedProxy.ts @@ -0,0 +1,13 @@ +import config from 'configs/app'; + +// FIXME +// I was not able to figure out how to send CORS with credentials from localhost +// unsuccessfully tried different ways, even custom local dev domain +// so for local development we have to use next.js api as proxy server +export default function isNeedProxy() { + if (config.app.useProxy) { + return true; + } + + return config.app.host === 'localhost' && config.app.host !== config.api.host; +} diff --git a/lib/api/resources.ts b/lib/api/resources.ts new file mode 100644 index 0000000000..3f1d6cf6f1 --- /dev/null +++ b/lib/api/resources.ts @@ -0,0 +1,1147 @@ +import type * as bens from '@blockscout/bens-types'; +import type * as stats from '@blockscout/stats-types'; +import type * as visualizer from '@blockscout/visualizer-types'; +import { getFeaturePayload } from 'configs/app/features/types'; +import type { + UserInfo, + CustomAbis, + ApiKeys, + VerifiedAddressResponse, + TokenInfoApplicationConfig, + TokenInfoApplications, + WatchlistResponse, + TransactionTagsResponse, + AddressTagsResponse, +} from 'types/api/account'; +import type { + Address, + AddressCounters, + AddressTabsCounters, + AddressTransactionsResponse, + AddressTokenTransferResponse, + AddressCoinBalanceHistoryResponse, + AddressCoinBalanceHistoryChart, + AddressBlocksValidatedResponse, + AddressInternalTxsResponse, + AddressTxsFilters, + AddressTokenTransferFilters, + AddressTokensFilter, + AddressTokensResponse, + AddressWithdrawalsResponse, + AddressNFTsResponse, + AddressCollectionsResponse, + AddressNFTTokensFilter, + AddressCoinBalanceHistoryChartOld, + AddressMudTables, + AddressMudTablesFilter, + AddressMudRecords, + AddressMudRecordsFilter, + AddressMudRecordsSorting, + AddressMudRecord, +} from 'types/api/address'; +import type { AddressesResponse } from 'types/api/addresses'; +import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; +import type { + ArbitrumL2MessagesResponse, + ArbitrumL2TxnBatch, + ArbitrumL2TxnBatchesResponse, + ArbitrumL2BatchTxs, + ArbitrumL2BatchBlocks, +} from 'types/api/arbitrumL2'; +import type { TxBlobs, Blob } from 'types/api/blobs'; +import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block'; +import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; +import type { BackendVersionConfig } from 'types/api/configs'; +import type { + SmartContract, + SmartContractVerificationConfigRaw, + SolidityscanReport, + SmartContractSecurityAudits, +} from 'types/api/contract'; +import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; +import type { + EnsAddressLookupFilters, + EnsDomainLookupFilters, + EnsLookupSorting, +} from 'types/api/ens'; +import type { IndexingStatus } from 'types/api/indexingStatus'; +import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; +import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; +import type { MudWorldsResponse } from 'types/api/mudWorlds'; +import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves'; +import type { + OptimisticL2DepositsResponse, + OptimisticL2DepositsItem, + OptimisticL2OutputRootsResponse, + OptimisticL2TxnBatchesResponse, + OptimisticL2WithdrawalsResponse, + OptimisticL2DisputeGamesResponse, +} from 'types/api/optimisticL2'; +import type { RawTracesResponse } from 'types/api/rawTrace'; +import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; +import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; +import type { HomeStats } from 'types/api/stats'; +import type { + TokenCounters, + TokenInfo, + TokenHolders, + TokenInventoryResponse, + TokenInstance, + TokenInstanceTransfersCount, + TokenVerifiedInfo, + TokenInventoryFilters, +} from 'types/api/token'; +import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens'; +import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; +import type { + TransactionsResponseValidated, + TransactionsResponsePending, + Transaction, + TransactionsResponseWatchlist, + TransactionsSorting, + TransactionsResponseWithBlobs, + TransactionsStats, +} from 'types/api/transaction'; +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; +import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters'; +import type { TxStateChanges } from 'types/api/txStateChanges'; +import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps'; +import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ValidatorsSorting } from 'types/api/validators'; +import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; +import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; +import type { + ZkEvmL2DepositsResponse, + ZkEvmL2TxnBatch, + ZkEvmL2TxnBatchesItem, + ZkEvmL2TxnBatchesResponse, + ZkEvmL2TxnBatchTxs, + ZkEvmL2WithdrawalsResponse, +} from 'types/api/zkEvmL2'; +import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2'; +import type { MarketplaceAppOverview } from 'types/client/marketplace'; +import type { ArrayElement } from 'types/utils'; + +import config from 'configs/app'; + +const marketplaceFeature = getFeaturePayload(config.features.marketplace); +const marketplaceApi = marketplaceFeature && 'api' in marketplaceFeature ? marketplaceFeature.api : undefined; + +export interface ApiResource { + path: ResourcePath; + endpoint?: string; + basePath?: string; + pathParams?: Array; + needAuth?: boolean; // for external APIs which require authentication + headers?: RequestInit['headers']; +} + +export const SORTING_FIELDS = [ 'sort', 'order' ]; + +export const RESOURCES = { + // ACCOUNT + csrf: { + path: '/api/account/v2/get_csrf', + }, + user_info: { + path: '/api/account/v2/user/info', + }, + email_resend: { + path: '/api/account/v2/email/resend', + }, + custom_abi: { + path: '/api/account/v2/user/custom_abis/:id?', + pathParams: [ 'id' as const ], + }, + watchlist: { + path: '/api/account/v2/user/watchlist/:id?', + pathParams: [ 'id' as const ], + filterFields: [ ], + }, + private_tags_address: { + path: '/api/account/v2/user/tags/address/:id?', + pathParams: [ 'id' as const ], + filterFields: [ ], + }, + private_tags_tx: { + path: '/api/account/v2/user/tags/transaction/:id?', + pathParams: [ 'id' as const ], + filterFields: [ ], + }, + api_keys: { + path: '/api/account/v2/user/api_keys/:id?', + pathParams: [ 'id' as const ], + }, + + // ACCOUNT: ADDRESS VERIFICATION & TOKEN INFO + address_verification: { + path: '/api/v1/chains/:chainId/verified-addresses:type', + pathParams: [ 'chainId' as const, 'type' as const ], + endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint, + basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath, + needAuth: true, + }, + + verified_addresses: { + path: '/api/v1/chains/:chainId/verified-addresses', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint, + basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath, + needAuth: true, + }, + + token_info_applications_config: { + path: '/api/v1/chains/:chainId/token-info-submissions/selectors', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath, + needAuth: true, + }, + + token_info_applications: { + path: '/api/v1/chains/:chainId/token-info-submissions/:id?', + pathParams: [ 'chainId' as const, 'id' as const ], + endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath, + needAuth: true, + }, + + // STATS MICROSERVICE API + stats_counters: { + path: '/api/v1/counters', + endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, + basePath: getFeaturePayload(config.features.stats)?.api.basePath, + }, + stats_lines: { + path: '/api/v1/lines', + endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, + basePath: getFeaturePayload(config.features.stats)?.api.basePath, + }, + stats_line: { + path: '/api/v1/lines/:id', + pathParams: [ 'id' as const ], + endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, + basePath: getFeaturePayload(config.features.stats)?.api.basePath, + }, + + // NAME SERVICE + addresses_lookup: { + path: '/api/v1/:chainId/addresses\\:lookup', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const, 'protocols' as const ], + }, + domain_info: { + path: '/api/v1/:chainId/domains/:name', + pathParams: [ 'chainId' as const, 'name' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + }, + domain_events: { + path: '/api/v1/:chainId/domains/:name/events', + pathParams: [ 'chainId' as const, 'name' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + }, + domains_lookup: { + path: '/api/v1/:chainId/domains\\:lookup', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + filterFields: [ 'name' as const, 'only_active' as const, 'protocols' as const ], + }, + domain_protocols: { + path: '/api/v1/:chainId/protocols', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + }, + + // METADATA SERVICE & PUBLIC TAGS + address_metadata_info: { + path: '/api/v1/metadata', + endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath, + }, + address_metadata_tag_search: { + path: '/api/v1/tags:search', + endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath, + }, + address_metadata_tag_types: { + path: '/api/v1/public-tag-types', + endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, + basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath, + }, + public_tag_application: { + path: '/api/v1/chains/:chainId/metadata-submissions/tag', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.publicTagsSubmission)?.api.endpoint, + basePath: getFeaturePayload(config.features.publicTagsSubmission)?.api.basePath, + }, + + // VISUALIZATION + visualize_sol2uml: { + path: '/api/v1/solidity\\:visualize-contracts', + endpoint: getFeaturePayload(config.features.sol2uml)?.api.endpoint, + basePath: getFeaturePayload(config.features.sol2uml)?.api.basePath, + }, + + // MARKETPLACE + marketplace_dapps: { + path: '/api/v1/chains/:chainId/marketplace/dapps', + pathParams: [ 'chainId' as const ], + endpoint: marketplaceApi?.endpoint, + basePath: marketplaceApi?.basePath, + }, + marketplace_dapp: { + path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId', + pathParams: [ 'chainId' as const, 'dappId' as const ], + endpoint: marketplaceApi?.endpoint, + basePath: marketplaceApi?.basePath, + }, + + // BLOCKS, TXS + blocks: { + path: '/api/v2/blocks', + filterFields: [ 'type' as const ], + }, + block: { + path: '/api/v2/blocks/:height_or_hash', + pathParams: [ 'height_or_hash' as const ], + }, + block_txs: { + path: '/api/v2/blocks/:height_or_hash/transactions', + pathParams: [ 'height_or_hash' as const ], + filterFields: [ 'type' as const ], + }, + block_withdrawals: { + path: '/api/v2/blocks/:height_or_hash/withdrawals', + pathParams: [ 'height_or_hash' as const ], + filterFields: [], + }, + txs_stats: { + path: '/api/v2/transactions/stats', + }, + txs_validated: { + path: '/api/v2/transactions', + filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], + }, + txs_pending: { + path: '/api/v2/transactions', + filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], + }, + txs_with_blobs: { + path: '/api/v2/transactions', + filterFields: [ 'type' as const ], + }, + txs_watchlist: { + path: '/api/v2/transactions/watchlist', + filterFields: [ ], + }, + txs_execution_node: { + path: '/api/v2/transactions/execution-node/:hash', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + tx: { + path: '/api/v2/transactions/:hash', + pathParams: [ 'hash' as const ], + }, + tx_internal_txs: { + path: '/api/v2/transactions/:hash/internal-transactions', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + tx_logs: { + path: '/api/v2/transactions/:hash/logs', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + tx_token_transfers: { + path: '/api/v2/transactions/:hash/token-transfers', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + }, + tx_raw_trace: { + path: '/api/v2/transactions/:hash/raw-trace', + pathParams: [ 'hash' as const ], + }, + tx_state_changes: { + path: '/api/v2/transactions/:hash/state-changes', + pathParams: [ 'hash' as const ], + filterFields: [], + }, + tx_blobs: { + path: '/api/v2/transactions/:hash/blobs', + pathParams: [ 'hash' as const ], + }, + tx_interpretation: { + path: '/api/v2/transactions/:hash/summary', + pathParams: [ 'hash' as const ], + }, + withdrawals: { + path: '/api/v2/withdrawals', + filterFields: [], + }, + withdrawals_counters: { + path: '/api/v2/withdrawals/counters', + }, + + // ADDRESSES + addresses: { + path: '/api/v2/addresses/', + filterFields: [ ], + }, + + // ADDRESS + address: { + path: '/api/v2/addresses/:hash', + pathParams: [ 'hash' as const ], + }, + address_counters: { + path: '/api/v2/addresses/:hash/counters', + pathParams: [ 'hash' as const ], + }, + address_tabs_counters: { + path: '/api/v2/addresses/:hash/tabs-counters', + pathParams: [ 'hash' as const ], + }, + // this resource doesn't have pagination, so causing huge problems on some addresses page + // address_token_balances: { + // path: '/api/v2/addresses/:hash/token-balances', + // }, + address_txs: { + path: '/api/v2/addresses/:hash/transactions', + pathParams: [ 'hash' as const ], + filterFields: [ 'filter' as const ], + }, + address_internal_txs: { + path: '/api/v2/addresses/:hash/internal-transactions', + pathParams: [ 'hash' as const ], + filterFields: [ 'filter' as const ], + }, + address_token_transfers: { + path: '/api/v2/addresses/:hash/token-transfers', + pathParams: [ 'hash' as const ], + filterFields: [ 'filter' as const, 'type' as const, 'token' as const ], + }, + address_blocks_validated: { + path: '/api/v2/addresses/:hash/blocks-validated', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + address_coin_balance: { + path: '/api/v2/addresses/:hash/coin-balance-history', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + address_coin_balance_chart: { + path: '/api/v2/addresses/:hash/coin-balance-history-by-day', + pathParams: [ 'hash' as const ], + }, + address_logs: { + path: '/api/v2/addresses/:hash/logs', + pathParams: [ 'hash' as const ], + filterFields: [ ], + }, + address_tokens: { + path: '/api/v2/addresses/:hash/tokens', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + }, + address_nfts: { + path: '/api/v2/addresses/:hash/nft', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + }, + address_collections: { + path: '/api/v2/addresses/:hash/nft/collections', + pathParams: [ 'hash' as const ], + filterFields: [ 'type' as const ], + }, + address_withdrawals: { + path: '/api/v2/addresses/:hash/withdrawals', + pathParams: [ 'hash' as const ], + filterFields: [], + }, + + // CONTRACT + contract: { + path: '/api/v2/smart-contracts/:hash', + pathParams: [ 'hash' as const ], + }, + contract_verification_config: { + path: '/api/v2/smart-contracts/verification/config', + }, + contract_verification_via: { + path: '/api/v2/smart-contracts/:hash/verification/via/:method', + pathParams: [ 'hash' as const, 'method' as const ], + }, + contract_solidityscan_report: { + path: '/api/v2/smart-contracts/:hash/solidityscan-report', + pathParams: [ 'hash' as const ], + }, + contract_security_audits: { + path: '/api/v2/smart-contracts/:hash/audit-reports', + pathParams: [ 'hash' as const ], + }, + + verified_contracts: { + path: '/api/v2/smart-contracts', + filterFields: [ 'q' as const, 'filter' as const ], + }, + verified_contracts_counters: { + path: '/api/v2/smart-contracts/counters', + }, + + // TOKEN + token: { + path: '/api/v2/tokens/:hash', + pathParams: [ 'hash' as const ], + }, + token_verified_info: { + path: '/api/v1/chains/:chainId/token-infos/:hash', + pathParams: [ 'chainId' as const, 'hash' as const ], + endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint, + basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath, + }, + token_counters: { + path: '/api/v2/tokens/:hash/counters', + pathParams: [ 'hash' as const ], + }, + token_holders: { + path: '/api/v2/tokens/:hash/holders', + pathParams: [ 'hash' as const ], + filterFields: [], + }, + token_transfers: { + path: '/api/v2/tokens/:hash/transfers', + pathParams: [ 'hash' as const ], + filterFields: [], + }, + token_inventory: { + path: '/api/v2/tokens/:hash/instances', + pathParams: [ 'hash' as const ], + filterFields: [ 'holder_address_hash' as const ], + }, + tokens: { + path: '/api/v2/tokens', + filterFields: [ 'q' as const, 'type' as const ], + }, + tokens_bridged: { + path: '/api/v2/tokens/bridged', + filterFields: [ 'q' as const, 'chain_ids' as const ], + }, + + // TOKEN INSTANCE + token_instance: { + path: '/api/v2/tokens/:hash/instances/:id', + pathParams: [ 'hash' as const, 'id' as const ], + }, + token_instance_transfers_count: { + path: '/api/v2/tokens/:hash/instances/:id/transfers-count', + pathParams: [ 'hash' as const, 'id' as const ], + }, + token_instance_transfers: { + path: '/api/v2/tokens/:hash/instances/:id/transfers', + pathParams: [ 'hash' as const, 'id' as const ], + filterFields: [], + }, + token_instance_holders: { + path: '/api/v2/tokens/:hash/instances/:id/holders', + pathParams: [ 'hash' as const, 'id' as const ], + filterFields: [], + }, + token_instance_refresh_metadata: { + path: '/api/v2/tokens/:hash/instances/:id/refetch-metadata', + pathParams: [ 'hash' as const, 'id' as const ], + filterFields: [], + }, + + // APP STATS + stats: { + path: '/api/v2/stats', + headers: { + 'updated-gas-oracle': 'true', + }, + }, + stats_charts_txs: { + path: '/api/v2/stats/charts/transactions', + }, + stats_charts_market: { + path: '/api/v2/stats/charts/market', + }, + stats_charts_secondary_coin_price: { + path: '/api/v2/stats/charts/secondary-coin-market', + }, + + // HOMEPAGE + homepage_blocks: { + path: '/api/v2/main-page/blocks', + }, + homepage_deposits: { + path: '/api/v2/main-page/optimism-deposits', + }, + homepage_txs: { + path: '/api/v2/main-page/transactions', + }, + homepage_zkevm_l2_batches: { + path: '/api/v2/main-page/zkevm/batches/confirmed', + }, + homepage_txs_watchlist: { + path: '/api/v2/main-page/transactions/watchlist', + }, + homepage_indexing_status: { + path: '/api/v2/main-page/indexing-status', + }, + homepage_zkevm_latest_batch: { + path: '/api/v2/main-page/zkevm/batches/latest-number', + }, + homepage_zksync_latest_batch: { + path: '/api/v2/main-page/zksync/batches/latest-number', + }, + + // SEARCH + quick_search: { + path: '/api/v2/search/quick', + filterFields: [ 'q' ], + }, + search: { + path: '/api/v2/search', + filterFields: [ 'q' ], + }, + search_check_redirect: { + path: '/api/v2/search/check-redirect', + }, + + // optimistic L2 + optimistic_l2_deposits: { + path: '/api/v2/optimism/deposits', + filterFields: [], + }, + + optimistic_l2_deposits_count: { + path: '/api/v2/optimism/deposits/count', + }, + + optimistic_l2_withdrawals: { + path: '/api/v2/optimism/withdrawals', + filterFields: [], + }, + + optimistic_l2_withdrawals_count: { + path: '/api/v2/optimism/withdrawals/count', + }, + + optimistic_l2_output_roots: { + path: '/api/v2/optimism/output-roots', + filterFields: [], + }, + + optimistic_l2_output_roots_count: { + path: '/api/v2/optimism/output-roots/count', + }, + + optimistic_l2_txn_batches: { + path: '/api/v2/optimism/txn-batches', + filterFields: [], + }, + + optimistic_l2_txn_batches_count: { + path: '/api/v2/optimism/txn-batches/count', + }, + + optimistic_l2_dispute_games: { + path: '/api/v2/optimism/games', + filterFields: [], + }, + + optimistic_l2_dispute_games_count: { + path: '/api/v2/optimism/games/count', + }, + + // MUD worlds on optimism + mud_worlds: { + path: '/api/v2/mud/worlds', + filterFields: [], + }, + + address_mud_tables: { + path: '/api/v2/mud/worlds/:hash/tables', + pathParams: [ 'hash' as const ], + filterFields: [ 'q' as const ], + }, + + address_mud_tables_count: { + path: '/api/v2/mud/worlds/:hash/tables/count', + pathParams: [ 'hash' as const ], + }, + + address_mud_records: { + path: '/api/v2/mud/worlds/:hash/tables/:table_id/records', + pathParams: [ 'hash' as const, 'table_id' as const ], + filterFields: [ 'filter_key0' as const, 'filter_key1' as const ], + }, + + address_mud_record: { + path: '/api/v2/mud/worlds/:hash/tables/:table_id/records/:record_id', + pathParams: [ 'hash' as const, 'table_id' as const, 'record_id' as const ], + }, + + // arbitrum L2 + arbitrum_l2_messages: { + path: '/api/v2/arbitrum/messages/:direction', + pathParams: [ 'direction' as const ], + filterFields: [], + }, + + arbitrum_l2_messages_count: { + path: '/api/v2/arbitrum/messages/:direction/count', + pathParams: [ 'direction' as const ], + }, + + arbitrum_l2_txn_batches: { + path: '/api/v2/arbitrum/batches', + filterFields: [], + }, + + arbitrum_l2_txn_batches_count: { + path: '/api/v2/arbitrum/batches/count', + }, + + arbitrum_l2_txn_batch: { + path: '/api/v2/arbitrum/batches/:number', + pathParams: [ 'number' as const ], + }, + + arbitrum_l2_txn_batch_txs: { + path: '/api/v2/transactions/arbitrum-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + }, + + arbitrum_l2_txn_batch_blocks: { + path: '/api/v2/blocks/arbitrum-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + }, + + // zkEvm L2 + zkevm_l2_deposits: { + path: '/api/v2/zkevm/deposits', + filterFields: [], + }, + + zkevm_l2_deposits_count: { + path: '/api/v2/zkevm/deposits/count', + }, + + zkevm_l2_withdrawals: { + path: '/api/v2/zkevm/withdrawals', + filterFields: [], + }, + + zkevm_l2_withdrawals_count: { + path: '/api/v2/zkevm/withdrawals/count', + }, + + zkevm_l2_txn_batches: { + path: '/api/v2/zkevm/batches', + filterFields: [], + }, + + zkevm_l2_txn_batches_count: { + path: '/api/v2/zkevm/batches/count', + }, + + zkevm_l2_txn_batch: { + path: '/api/v2/zkevm/batches/:number', + pathParams: [ 'number' as const ], + }, + + zkevm_l2_txn_batch_txs: { + path: '/api/v2/transactions/zkevm-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + }, + + // zkSync L2 + zksync_l2_txn_batches: { + path: '/api/v2/zksync/batches', + filterFields: [], + }, + + zksync_l2_txn_batches_count: { + path: '/api/v2/zksync/batches/count', + }, + + zksync_l2_txn_batch: { + path: '/api/v2/zksync/batches/:number', + pathParams: [ 'number' as const ], + }, + + zksync_l2_txn_batch_txs: { + path: '/api/v2/transactions/zksync-batch/:number', + pathParams: [ 'number' as const ], + filterFields: [], + }, + + // SHIBARIUM L2 + shibarium_deposits: { + path: '/api/v2/shibarium/deposits', + filterFields: [], + }, + + shibarium_deposits_count: { + path: '/api/v2/shibarium/deposits/count', + }, + + shibarium_withdrawals: { + path: '/api/v2/shibarium/withdrawals', + filterFields: [], + }, + + shibarium_withdrawals_count: { + path: '/api/v2/shibarium/withdrawals/count', + }, + + // NOVES-FI + noves_transaction: { + path: '/api/v2/proxy/noves-fi/transactions/:hash', + pathParams: [ 'hash' as const ], + }, + noves_address_history: { + path: '/api/v2/proxy/noves-fi/addresses/:address/transactions', + pathParams: [ 'address' as const ], + filterFields: [], + }, + noves_describe_txs: { + path: '/api/v2/proxy/noves-fi/transaction-descriptions', + }, + + // USER OPS + user_ops: { + path: '/api/v2/proxy/account-abstraction/operations', + filterFields: [ 'transaction_hash' as const, 'sender' as const ], + }, + user_op: { + path: '/api/v2/proxy/account-abstraction/operations/:hash', + pathParams: [ 'hash' as const ], + }, + user_ops_account: { + path: '/api/v2/proxy/account-abstraction/accounts/:hash', + pathParams: [ 'hash' as const ], + }, + user_op_interpretation: { + path: '/api/v2/proxy/account-abstraction/operations/:hash/summary', + pathParams: [ 'hash' as const ], + }, + + // VALIDATORS + validators: { + path: '/api/v2/validators/:chainType', + pathParams: [ 'chainType' as const ], + filterFields: [ 'address_hash' as const, 'state_filter' as const ], + }, + validators_counters: { + path: '/api/v2/validators/:chainType/counters', + pathParams: [ 'chainType' as const ], + }, + + // BLOBS + blob: { + path: '/api/v2/blobs/:hash', + pathParams: [ 'hash' as const ], + }, + + // CONFIGS + config_backend_version: { + path: '/api/v2/config/backend-version', + }, + + // CSV EXPORT + csv_export_token_holders: { + path: '/api/v2/tokens/:hash/holders/csv', + pathParams: [ 'hash' as const ], + }, + + // OTHER + api_v2_key: { + path: '/api/v2/key', + }, + + // API V1 + csv_export_txs: { + path: '/api/v1/transactions-csv', + }, + csv_export_internal_txs: { + path: '/api/v1/internal-transactions-csv', + }, + csv_export_token_transfers: { + path: '/api/v1/token-transfers-csv', + }, + csv_export_logs: { + path: '/api/v1/logs-csv', + }, + graphql: { + path: '/api/v1/graphql', + }, + block_countdown: { + path: '/api', + }, +}; + +export type ResourceName = keyof typeof RESOURCES; + +type ResourcePathMap = { + [K in ResourceName]: typeof RESOURCES[K]['path'] +} +export type ResourcePath = ResourcePathMap[keyof ResourcePathMap] + +export type ResourceFiltersKey = typeof RESOURCES[R] extends {filterFields: Array} ? + ArrayElement : + never; + +export const resourceKey = (x: keyof typeof RESOURCES) => x; + +type ResourcePathParamName = + typeof RESOURCES[Resource] extends { pathParams: Array } ? + ArrayElement : + string; + +export type ResourcePathParams = typeof RESOURCES[Resource] extends { pathParams: Array } ? + Record, string | undefined> : + never; + +export interface ResourceError { + payload?: T; + status: Response['status']; + statusText: Response['statusText']; +} + +export type ResourceErrorAccount = ResourceError<{ errors: T }> + +export type PaginatedResources = 'blocks' | 'block_txs' | +'txs_validated' | 'txs_pending' | 'txs_with_blobs' | 'txs_watchlist' | 'txs_execution_node' | +'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | 'tx_blobs' | +'addresses' | +'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | +'search' | +'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | +'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | +'token_instance_transfers' | 'token_instance_holders' | +'verified_contracts' | +'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' | +'optimistic_l2_dispute_games' | +'mud_worlds'| 'address_mud_tables' | 'address_mud_records' | +'shibarium_deposits' | 'shibarium_withdrawals' | +'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' | +'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | +'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | +'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | +'watchlist' | 'private_tags_address' | 'private_tags_tx' | +'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history'; + +export type PaginatedResponse = ResourcePayload; + +/* eslint-disable @typescript-eslint/indent */ +// !!! IMPORTANT !!! +// Don't add any new types here because TypeScript cannot handle it properly +// use ResourcePayloadB instead +export type ResourcePayloadA = +Q extends 'user_info' ? UserInfo : +Q extends 'custom_abi' ? CustomAbis : +Q extends 'private_tags_address' ? AddressTagsResponse : +Q extends 'private_tags_tx' ? TransactionTagsResponse : +Q extends 'api_keys' ? ApiKeys : +Q extends 'watchlist' ? WatchlistResponse : +Q extends 'verified_addresses' ? VerifiedAddressResponse : +Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig : +Q extends 'token_info_applications' ? TokenInfoApplications : +Q extends 'stats' ? HomeStats : +Q extends 'stats_charts_txs' ? ChartTransactionResponse : +Q extends 'stats_charts_market' ? ChartMarketResponse : +Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse : +Q extends 'homepage_blocks' ? Array : +Q extends 'homepage_txs' ? Array : +Q extends 'homepage_txs_watchlist' ? Array : +Q extends 'homepage_deposits' ? Array : +Q extends 'homepage_zkevm_l2_batches' ? { items: Array } : +Q extends 'homepage_indexing_status' ? IndexingStatus : +Q extends 'homepage_zkevm_latest_batch' ? number : +Q extends 'homepage_zksync_latest_batch' ? number : +Q extends 'stats_counters' ? stats.Counters : +Q extends 'stats_lines' ? stats.LineCharts : +Q extends 'stats_line' ? stats.LineChart : +Q extends 'blocks' ? BlocksResponse : +Q extends 'block' ? Block : +Q extends 'block_countdown' ? BlockCountdownResponse : +Q extends 'block_txs' ? BlockTransactionsResponse : +Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : +Q extends 'txs_stats' ? TransactionsStats : +Q extends 'txs_validated' ? TransactionsResponseValidated : +Q extends 'txs_pending' ? TransactionsResponsePending : +Q extends 'txs_with_blobs' ? TransactionsResponseWithBlobs : +Q extends 'txs_watchlist' ? TransactionsResponseWatchlist : +Q extends 'txs_execution_node' ? TransactionsResponseValidated : +Q extends 'tx' ? Transaction : +Q extends 'tx_internal_txs' ? InternalTransactionsResponse : +Q extends 'tx_logs' ? LogsResponseTx : +Q extends 'tx_token_transfers' ? TokenTransferResponse : +Q extends 'tx_raw_trace' ? RawTracesResponse : +Q extends 'tx_state_changes' ? TxStateChanges : +Q extends 'tx_blobs' ? TxBlobs : +Q extends 'tx_interpretation' ? TxInterpretationResponse : +Q extends 'addresses' ? AddressesResponse : +Q extends 'address' ? Address : +Q extends 'address_counters' ? AddressCounters : +Q extends 'address_tabs_counters' ? AddressTabsCounters : +Q extends 'address_txs' ? AddressTransactionsResponse : +Q extends 'address_internal_txs' ? AddressInternalTxsResponse : +Q extends 'address_token_transfers' ? AddressTokenTransferResponse : +Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse : +Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : +Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChartOld | AddressCoinBalanceHistoryChart : +Q extends 'address_logs' ? LogsResponseAddress : +Q extends 'address_tokens' ? AddressTokensResponse : +Q extends 'address_nfts' ? AddressNFTsResponse : +Q extends 'address_collections' ? AddressCollectionsResponse : +Q extends 'address_withdrawals' ? AddressWithdrawalsResponse : +Q extends 'token' ? TokenInfo : +Q extends 'token_verified_info' ? TokenVerifiedInfo : +Q extends 'token_counters' ? TokenCounters : +Q extends 'token_transfers' ? TokenTransferResponse : +Q extends 'token_holders' ? TokenHolders : +Q extends 'token_instance' ? TokenInstance : +Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount : +Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse : +Q extends 'token_instance_holders' ? TokenHolders : +Q extends 'token_inventory' ? TokenInventoryResponse : +Q extends 'tokens' ? TokensResponse : +Q extends 'tokens_bridged' ? TokensResponse : +Q extends 'quick_search' ? Array : +Q extends 'search' ? SearchResult : +Q extends 'search_check_redirect' ? SearchRedirectResult : +Q extends 'contract' ? SmartContract : +Q extends 'contract_solidityscan_report' ? SolidityscanReport : +Q extends 'verified_contracts' ? VerifiedContractsResponse : +Q extends 'verified_contracts_counters' ? VerifiedContractsCounters : +Q extends 'visualize_sol2uml' ? visualizer.VisualizeResponse : +Q extends 'contract_verification_config' ? SmartContractVerificationConfigRaw : +Q extends 'withdrawals' ? WithdrawalsResponse : +Q extends 'withdrawals_counters' ? WithdrawalsCounters : +Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse : +Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse : +Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse : +Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse : +Q extends 'optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse : +Q extends 'optimistic_l2_output_roots_count' ? number : +Q extends 'optimistic_l2_withdrawals_count' ? number : +Q extends 'optimistic_l2_deposits_count' ? number : +Q extends 'optimistic_l2_txn_batches_count' ? number : +Q extends 'optimistic_l2_dispute_games_count' ? number : +never; +// !!! IMPORTANT !!! +// See comment above +/* eslint-enable @typescript-eslint/indent */ + +/* eslint-disable @typescript-eslint/indent */ +export type ResourcePayloadB = +Q extends 'config_backend_version' ? BackendVersionConfig : +Q extends 'address_metadata_info' ? AddressMetadataInfo : +Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse : +Q extends 'blob' ? Blob : +Q extends 'marketplace_dapps' ? Array : +Q extends 'marketplace_dapp' ? MarketplaceAppOverview : +Q extends 'validators' ? ValidatorsResponse : +Q extends 'validators_counters' ? ValidatorsCountersResponse : +Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : +Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : +Q extends 'shibarium_withdrawals_count' ? number : +Q extends 'shibarium_deposits_count' ? number : +Q extends 'arbitrum_l2_messages' ? ArbitrumL2MessagesResponse : +Q extends 'arbitrum_l2_messages_count' ? number : +Q extends 'arbitrum_l2_txn_batches' ? ArbitrumL2TxnBatchesResponse : +Q extends 'arbitrum_l2_txn_batches_count' ? number : +Q extends 'arbitrum_l2_txn_batch' ? ArbitrumL2TxnBatch : +Q extends 'arbitrum_l2_txn_batch_txs' ? ArbitrumL2BatchTxs : +Q extends 'arbitrum_l2_txn_batch_blocks' ? ArbitrumL2BatchBlocks : +Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse : +Q extends 'zkevm_l2_deposits_count' ? number : +Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse : +Q extends 'zkevm_l2_withdrawals_count' ? number : +Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse : +Q extends 'zkevm_l2_txn_batches_count' ? number : +Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : +Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : +Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse : +Q extends 'zksync_l2_txn_batches_count' ? number : +Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch : +Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs : +Q extends 'contract_security_audits' ? SmartContractSecurityAudits : +Q extends 'addresses_lookup' ? bens.LookupAddressResponse : +Q extends 'domain_info' ? bens.DetailedDomain : +Q extends 'domain_events' ? bens.ListDomainEventsResponse : +Q extends 'domains_lookup' ? bens.LookupDomainNameResponse : +Q extends 'domain_protocols' ? bens.GetProtocolsResponse : +Q extends 'user_ops' ? UserOpsResponse : +Q extends 'user_op' ? UserOp : +Q extends 'user_ops_account' ? UserOpsAccount : +Q extends 'user_op_interpretation'? TxInterpretationResponse : +Q extends 'noves_transaction' ? NovesResponseData : +Q extends 'noves_address_history' ? NovesAccountHistoryResponse : +Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse : +Q extends 'mud_worlds' ? MudWorldsResponse : +Q extends 'address_mud_tables' ? AddressMudTables : +Q extends 'address_mud_tables_count' ? number : +Q extends 'address_mud_records' ? AddressMudRecords : +Q extends 'address_mud_record' ? AddressMudRecord : +never; +/* eslint-enable @typescript-eslint/indent */ + +export type ResourcePayload = ResourcePayloadA | ResourcePayloadB; +export type PaginatedResponseItems = Q extends PaginatedResources ? ResourcePayloadA['items'] | ResourcePayloadB['items'] : never; +export type PaginatedResponseNextPageParams = Q extends PaginatedResources ? + ResourcePayloadA['next_page_params'] | ResourcePayloadB['next_page_params'] : + never; + +/* eslint-disable @typescript-eslint/indent */ +export type PaginationFilters = +Q extends 'blocks' ? BlockFilters : +Q extends 'block_txs' ? TTxsWithBlobsFilters : +Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : +Q extends 'txs_with_blobs' ? TTxsWithBlobsFilters : +Q extends 'tx_token_transfers' ? TokenTransferFilters : +Q extends 'token_transfers' ? TokenTransferFilters : +Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : +Q extends 'address_token_transfers' ? AddressTokenTransferFilters : +Q extends 'address_tokens' ? AddressTokensFilter : +Q extends 'address_nfts' ? AddressNFTTokensFilter : +Q extends 'address_collections' ? AddressNFTTokensFilter : +Q extends 'search' ? SearchResultFilters : +Q extends 'token_inventory' ? TokenInventoryFilters : +Q extends 'tokens' ? TokensFilters : +Q extends 'tokens_bridged' ? TokensBridgedFilters : +Q extends 'verified_contracts' ? VerifiedContractsFilters : +Q extends 'addresses_lookup' ? EnsAddressLookupFilters : +Q extends 'domains_lookup' ? EnsDomainLookupFilters : +Q extends 'user_ops' ? UserOpsFilters : +Q extends 'validators' ? ValidatorsFilters : +Q extends 'address_mud_tables' ? AddressMudTablesFilter : +Q extends 'address_mud_records' ? AddressMudRecordsFilter : +never; +/* eslint-enable @typescript-eslint/indent */ + +/* eslint-disable @typescript-eslint/indent */ +export type PaginationSorting = +Q extends 'tokens' ? TokensSorting : +Q extends 'tokens_bridged' ? TokensSorting : +Q extends 'verified_contracts' ? VerifiedContractsSorting : +Q extends 'address_txs' ? TransactionsSorting : +Q extends 'addresses_lookup' ? EnsLookupSorting : +Q extends 'domains_lookup' ? EnsLookupSorting : +Q extends 'validators' ? ValidatorsSorting : +Q extends 'address_mud_records' ? AddressMudRecordsSorting : +never; +/* eslint-enable @typescript-eslint/indent */ diff --git a/lib/api/useApiFetch.tsx b/lib/api/useApiFetch.tsx new file mode 100644 index 0000000000..85da773ab1 --- /dev/null +++ b/lib/api/useApiFetch.tsx @@ -0,0 +1,65 @@ +import { useQueryClient } from '@tanstack/react-query'; +import _omit from 'lodash/omit'; +import _pickBy from 'lodash/pickBy'; +import React from 'react'; + +import type { CsrfData } from 'types/client/account'; + +import config from 'configs/app'; +import isBodyAllowed from 'lib/api/isBodyAllowed'; +import isNeedProxy from 'lib/api/isNeedProxy'; +import { getResourceKey } from 'lib/api/useApiQuery'; +import * as cookies from 'lib/cookies'; +import type { Params as FetchParams } from 'lib/hooks/useFetch'; +import useFetch from 'lib/hooks/useFetch'; + +import buildUrl from './buildUrl'; +import { RESOURCES } from './resources'; +import type { ApiResource, ResourceName, ResourcePathParams } from './resources'; + +export interface Params { + pathParams?: ResourcePathParams; + queryParams?: Record | number | boolean | undefined>; + fetchParams?: Pick; +} + +export default function useApiFetch() { + const fetch = useFetch(); + const queryClient = useQueryClient(); + const { token: csrfToken } = queryClient.getQueryData(getResourceKey('csrf')) || {}; + + return React.useCallback(( + resourceName: R, + { pathParams, queryParams, fetchParams }: Params = {}, + ) => { + const apiToken = cookies.get(cookies.NAMES.API_TOKEN); + + const resource: ApiResource = RESOURCES[resourceName]; + const url = buildUrl(resourceName, pathParams, queryParams); + const withBody = isBodyAllowed(fetchParams?.method); + const headers = _pickBy({ + 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined, + Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined, + 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined, + ...resource.headers, + ...fetchParams?.headers, + }, Boolean) as HeadersInit; + + return fetch( + url, + { + // as of today, we use cookies only + // for user authentication in My account + // for API rate-limits (cannot use in the condition though, but we agreed with devops team that should not be an issue) + // change condition here if something is changed + credentials: config.features.account.isEnabled ? 'include' : 'same-origin', + headers, + ..._omit(fetchParams, 'headers'), + }, + { + resource: resource.path, + omitSentryErrorLog: true, // disable logging of API errors to Sentry + }, + ); + }, [ fetch, csrfToken ]); +} diff --git a/lib/api/useApiQuery.tsx b/lib/api/useApiQuery.tsx new file mode 100644 index 0000000000..91b2ab6ae5 --- /dev/null +++ b/lib/api/useApiQuery.tsx @@ -0,0 +1,37 @@ +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +import type { ResourceError, ResourceName, ResourcePayload } from './resources'; +import type { Params as ApiFetchParams } from './useApiFetch'; +import useApiFetch from './useApiFetch'; + +export interface Params> extends ApiFetchParams { + queryOptions?: Omit, ResourceError, D>, 'queryKey' | 'queryFn'>; +} + +export function getResourceKey(resource: R, { pathParams, queryParams }: Params = {}) { + if (pathParams || queryParams) { + return [ resource, { ...pathParams, ...queryParams } ]; + } + + return [ resource ]; +} + +export default function useApiQuery>( + resource: R, + { queryOptions, pathParams, queryParams, fetchParams }: Params = {}, +) { + const apiFetch = useApiFetch(); + + return useQuery, ResourceError, D>({ + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: getResourceKey(resource, { pathParams, queryParams }), + queryFn: async() => { + // all errors and error typing is handled by react-query + // so error response will never go to the data + // that's why we are safe here to do type conversion "as Promise>" + return apiFetch(resource, { pathParams, queryParams, fetchParams }) as Promise>; + }, + ...queryOptions, + }); +} diff --git a/lib/api/useQueryClientConfig.tsx b/lib/api/useQueryClientConfig.tsx new file mode 100644 index 0000000000..b7a8fd2550 --- /dev/null +++ b/lib/api/useQueryClientConfig.tsx @@ -0,0 +1,33 @@ +import { QueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; +import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; + +export const retry = (failureCount: number, error: unknown) => { + const errorPayload = getErrorObjPayload<{ status: number }>(error); + const status = errorPayload?.status || getErrorObjStatusCode(error); + if (status && status >= 400 && status < 500) { + // don't do retry for client error responses + return false; + } + return failureCount < 2; +}; + +export default function useQueryClientConfig() { + const [ queryClient ] = React.useState(() => new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry, + throwOnError: (error) => { + const status = getErrorObjStatusCode(error); + // don't catch error for "Too many requests" response + return status === 429; + }, + }, + }, + })); + + return queryClient; +} diff --git a/lib/bigint/compareBns.ts b/lib/bigint/compareBns.ts new file mode 100644 index 0000000000..34a86239c4 --- /dev/null +++ b/lib/bigint/compareBns.ts @@ -0,0 +1,13 @@ +import BigNumber from 'bignumber.js'; + +export default function compareBns(value1: string | number, value2: string | number) { + const value1Bn = new BigNumber(value1); + const value2Bn = new BigNumber(value2); + if (value1Bn.isGreaterThan(value2Bn)) { + return 1; + } + if (value1Bn.isLessThan(value2Bn)) { + return -1; + } + return 0; +} diff --git a/lib/bigint/sumBnReducer.ts b/lib/bigint/sumBnReducer.ts new file mode 100644 index 0000000000..6057853a96 --- /dev/null +++ b/lib/bigint/sumBnReducer.ts @@ -0,0 +1,5 @@ +import type BigNumber from 'bignumber.js'; + +export default function sumBnReducer(result: BigNumber, item: BigNumber) { + return result.plus(item); +} diff --git a/lib/blob/guessDataType.ts b/lib/blob/guessDataType.ts new file mode 100644 index 0000000000..fb409019e3 --- /dev/null +++ b/lib/blob/guessDataType.ts @@ -0,0 +1,12 @@ +import filetype from 'magic-bytes.js'; + +import hexToBytes from 'lib/hexToBytes'; + +import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes'; + +export default function guessDataType(data: string) { + const bytes = new Uint8Array(hexToBytes(data)); + const filteredBytes = removeNonSignificantZeroBytes(bytes); + + return filetype(filteredBytes)[0]; +} diff --git a/lib/blob/index.ts b/lib/blob/index.ts new file mode 100644 index 0000000000..ab178e8231 --- /dev/null +++ b/lib/blob/index.ts @@ -0,0 +1 @@ +export { default as guessDataType } from './guessDataType'; diff --git a/lib/blob/removeNonSignificantZeroBytes.ts b/lib/blob/removeNonSignificantZeroBytes.ts new file mode 100644 index 0000000000..9b25287478 --- /dev/null +++ b/lib/blob/removeNonSignificantZeroBytes.ts @@ -0,0 +1,20 @@ +export default function removeNonSignificantZeroBytes(bytes: Uint8Array) { + return shouldRemoveBytes(bytes) ? bytes.filter((item, index) => index % 32) : bytes; +} + +// check if every 0, 32, 64, etc byte is 0 in the provided array +function shouldRemoveBytes(bytes: Uint8Array) { + let result = true; + + for (let index = 0; index < bytes.length; index += 32) { + const element = bytes[index]; + if (element === 0) { + continue; + } else { + result = false; + break; + } + } + + return result; +} diff --git a/lib/block/getBlockReward.ts b/lib/block/getBlockReward.ts new file mode 100644 index 0000000000..3c00ac8d19 --- /dev/null +++ b/lib/block/getBlockReward.ts @@ -0,0 +1,18 @@ +import BigNumber from 'bignumber.js'; + +import type { Block } from 'types/api/block'; + +export default function getBlockReward(block: Block) { + const txFees = BigNumber(block.tx_fees || 0); + const burntFees = BigNumber(block.burnt_fees || 0); + const minerReward = block.rewards?.find(({ type }) => type === 'Miner Reward' || type === 'Validator Reward')?.reward; + const totalReward = BigNumber(minerReward || 0); + const staticReward = totalReward.minus(txFees).plus(burntFees); + + return { + totalReward, + staticReward, + txFees, + burntFees, + }; +} diff --git a/lib/block/getBlockTotalReward.ts b/lib/block/getBlockTotalReward.ts new file mode 100644 index 0000000000..3426b08501 --- /dev/null +++ b/lib/block/getBlockTotalReward.ts @@ -0,0 +1,13 @@ +import BigNumber from 'bignumber.js'; + +import type { Block } from 'types/api/block'; + +import { WEI, ZERO } from 'lib/consts'; + +export default function getBlockTotalReward(block: Block) { + const totalReward = block.rewards + ?.map(({ reward }) => BigNumber(reward)) + .reduce((result, item) => result.plus(item), ZERO) || ZERO; + + return totalReward.div(WEI); +} diff --git a/lib/bytesToBase64.ts b/lib/bytesToBase64.ts new file mode 100644 index 0000000000..60b23ad437 --- /dev/null +++ b/lib/bytesToBase64.ts @@ -0,0 +1,10 @@ +export default function bytesToBase64(bytes: Uint8Array) { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + + const base64String = btoa(binary); + + return base64String; +} diff --git a/lib/capitalizeFirstLetter.ts b/lib/capitalizeFirstLetter.ts new file mode 100644 index 0000000000..ae054d9423 --- /dev/null +++ b/lib/capitalizeFirstLetter.ts @@ -0,0 +1,7 @@ +export default function capitalizeFirstLetter(text: string) { + if (!text || !text.length) { + return ''; + } + + return text.charAt(0).toUpperCase() + text.slice(1); +} diff --git a/lib/consts.ts b/lib/consts.ts new file mode 100644 index 0000000000..10f889ad0b --- /dev/null +++ b/lib/consts.ts @@ -0,0 +1,19 @@ +import BigNumber from 'bignumber.js'; + +export const WEI = new BigNumber(10 ** 18); +export const GWEI = new BigNumber(10 ** 9); +export const WEI_IN_GWEI = WEI.dividedBy(GWEI); +export const ZERO = new BigNumber(0); + +export const SECOND = 1_000; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; +export const WEEK = 7 * DAY; +export const MONTH = 30 * DAY; +export const YEAR = 365 * DAY; + +export const Kb = 1_000; +export const Mb = 1_000 * Kb; + +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; diff --git a/lib/contexts/addressHighlight.tsx b/lib/contexts/addressHighlight.tsx new file mode 100644 index 0000000000..085e676ecc --- /dev/null +++ b/lib/contexts/addressHighlight.tsx @@ -0,0 +1,69 @@ +import React from 'react'; + +interface AddressHighlightProviderProps { + children: React.ReactNode; +} + +interface TAddressHighlightContext { + onMouseEnter: (event: React.MouseEvent) => void; + onMouseLeave: (event: React.MouseEvent) => void; +} + +export const AddressHighlightContext = React.createContext(null); + +export function AddressHighlightProvider({ children }: AddressHighlightProviderProps) { + const timeoutId = React.useRef(null); + const hashRef = React.useRef(null); + + const onMouseEnter = React.useCallback((event: React.MouseEvent) => { + const hash = event.currentTarget.getAttribute('data-hash'); + if (hash) { + hashRef.current = hash; + timeoutId.current = window.setTimeout(() => { + // for better performance we update DOM-nodes directly bypassing React reconciliation + const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`); + for (const node of nodes) { + node.classList.add('address-entity_highlighted'); + } + }, 100); + } + }, []); + + const onMouseLeave = React.useCallback(() => { + if (hashRef.current) { + const nodes = window.document.querySelectorAll(`[data-hash="${ hashRef.current }"]`); + for (const node of nodes) { + node.classList.remove('address-entity_highlighted'); + } + hashRef.current = null; + } + typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current); + }, []); + + const value = React.useMemo(() => { + return { + onMouseEnter, + onMouseLeave, + }; + }, [ onMouseEnter, onMouseLeave ]); + + React.useEffect(() => { + return () => { + typeof timeoutId.current === 'number' && window.clearTimeout(timeoutId.current); + }; + }, []); + + return ( + + { children } + + ); +} + +export function useAddressHighlightContext(disabled?: boolean) { + const context = React.useContext(AddressHighlightContext); + if (context === undefined || disabled) { + return null; + } + return context; +} diff --git a/lib/contexts/app.tsx b/lib/contexts/app.tsx new file mode 100644 index 0000000000..dbeceace4d --- /dev/null +++ b/lib/contexts/app.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext } from 'react'; + +import type { Route } from 'nextjs-routes'; +import type { Props as PageProps } from 'nextjs/getServerSideProps'; + +type Props = { + children: React.ReactNode; + pageProps: PageProps; +} + +const AppContext = createContext({ + cookies: '', + referrer: '', + query: {}, + adBannerProvider: null, + apiData: null, +}); + +export function AppContextProvider({ children, pageProps }: Props) { + return ( + + { children } + + ); +} + +export function useAppContext() { + return useContext>(AppContext); +} diff --git a/lib/contexts/chakra.tsx b/lib/contexts/chakra.tsx new file mode 100644 index 0000000000..d009d3b5f0 --- /dev/null +++ b/lib/contexts/chakra.tsx @@ -0,0 +1,26 @@ +import { + ChakraProvider as ChakraProviderDefault, + cookieStorageManagerSSR, + localStorageManager, +} from '@chakra-ui/react'; +import type { ChakraProviderProps } from '@chakra-ui/react'; +import React from 'react'; + +import theme from 'theme'; + +interface Props extends ChakraProviderProps { + cookies?: string; +} + +export function ChakraProvider({ cookies, children }: Props) { + const colorModeManager = + typeof cookies === 'string' ? + cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) : + localStorageManager; + + return ( + + { children } + + ); +} diff --git a/lib/contexts/marketplace.tsx b/lib/contexts/marketplace.tsx new file mode 100644 index 0000000000..2aba76e39b --- /dev/null +++ b/lib/contexts/marketplace.tsx @@ -0,0 +1,48 @@ +import { useRouter } from 'next/router'; +import React, { createContext, useContext, useEffect, useState, useMemo } from 'react'; + +type Props = { + children: React.ReactNode; +} + +type TMarketplaceContext = { + isAutoConnectDisabled: boolean; + setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void; +} + +const MarketplaceContext = createContext({ + isAutoConnectDisabled: false, + setIsAutoConnectDisabled: () => {}, +}); + +export function MarketplaceContextProvider({ children }: Props) { + const router = useRouter(); + const [ isAutoConnectDisabled, setIsAutoConnectDisabled ] = useState(false); + + useEffect(() => { + const handleRouteChange = () => { + setIsAutoConnectDisabled(false); + }; + + router.events.on('routeChangeStart', handleRouteChange); + + return () => { + router.events.off('routeChangeStart', handleRouteChange); + }; + }, [ router.events ]); + + const value = useMemo(() => ({ + isAutoConnectDisabled, + setIsAutoConnectDisabled, + }), [ isAutoConnectDisabled, setIsAutoConnectDisabled ]); + + return ( + + { children } + + ); +} + +export function useMarketplaceContext() { + return useContext(MarketplaceContext); +} diff --git a/lib/contexts/scrollDirection.tsx b/lib/contexts/scrollDirection.tsx new file mode 100644 index 0000000000..154b5438b4 --- /dev/null +++ b/lib/contexts/scrollDirection.tsx @@ -0,0 +1,58 @@ +import clamp from 'lodash/clamp'; +import throttle from 'lodash/throttle'; +import React from 'react'; + +const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null); +import isBrowser from 'lib/isBrowser'; + +const SCROLL_DIFF_THRESHOLD = 20; + +type Directions = 'up' | 'down'; + +interface Props { + children: React.ReactNode; +} + +export function ScrollDirectionProvider({ children }: Props) { + const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0); + const [ scrollDirection, setDirection ] = React.useState(null); + + const handleScroll = React.useCallback(() => { + const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight); + const scrollDiff = currentScrollPosition - prevScrollPosition.current; + + if (window.pageYOffset === 0) { + setDirection(null); + } else if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) { + setDirection(scrollDiff < 0 ? 'up' : 'down'); + } + + prevScrollPosition.current = currentScrollPosition; + }, [ ]); + + React.useEffect(() => { + const throttledHandleScroll = throttle(handleScroll, 300); + + window.addEventListener('scroll', throttledHandleScroll); + + return () => { + window.removeEventListener('scroll', throttledHandleScroll); + }; + // replicate componentDidMount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + { children } + + ); +} + +export function useScrollDirection() { + const context = React.useContext(ScrollDirectionContext); + if (context === undefined) { + throw new Error('useScrollDirection must be used within a ScrollDirectionProvider'); + } + return context; +} diff --git a/lib/contracts/licenses.ts b/lib/contracts/licenses.ts new file mode 100644 index 0000000000..123149e294 --- /dev/null +++ b/lib/contracts/licenses.ts @@ -0,0 +1,88 @@ +import type { ContractLicense } from 'types/client/contract'; + +export const CONTRACT_LICENSES: Array = [ + { + type: 'none', + label: 'None', + title: 'No License', + url: 'https://choosealicense.com/no-permission/', + }, + { + type: 'unlicense', + label: 'Unlicense', + title: 'The Unlicense', + url: 'https://choosealicense.com/licenses/unlicense/', + }, + { + type: 'mit', + label: 'MIT', + title: 'MIT License', + url: 'https://choosealicense.com/licenses/mit/', + }, + { + type: 'gnu_gpl_v2', + label: 'GNU GPLv2', + title: 'GNU General Public License v2.0', + url: 'https://choosealicense.com/licenses/gpl-2.0/', + }, + { + type: 'gnu_gpl_v3', + label: 'GNU GPLv3', + title: 'GNU General Public License v3.0', + url: 'https://choosealicense.com/licenses/gpl-3.0/', + }, + { + type: 'gnu_lgpl_v2_1', + label: 'GNU LGPLv2.1', + title: 'GNU Lesser General Public License v2.1', + url: 'https://choosealicense.com/licenses/lgpl-2.1/', + }, + { + type: 'gnu_lgpl_v3', + label: 'GNU LGPLv3', + title: 'GNU Lesser General Public License v3.0', + url: 'https://choosealicense.com/licenses/lgpl-3.0/', + }, + { + type: 'bsd_2_clause', + label: 'BSD-2-Clause', + title: 'BSD 2-clause "Simplified" license', + url: 'https://choosealicense.com/licenses/bsd-2-clause/', + }, + { + type: 'bsd_3_clause', + label: 'BSD-3-Clause', + title: 'BSD 3-clause "New" Or "Revised" license', + url: 'https://choosealicense.com/licenses/bsd-3-clause/', + }, + { + type: 'mpl_2_0', + label: 'MPL-2.0', + title: 'Mozilla Public License 2.0', + url: 'https://choosealicense.com/licenses/mpl-2.0/', + }, + { + type: 'osl_3_0', + label: 'OSL-3.0', + title: 'Open Software License 3.0', + url: 'https://choosealicense.com/licenses/osl-3.0/', + }, + { + type: 'apache_2_0', + label: 'Apache', + title: 'Apache 2.0', + url: 'https://choosealicense.com/licenses/apache-2.0/', + }, + { + type: 'gnu_agpl_v3', + label: 'GNU AGPLv3', + title: 'GNU Affero General Public License', + url: 'https://choosealicense.com/licenses/agpl-3.0/', + }, + { + type: 'bsl_1_1', + label: 'BSL 1.1', + title: 'Business Source License', + url: 'https://mariadb.com/bsl11/', + }, +]; diff --git a/lib/cookies.ts b/lib/cookies.ts new file mode 100644 index 0000000000..cef6a38a42 --- /dev/null +++ b/lib/cookies.ts @@ -0,0 +1,39 @@ +import Cookies from 'js-cookie'; + +import isBrowser from './isBrowser'; + +export enum NAMES { + NAV_BAR_COLLAPSED='nav_bar_collapsed', + API_TOKEN='_explorer_key', + INVALID_SESSION='invalid_session', + CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed', + TXS_SORT='txs_sort', + COLOR_MODE='chakra-ui-color-mode', + COLOR_MODE_HEX='chakra-ui-color-mode-hex', + ADDRESS_IDENTICON_TYPE='address_identicon_type', + INDEXING_ALERT='indexing_alert', + ADBLOCK_DETECTED='adblock_detected', + MIXPANEL_DEBUG='_mixpanel_debug', + ADDRESS_NFT_DISPLAY_TYPE='address_nft_display_type', + UUID='uuid', +} + +export function get(name?: NAMES | undefined | null, serverCookie?: string) { + if (!isBrowser()) { + return serverCookie ? getFromCookieString(serverCookie, name) : undefined; + } + + if (name) { + return Cookies.get(name); + } +} + +export function set(name: string, value: string, attributes: Cookies.CookieAttributes = {}) { + attributes.path = '/'; + + return Cookies.set(name, value, attributes); +} + +export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) { + return cookieString.split(`${ name }=`)[1]?.split(';')[0]; +} diff --git a/lib/date/dayjs.ts b/lib/date/dayjs.ts new file mode 100644 index 0000000000..e0191ce1e0 --- /dev/null +++ b/lib/date/dayjs.ts @@ -0,0 +1,65 @@ +// eslint-disable-next-line no-restricted-imports +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; +import minMax from 'dayjs/plugin/minMax'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import updateLocale from 'dayjs/plugin/updateLocale'; +import weekOfYear from 'dayjs/plugin/weekOfYear'; + +import { nbsp } from 'lib/html-entities'; + +const relativeTimeConfig = { + thresholds: [ + { l: 's', r: 1 }, + { l: 'ss', r: 59, d: 'second' }, + { l: 'm', r: 1 }, + { l: 'mm', r: 59, d: 'minute' }, + { l: 'h', r: 1 }, + { l: 'hh', r: 23, d: 'hour' }, + { l: 'd', r: 1 }, + { l: 'dd', r: 6, d: 'day' }, + { l: 'w', r: 1 }, + { l: 'ww', r: 4, d: 'week' }, + { l: 'M', r: 1 }, + { l: 'MM', r: 11, d: 'month' }, + { l: 'y', r: 17 }, + { l: 'yy', d: 'year' }, + ], +}; + +dayjs.extend(relativeTime, relativeTimeConfig); +dayjs.extend(updateLocale); +dayjs.extend(localizedFormat); +dayjs.extend(duration); +dayjs.extend(weekOfYear); +dayjs.extend(minMax); + +dayjs.updateLocale('en', { + formats: { + llll: `MMM DD YYYY HH:mm:ss A (Z${ nbsp }UTC)`, + lll: 'MMM D, YYYY h:mm A', + }, + relativeTime: { + s: '1s', + ss: '%ds', + future: 'in %s', + past: '%s ago', + m: '1m', + mm: '%dm', + h: '1h', + hh: '%dh', + d: '1d', + dd: '%dd', + w: '1w', + ww: '%dw', + M: '1mo', + MM: '%dmo', + y: '1y', + yy: '%dy', + }, +}); + +dayjs.locale('en'); + +export default dayjs; diff --git a/lib/delay.ts b/lib/delay.ts new file mode 100644 index 0000000000..efd2e216a0 --- /dev/null +++ b/lib/delay.ts @@ -0,0 +1,3 @@ +export default function delay(time: number) { + return new Promise((resolve) => window.setTimeout(resolve, time)); +} diff --git a/lib/downloadBlob.ts b/lib/downloadBlob.ts new file mode 100644 index 0000000000..1a20dc6236 --- /dev/null +++ b/lib/downloadBlob.ts @@ -0,0 +1,10 @@ +export default function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', filename); + link.click(); + + link.remove(); + URL.revokeObjectURL(url); +} diff --git a/lib/errors/getErrorCause.ts b/lib/errors/getErrorCause.ts new file mode 100644 index 0000000000..5b553a7018 --- /dev/null +++ b/lib/errors/getErrorCause.ts @@ -0,0 +1,8 @@ +export default function getErrorCause(error: Error | undefined): Record | undefined { + return ( + error && 'cause' in error && + typeof error.cause === 'object' && error.cause !== null && + error.cause as Record + ) || + undefined; +} diff --git a/lib/errors/getErrorCauseStatusCode.ts b/lib/errors/getErrorCauseStatusCode.ts new file mode 100644 index 0000000000..f9884f03cd --- /dev/null +++ b/lib/errors/getErrorCauseStatusCode.ts @@ -0,0 +1,6 @@ +import getErrorCause from './getErrorCause'; + +export default function getErrorCauseStatusCode(error: Error | undefined): number | undefined { + const cause = getErrorCause(error); + return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined; +} diff --git a/lib/errors/getErrorObj.ts b/lib/errors/getErrorObj.ts new file mode 100644 index 0000000000..7a06fdc6cd --- /dev/null +++ b/lib/errors/getErrorObj.ts @@ -0,0 +1,15 @@ +export default function getErrorObj(error: unknown) { + if (typeof error !== 'object') { + return; + } + + if (Array.isArray(error)) { + return; + } + + if (error === null) { + return; + } + + return error; +} diff --git a/lib/errors/getErrorObjPayload.ts b/lib/errors/getErrorObjPayload.ts new file mode 100644 index 0000000000..524979fe41 --- /dev/null +++ b/lib/errors/getErrorObjPayload.ts @@ -0,0 +1,23 @@ +import getErrorObj from './getErrorObj'; + +export default function getErrorObjPayload(error: unknown): Payload | undefined { + const errorObj = getErrorObj(error); + + if (!errorObj || !('payload' in errorObj)) { + return; + } + + if (typeof errorObj.payload !== 'object') { + return; + } + + if (errorObj === null) { + return; + } + + if (Array.isArray(errorObj)) { + return; + } + + return errorObj.payload as Payload; +} diff --git a/lib/errors/getErrorObjStatusCode.ts b/lib/errors/getErrorObjStatusCode.ts new file mode 100644 index 0000000000..a672b9d7a4 --- /dev/null +++ b/lib/errors/getErrorObjStatusCode.ts @@ -0,0 +1,11 @@ +import getErrorObj from './getErrorObj'; + +export default function getErrorObjStatusCode(error: unknown) { + const errorObj = getErrorObj(error); + + if (!errorObj || !('status' in errorObj) || typeof errorObj.status !== 'number') { + return; + } + + return errorObj.status; +} diff --git a/lib/errors/getResourceErrorPayload.tsx b/lib/errors/getResourceErrorPayload.tsx new file mode 100644 index 0000000000..bbe3981a38 --- /dev/null +++ b/lib/errors/getResourceErrorPayload.tsx @@ -0,0 +1,9 @@ +import type { ResourceError } from 'lib/api/resources'; + +import getErrorCause from './getErrorCause'; + +export default function getResourceErrorPayload | string>(error: Error | undefined): +ResourceError['payload'] | undefined { + const cause = getErrorCause(error); + return cause && 'payload' in cause ? cause.payload as ResourceError['payload'] : undefined; +} diff --git a/lib/errors/throwOnAbsentParamError.ts b/lib/errors/throwOnAbsentParamError.ts new file mode 100644 index 0000000000..5978b600a5 --- /dev/null +++ b/lib/errors/throwOnAbsentParamError.ts @@ -0,0 +1,5 @@ +export default function throwOnAbsentParamError(param: unknown) { + if (!param) { + throw new Error('Required param not provided', { cause: { status: 404 } }); + } +} diff --git a/lib/errors/throwOnResourceLoadError.ts b/lib/errors/throwOnResourceLoadError.ts new file mode 100644 index 0000000000..32e5169fc7 --- /dev/null +++ b/lib/errors/throwOnResourceLoadError.ts @@ -0,0 +1,19 @@ +import type { ResourceError, ResourceName } from 'lib/api/resources'; + +type Params = ({ + isError: true; + error: ResourceError; +} | { + isError: false; + error: null; +}) & { + resource?: ResourceName; +} + +export const RESOURCE_LOAD_ERROR_MESSAGE = 'Resource load error'; + +export default function throwOnResourceLoadError({ isError, error, resource }: Params) { + if (isError) { + throw Error(RESOURCE_LOAD_ERROR_MESSAGE, { cause: { ...error, resource } as unknown as Error }); + } +} diff --git a/lib/escapeRegExp.ts b/lib/escapeRegExp.ts new file mode 100644 index 0000000000..a92ec80037 --- /dev/null +++ b/lib/escapeRegExp.ts @@ -0,0 +1,3 @@ +export default function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} diff --git a/lib/getArbitrumVerificationStepStatus.ts b/lib/getArbitrumVerificationStepStatus.ts new file mode 100644 index 0000000000..e86114d115 --- /dev/null +++ b/lib/getArbitrumVerificationStepStatus.ts @@ -0,0 +1,25 @@ +import type { ArbitrumBatchStatus, ArbitrumL2TxData } from 'types/api/arbitrumL2'; + +type Args = { + status: ArbitrumBatchStatus; + commitment_transaction: ArbitrumL2TxData; + confirmation_transaction: ArbitrumL2TxData; +} + +export default function getArbitrumVerificationStepStatus({ + status, + commitment_transaction: commitTx, + confirmation_transaction: confirmTx, +}: Args) { + if (status === 'Sent to base') { + if (commitTx.status === 'unfinalized') { + return 'pending'; + } + } + if (status === 'Confirmed on base') { + if (confirmTx.status === 'unfinalized') { + return 'pending'; + } + } + return 'finalized'; +} diff --git a/lib/getCurrencyValue.ts b/lib/getCurrencyValue.ts new file mode 100644 index 0000000000..1009b1e80f --- /dev/null +++ b/lib/getCurrencyValue.ts @@ -0,0 +1,32 @@ +import BigNumber from 'bignumber.js'; + +import { ZERO } from 'lib/consts'; + +interface Params { + value: string; + exchangeRate?: string | null; + accuracy?: number; + accuracyUsd?: number; + decimals?: string | null; +} + +export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimals, exchangeRate }: Params) { + const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18'))); + const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat(); + + let usdResult: string | undefined; + let usdBn = ZERO; + + if (exchangeRate) { + const exchangeRateBn = new BigNumber(exchangeRate); + usdBn = valueCurr.times(exchangeRateBn); + if (accuracyUsd && !usdBn.isEqualTo(0)) { + const usdBnDp = usdBn.dp(accuracyUsd); + usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat(); + } else { + usdResult = usdBn.toFormat(); + } + } + + return { valueStr: valueResult, usd: usdResult, usdBn }; +} diff --git a/lib/getErrorMessage.ts b/lib/getErrorMessage.ts new file mode 100644 index 0000000000..b9fa1c9e77 --- /dev/null +++ b/lib/getErrorMessage.ts @@ -0,0 +1,3 @@ +export default function getErrorMessage(error: Record> | undefined, field: string) { + return error?.[field]?.join(', '); +} diff --git a/lib/getFilterValueFromQuery.ts b/lib/getFilterValueFromQuery.ts new file mode 100644 index 0000000000..77d3c99893 --- /dev/null +++ b/lib/getFilterValueFromQuery.ts @@ -0,0 +1,5 @@ +export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined): FilterType | undefined { + if (typeof val === 'string' && filterValues.includes(val as FilterType)) { + return val as FilterType; + } +} diff --git a/lib/getFilterValuesFromQuery.ts b/lib/getFilterValuesFromQuery.ts new file mode 100644 index 0000000000..ce5eddeb63 --- /dev/null +++ b/lib/getFilterValuesFromQuery.ts @@ -0,0 +1,15 @@ +export default function getFilterValue(filterValues: ReadonlyArray, val: string | Array | undefined) { + if (val === undefined) { + return; + } + + const valArray = []; + if (typeof val === 'string') { + valArray.push(...val.split(',')); + } + if (Array.isArray(val)) { + val.forEach(el => valArray.push(...el.split(','))); + } + + return valArray.filter(el => filterValues.includes(el as unknown as FilterType)) as unknown as Array; +} diff --git a/lib/getValueWithUnit.tsx b/lib/getValueWithUnit.tsx new file mode 100644 index 0000000000..eeaf8a0883 --- /dev/null +++ b/lib/getValueWithUnit.tsx @@ -0,0 +1,23 @@ +import BigNumber from 'bignumber.js'; + +import type { Unit } from 'types/unit'; + +import { WEI, GWEI } from 'lib/consts'; + +export default function getValueWithUnit(value: string | number, unit: Unit = 'wei') { + let unitBn: BigNumber.Value; + switch (unit) { + case 'wei': + unitBn = WEI; + break; + case 'gwei': + unitBn = GWEI; + break; + default: + unitBn = new BigNumber(1); + } + + const valueBn = new BigNumber(value); + const valueCurr = valueBn.dividedBy(unitBn); + return valueCurr; +} diff --git a/lib/growthbook/consts.ts b/lib/growthbook/consts.ts new file mode 100644 index 0000000000..b687eedbb3 --- /dev/null +++ b/lib/growthbook/consts.ts @@ -0,0 +1,2 @@ +export const STORAGE_KEY = 'growthbook:experiments'; +export const STORAGE_LIMIT = 20; diff --git a/lib/growthbook/init.ts b/lib/growthbook/init.ts new file mode 100644 index 0000000000..d98b2b94b7 --- /dev/null +++ b/lib/growthbook/init.ts @@ -0,0 +1,69 @@ +import { GrowthBook } from '@growthbook/growthbook-react'; + +import config from 'configs/app'; +import * as mixpanel from 'lib/mixpanel'; + +import { STORAGE_KEY, STORAGE_LIMIT } from './consts'; + +export interface GrowthBookFeatures { + test_value: string; +} + +export const growthBook = (() => { + const feature = config.features.growthBook; + + if (!feature.isEnabled) { + return; + } + + return new GrowthBook({ + apiHost: 'https://cdn.growthbook.io', + clientKey: feature.clientKey, + enableDevMode: config.app.isDev, + attributes: { + id: mixpanel.getUuid(), + chain_id: config.chain.id, + }, + trackingCallback: (experiment, result) => { + if (isExperimentStarted(experiment.key)) { + return; + } + + saveExperimentInStorage(experiment.key); + mixpanel.logEvent(mixpanel.EventTypes.EXPERIMENT_STARTED, { + 'Experiment name': experiment.key, + 'Variant name': result.value, + Source: 'growthbook', + }); + }, + }); +})(); + +function getStorageValue(): Array | undefined { + const item = window.localStorage.getItem(STORAGE_KEY); + if (!item) { + return; + } + + try { + const parsedValue = JSON.parse(item); + if (Array.isArray(parsedValue)) { + return parsedValue; + } + } catch { + return; + } +} + +function isExperimentStarted(key: string): boolean { + const items = getStorageValue() ?? []; + return items.some((item) => item === key); +} + +function saveExperimentInStorage(key: string) { + const items = getStorageValue() ?? []; + const newItems = [ key, ...items ].slice(0, STORAGE_LIMIT); + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newItems)); + } catch (error) {} +} diff --git a/lib/growthbook/useFeatureValue.ts b/lib/growthbook/useFeatureValue.ts new file mode 100644 index 0000000000..7dde7e8522 --- /dev/null +++ b/lib/growthbook/useFeatureValue.ts @@ -0,0 +1,14 @@ +import type { WidenPrimitives } from '@growthbook/growthbook'; +import { useFeatureValue, useGrowthBook } from '@growthbook/growthbook-react'; + +import type { GrowthBookFeatures } from './init'; + +export default function useGbFeatureValue( + name: Name, + fallback: GrowthBookFeatures[Name], +): { value: WidenPrimitives; isLoading: boolean } { + const value = useFeatureValue(name, fallback); + const growthBook = useGrowthBook(); + + return { value, isLoading: !(growthBook?.ready ?? true) }; +} diff --git a/lib/growthbook/useLoadFeatures.ts b/lib/growthbook/useLoadFeatures.ts new file mode 100644 index 0000000000..993e84505c --- /dev/null +++ b/lib/growthbook/useLoadFeatures.ts @@ -0,0 +1,17 @@ +import React from 'react'; + +import { SECOND } from 'lib/consts'; + +import { growthBook } from './init'; + +export default function useLoadFeatures() { + React.useEffect(() => { + growthBook?.setAttributes({ + ...growthBook.getAttributes(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + language: window.navigator.language, + }); + + growthBook?.loadFeatures({ timeout: SECOND }); + }, []); +} diff --git a/lib/hexToAddress.ts b/lib/hexToAddress.ts new file mode 100644 index 0000000000..4ff126edde --- /dev/null +++ b/lib/hexToAddress.ts @@ -0,0 +1,4 @@ +export default function hexToAddress(hex: string) { + const shortenHex = hex.slice(0, 66); + return shortenHex.slice(0, 2) + shortenHex.slice(26); +} diff --git a/lib/hexToBase64.ts b/lib/hexToBase64.ts new file mode 100644 index 0000000000..5b1366a6da --- /dev/null +++ b/lib/hexToBase64.ts @@ -0,0 +1,8 @@ +import bytesToBase64 from './bytesToBase64'; +import hexToBytes from './hexToBytes'; + +export default function hexToBase64(hex: string) { + const bytes = new Uint8Array(hexToBytes(hex)); + + return bytesToBase64(bytes); +} diff --git a/lib/hexToBytes.ts b/lib/hexToBytes.ts new file mode 100644 index 0000000000..e34435fbf4 --- /dev/null +++ b/lib/hexToBytes.ts @@ -0,0 +1,9 @@ +// hex can be with prefix - `0x{string}` - or without it - `{string}` +export default function hexToBytes(hex: string) { + const bytes = []; + const startIndex = hex.startsWith('0x') ? 2 : 0; + for (let c = startIndex; c < hex.length; c += 2) { + bytes.push(parseInt(hex.substring(c, c + 2), 16)); + } + return bytes; +} diff --git a/lib/hexToDecimal.ts b/lib/hexToDecimal.ts new file mode 100644 index 0000000000..43bf1be78b --- /dev/null +++ b/lib/hexToDecimal.ts @@ -0,0 +1,4 @@ +export default function hetToDecimal(hex: string) { + const strippedHex = hex.startsWith('0x') ? hex.slice(2) : hex; + return parseInt(strippedHex, 16); +} diff --git a/lib/hexToUtf8.ts b/lib/hexToUtf8.ts new file mode 100644 index 0000000000..8766ee25cc --- /dev/null +++ b/lib/hexToUtf8.ts @@ -0,0 +1,8 @@ +import hexToBytes from 'lib/hexToBytes'; + +export default function hexToUtf8(hex: string) { + const utf8decoder = new TextDecoder(); + const bytes = new Uint8Array(hexToBytes(hex)); + + return utf8decoder.decode(bytes); +} diff --git a/lib/highlightText.ts b/lib/highlightText.ts new file mode 100644 index 0000000000..85bfe91066 --- /dev/null +++ b/lib/highlightText.ts @@ -0,0 +1,8 @@ +import xss from 'xss'; + +import escapeRegExp from 'lib/escapeRegExp'; + +export default function highlightText(text: string, query: string) { + const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi'); + return xss(text.replace(regex, '$1')); +} diff --git a/lib/hooks/useAdblockDetect.tsx b/lib/hooks/useAdblockDetect.tsx new file mode 100644 index 0000000000..a3b42de1dd --- /dev/null +++ b/lib/hooks/useAdblockDetect.tsx @@ -0,0 +1,47 @@ +import { useEffect } from 'react'; + +import type { AdBannerProviders } from 'types/client/adProviders'; + +import config from 'configs/app'; +import { useAppContext } from 'lib/contexts/app'; +import * as cookies from 'lib/cookies'; +import isBrowser from 'lib/isBrowser'; + +const DEFAULT_URL = 'https://request-global.czilladx.com'; + +// in general, detect should work with any ad-provider url (that is alive) +// but we see some false-positive results in certain browsers +const TEST_URLS: Record = { + slise: 'https://v1.slise.xyz/serve', + coinzilla: 'https://request-global.czilladx.com', + adbutler: 'https://servedbyadbutler.com/app.js', + hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js', + // I don't have an url for getit to test + // getit: DEFAULT_URL, + none: DEFAULT_URL, +}; + +const feature = config.features.adsBanner; + +export default function useAdblockDetect() { + const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); + const provider = feature.isEnabled && feature.provider; + + useEffect(() => { + if (isBrowser() && !hasAdblockCookie && provider) { + const url = TEST_URLS[provider] || DEFAULT_URL; + fetch(url, { + method: 'HEAD', + mode: 'no-cors', + cache: 'no-store', + }) + .then(() => { + cookies.set(cookies.NAMES.ADBLOCK_DETECTED, 'false', { expires: 1 }); + }) + .catch(() => { + cookies.set(cookies.NAMES.ADBLOCK_DETECTED, 'true', { expires: 1 }); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} diff --git a/lib/hooks/useClientRect.tsx b/lib/hooks/useClientRect.tsx new file mode 100644 index 0000000000..05aa278f5b --- /dev/null +++ b/lib/hooks/useClientRect.tsx @@ -0,0 +1,37 @@ +import _debounce from 'lodash/debounce'; +import type { LegacyRef } from 'react'; +import React from 'react'; + +export default function useClientRect(): [ DOMRect | null, LegacyRef | undefined ] { + const [ rect, setRect ] = React.useState(null); + const nodeRef = React.useRef(); + + const ref = React.useCallback((node: E) => { + if (node !== null) { + setRect(node.getBoundingClientRect()); + } + nodeRef.current = node; + }, []); + + React.useEffect(() => { + const content = window.document.querySelector('main'); + if (!content) { + return; + } + + const resizeHandler = _debounce(() => { + setRect(nodeRef.current?.getBoundingClientRect() ?? null); + }, 100); + + const resizeObserver = new ResizeObserver(resizeHandler); + resizeObserver.observe(content); + resizeObserver.observe(window.document.body); + + return function cleanup() { + resizeObserver.unobserve(content); + resizeObserver.unobserve(window.document.body); + }; + }, [ ]); + + return [ rect, ref ]; +} diff --git a/lib/hooks/useContractTabs.tsx b/lib/hooks/useContractTabs.tsx new file mode 100644 index 0000000000..24667f69a1 --- /dev/null +++ b/lib/hooks/useContractTabs.tsx @@ -0,0 +1,143 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { Address } from 'types/api/address'; + +import useApiQuery from 'lib/api/useApiQuery'; +import * as cookies from 'lib/cookies'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import * as stubs from 'stubs/contract'; +import ContractCode from 'ui/address/contract/ContractCode'; +import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom'; +import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy'; +import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular'; +import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils'; + +const CONTRACT_TAB_IDS = [ + 'contract_code', + 'read_contract', + 'read_contract_rpc', + 'read_proxy', + 'read_custom_methods', + 'write_contract', + 'write_contract_rpc', + 'write_proxy', + 'write_custom_methods', +] as const; + +interface ContractTab { + id: typeof CONTRACT_TAB_IDS[number]; + title: string; + component: JSX.Element; +} + +interface ReturnType { + tabs: Array; + isLoading: boolean; +} + +export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean): ReturnType { + const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); + + const router = useRouter(); + const tab = getQueryParamString(router.query.tab); + + const isEnabled = Boolean(data?.hash) && data?.is_contract && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab); + + const enableQuery = React.useCallback(() => { + setIsQueryEnabled(true); + }, []); + + const contractQuery = useApiQuery('contract', { + pathParams: { hash: data?.hash }, + queryOptions: { + enabled: isEnabled && isQueryEnabled, + refetchOnMount: false, + placeholderData: data?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, + }, + }); + + const customAbiQuery = useApiQuery('custom_abi', { + queryOptions: { + enabled: isEnabled && isQueryEnabled && Boolean(cookies.get(cookies.NAMES.API_TOKEN)), + refetchOnMount: false, + }, + }); + + const channel = useSocketChannel({ + topic: `addresses:${ data?.hash?.toLowerCase() }`, + isDisabled: !isEnabled, + onJoin: enableQuery, + onSocketError: enableQuery, + }); + + const methods = React.useMemo(() => divideAbiIntoMethodTypes(contractQuery.data?.abi ?? []), [ contractQuery.data?.abi ]); + const methodsCustomAbi = React.useMemo(() => { + return divideAbiIntoMethodTypes( + customAbiQuery.data + ?.find((item) => data && item.contract_address_hash.toLowerCase() === data.hash.toLowerCase()) + ?.abi ?? + [], + ); + }, [ customAbiQuery.data, data ]); + + const verifiedImplementations = React.useMemo(() => { + return data?.implementations?.filter(({ name, address }) => name && address && address !== data?.hash) || []; + }, [ data?.hash, data?.implementations ]); + + return React.useMemo(() => { + return { + tabs: [ + { + id: 'contract_code' as const, + title: 'Code', + component: , + }, + methods.read.length > 0 && { + id: 'read_contract' as const, + title: 'Read contract', + component: , + }, + methodsCustomAbi.read.length > 0 && { + id: 'read_custom_methods' as const, + title: 'Read custom', + component: , + }, + verifiedImplementations.length > 0 && { + id: 'read_proxy' as const, + title: 'Read proxy', + component: ( + + ), + }, + methods.write.length > 0 && { + id: 'write_contract' as const, + title: 'Write contract', + component: , + }, + methodsCustomAbi.write.length > 0 && { + id: 'write_custom_methods' as const, + title: 'Write custom', + component: , + }, + verifiedImplementations.length > 0 && { + id: 'write_proxy' as const, + title: 'Write proxy', + component: ( + + ), + }, + ].filter(Boolean), + isLoading: contractQuery.isPlaceholderData, + }; + }, [ contractQuery, channel, data?.hash, verifiedImplementations, methods.read, methods.write, methodsCustomAbi.read, methodsCustomAbi.write ]); +} diff --git a/lib/hooks/useDebounce.tsx b/lib/hooks/useDebounce.tsx new file mode 100644 index 0000000000..5dfc71c141 --- /dev/null +++ b/lib/hooks/useDebounce.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export default function useDebounce(value: string, delay: number) { + const [ debouncedValue, setDebouncedValue ] = React.useState(value); + React.useEffect( + () => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, + [ value, delay ], + ); + return debouncedValue; +} diff --git a/lib/hooks/useFetch.tsx b/lib/hooks/useFetch.tsx new file mode 100644 index 0000000000..06eea3fcae --- /dev/null +++ b/lib/hooks/useFetch.tsx @@ -0,0 +1,79 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; + +import isBodyAllowed from 'lib/api/isBodyAllowed'; +import type { ResourceError, ResourcePath } from 'lib/api/resources'; + +export interface Params { + method?: RequestInit['method']; + headers?: RequestInit['headers']; + signal?: RequestInit['signal']; + body?: Record | FormData; + credentials?: RequestCredentials; +} + +interface Meta { + resource?: ResourcePath; + omitSentryErrorLog?: boolean; +} + +export default function useFetch() { + return React.useCallback((path: string, params?: Params, meta?: Meta): Promise> => { + const _body = params?.body; + const isFormData = _body instanceof FormData; + const withBody = isBodyAllowed(params?.method); + + const body: FormData | string | undefined = (() => { + if (!withBody) { + return; + } + + if (isFormData) { + return _body; + } + + return JSON.stringify(_body); + })(); + + const reqParams = { + ...params, + body, + headers: { + ...(withBody && !isFormData ? { 'Content-type': 'application/json' } : undefined), + ...params?.headers, + }, + }; + + return fetch(path, reqParams).then(response => { + if (!response.ok) { + const error = { + status: response.status, + statusText: response.statusText, + }; + + if (!meta?.omitSentryErrorLog) { + Sentry.captureException(new Error('Client fetch failed'), { tags: { + source: 'fetch', + 'source.resource': meta?.resource, + 'status.code': error.status, + 'status.text': error.statusText, + } }); + } + + return response.json().then( + (jsonError) => Promise.reject({ + payload: jsonError as Error, + status: response.status, + statusText: response.statusText, + }), + () => { + return Promise.reject(error); + }, + ); + + } else { + return response.json() as Promise; + } + }); + }, [ ]); +} diff --git a/lib/hooks/useFetchProfileInfo.tsx b/lib/hooks/useFetchProfileInfo.tsx new file mode 100644 index 0000000000..4df2f8d57b --- /dev/null +++ b/lib/hooks/useFetchProfileInfo.tsx @@ -0,0 +1,11 @@ +import useApiQuery from 'lib/api/useApiQuery'; +import * as cookies from 'lib/cookies'; + +export default function useFetchProfileInfo() { + return useApiQuery('user_info', { + queryOptions: { + refetchOnMount: false, + enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)), + }, + }); +} diff --git a/lib/hooks/useFirstMountState.tsx b/lib/hooks/useFirstMountState.tsx new file mode 100644 index 0000000000..b8850fd519 --- /dev/null +++ b/lib/hooks/useFirstMountState.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +// Returns true if component is just mounted (on first render) and false otherwise. +export function useFirstMountState(): boolean { + const isFirst = React.useRef(true); + + if (isFirst.current) { + isFirst.current = false; + + return true; + } + + return isFirst.current; +} diff --git a/lib/hooks/useGetCsrfToken.tsx b/lib/hooks/useGetCsrfToken.tsx new file mode 100644 index 0000000000..297f1567c2 --- /dev/null +++ b/lib/hooks/useGetCsrfToken.tsx @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/react'; +import { useQuery } from '@tanstack/react-query'; + +import buildUrl from 'lib/api/buildUrl'; +import isNeedProxy from 'lib/api/isNeedProxy'; +import { getResourceKey } from 'lib/api/useApiQuery'; +import * as cookies from 'lib/cookies'; +import useFetch from 'lib/hooks/useFetch'; + +export default function useGetCsrfToken() { + const nodeApiFetch = useFetch(); + + useQuery({ + queryKey: getResourceKey('csrf'), + queryFn: async() => { + if (!isNeedProxy()) { + const url = buildUrl('csrf'); + const apiResponse = await fetch(url, { credentials: 'include' }); + const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf'); + + if (!csrfFromHeader) { + Sentry.captureException(new Error('Client fetch failed'), { tags: { + source: 'fetch', + 'source.resource': 'csrf', + 'status.code': 500, + 'status.text': 'Unable to obtain csrf token from header', + } }); + return; + } + + return { token: csrfFromHeader }; + } + + return nodeApiFetch('/node-api/csrf'); + }, + enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)), + }); +} diff --git a/lib/hooks/useGradualIncrement.tsx b/lib/hooks/useGradualIncrement.tsx new file mode 100644 index 0000000000..e1bab30e27 --- /dev/null +++ b/lib/hooks/useGradualIncrement.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +const DURATION = 300; + +export default function useGradualIncrement(initialValue: number): [number, (inc: number) => void] { + const [ num, setNum ] = React.useState(initialValue); + const queue = React.useRef(0); + const timeoutId = React.useRef(0); + const delay = React.useRef(0); + + const incrementDelayed = React.useCallback(() => { + if (queue.current === 0) { + return; + } + + queue.current--; + setNum(prev => prev + 1); + timeoutId.current = 0; + }, []); + + const increment = React.useCallback((inc: number) => { + if (inc < 1) { + return; + } + + queue.current += inc; + + if (!timeoutId.current) { + timeoutId.current = window.setTimeout(incrementDelayed, 0); + } + }, [ incrementDelayed ]); + + React.useEffect(() => { + if (queue.current > 0 && !timeoutId.current) { + if (!delay.current) { + delay.current = DURATION / queue.current; + } else if (delay.current > DURATION / queue.current) { + // in case if queue size is increased since last DOM update + delay.current = DURATION / queue.current; + } + timeoutId.current = window.setTimeout(incrementDelayed, delay.current); + } + }, [ incrementDelayed, num ]); + + React.useEffect(() => { + return () => { + window.clearTimeout(timeoutId.current); + }; + }, []); + + return [ num, increment ]; +} diff --git a/lib/hooks/useHasAccount.ts b/lib/hooks/useHasAccount.ts new file mode 100644 index 0000000000..d314f6f36a --- /dev/null +++ b/lib/hooks/useHasAccount.ts @@ -0,0 +1,15 @@ +import config from 'configs/app'; +import { useAppContext } from 'lib/contexts/app'; +import * as cookies from 'lib/cookies'; + +export default function useHasAccount() { + const appProps = useAppContext(); + + if (!config.features.account.isEnabled) { + return false; + } + + const cookiesString = appProps.cookies; + const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString)); + return hasAuth; +} diff --git a/lib/hooks/useIsAccountActionAllowed.tsx b/lib/hooks/useIsAccountActionAllowed.tsx new file mode 100644 index 0000000000..c8df0912a3 --- /dev/null +++ b/lib/hooks/useIsAccountActionAllowed.tsx @@ -0,0 +1,28 @@ +import { useQueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import type { UserInfo } from 'types/api/account'; + +import { resourceKey } from 'lib/api/resources'; +import useLoginUrl from 'lib/hooks/useLoginUrl'; + +export default function useIsAccountActionAllowed() { + const queryClient = useQueryClient(); + + const profileData = queryClient.getQueryData([ resourceKey('user_info') ]); + const isAuth = Boolean(profileData); + const loginUrl = useLoginUrl(); + + return React.useCallback(() => { + if (!loginUrl) { + return false; + } + + if (!isAuth) { + window.location.assign(loginUrl); + return false; + } + + return true; + }, [ isAuth, loginUrl ]); +} diff --git a/lib/hooks/useIsInitialLoading.tsx b/lib/hooks/useIsInitialLoading.tsx new file mode 100644 index 0000000000..de07b15fd6 --- /dev/null +++ b/lib/hooks/useIsInitialLoading.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export default function useIsInitialLoading(isLoading: boolean | undefined) { + const [ isInitialLoading, setIsInitialLoading ] = React.useState(Boolean(isLoading)); + + React.useEffect(() => { + if (!isLoading) { + setIsInitialLoading(false); + } + }, [ isLoading ]); + + return isInitialLoading; +} diff --git a/lib/hooks/useIsMobile.tsx b/lib/hooks/useIsMobile.tsx new file mode 100644 index 0000000000..f8a8dcd7ee --- /dev/null +++ b/lib/hooks/useIsMobile.tsx @@ -0,0 +1,5 @@ +import { useBreakpointValue } from '@chakra-ui/react'; + +export default function useIsMobile(ssr = true) { + return useBreakpointValue({ base: true, lg: false }, { ssr }); +} diff --git a/lib/hooks/useIsMounted.tsx b/lib/hooks/useIsMounted.tsx new file mode 100644 index 0000000000..d14880ae1b --- /dev/null +++ b/lib/hooks/useIsMounted.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function useIsMounted() { + const [ isMounted, setIsMounted ] = React.useState(false); + + React.useEffect(() => { + setIsMounted(true); + }, [ ]); + + return isMounted; +} diff --git a/lib/hooks/useIsSafeAddress.tsx b/lib/hooks/useIsSafeAddress.tsx new file mode 100644 index 0000000000..49062f7cc2 --- /dev/null +++ b/lib/hooks/useIsSafeAddress.tsx @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; + +import config from 'configs/app'; +import useFetch from 'lib/hooks/useFetch'; + +const feature = config.features.safe; + +export default function useIsSafeAddress(hash: string | undefined): boolean { + const fetch = useFetch(); + + const { data } = useQuery({ + queryKey: [ 'safe_transaction_api', hash ], + queryFn: async() => { + if (!feature.isEnabled || !hash) { + return Promise.reject(); + } + + return fetch(`${ feature.apiUrl }/${ hash }`, undefined, { omitSentryErrorLog: true }); + }, + enabled: feature.isEnabled && Boolean(hash), + refetchOnMount: false, + }); + + return Boolean(data); +} diff --git a/lib/hooks/useIsSticky.tsx b/lib/hooks/useIsSticky.tsx new file mode 100644 index 0000000000..cc9c5c4b6d --- /dev/null +++ b/lib/hooks/useIsSticky.tsx @@ -0,0 +1,34 @@ +import throttle from 'lodash/throttle'; +import React from 'react'; + +export default function useIsSticky(ref: React.RefObject, offset = 0, isEnabled = true) { + const [ isSticky, setIsSticky ] = React.useState(false); + + const handleScroll = React.useCallback(() => { + if ( + Number(ref.current?.getBoundingClientRect().y) < offset + ) { + setIsSticky(true); + } else { + setIsSticky(false); + } + }, [ ref, offset ]); + + React.useEffect(() => { + if (!isEnabled) { + return; + } + + const throttledHandleScroll = throttle(handleScroll, 300); + + window.addEventListener('scroll', throttledHandleScroll); + + return () => { + window.removeEventListener('scroll', throttledHandleScroll); + }; + // replicate componentDidMount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isEnabled ]); + + return isSticky; +} diff --git a/lib/hooks/useIssueUrl.tsx b/lib/hooks/useIssueUrl.tsx new file mode 100644 index 0000000000..0ac696746e --- /dev/null +++ b/lib/hooks/useIssueUrl.tsx @@ -0,0 +1,32 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; + +export default function useIssueUrl(backendVersion: string | undefined) { + const [ isLoading, setIsLoading ] = React.useState(true); + const router = useRouter(); + + React.useEffect(() => { + setIsLoading(false); + }, [ ]); + + return React.useMemo(() => { + if (isLoading) { + return ''; + } + + const searchParams = new URLSearchParams({ + template: 'bug_report.yml', + labels: 'triage', + link: window.location.href, + 'backend-version': backendVersion || '', + 'frontend-version': [ config.UI.footer.frontendVersion, config.UI.footer.frontendCommit ].filter(Boolean).join('+'), + 'additional-information': `**User Agent:** ${ window.navigator.userAgent }`, + }); + return `https://github.com/blockscout/blockscout/issues/new/?${ searchParams.toString() }`; + // we need to update link whenever page url changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ backendVersion, isLoading, router.asPath ]); + +} diff --git a/lib/hooks/useLazyRenderedList.tsx b/lib/hooks/useLazyRenderedList.tsx new file mode 100644 index 0000000000..3f2d828fe0 --- /dev/null +++ b/lib/hooks/useLazyRenderedList.tsx @@ -0,0 +1,23 @@ +import _clamp from 'lodash/clamp'; +import React from 'react'; +import { useInView } from 'react-intersection-observer'; + +const STEP = 10; +const MIN_ITEMS_NUM = 50; + +export default function useLazyRenderedList(list: Array, isEnabled: boolean, minItemsNum: number = MIN_ITEMS_NUM) { + const [ renderedItemsNum, setRenderedItemsNum ] = React.useState(minItemsNum); + const { ref, inView } = useInView({ + rootMargin: '200px', + triggerOnce: false, + skip: !isEnabled || list.length <= minItemsNum, + }); + + React.useEffect(() => { + if (inView) { + setRenderedItemsNum((prev) => _clamp(prev + STEP, 0, list.length)); + } + }, [ inView, list.length ]); + + return { cutRef: ref, renderedItemsNum }; +} diff --git a/lib/hooks/useLoginUrl.tsx b/lib/hooks/useLoginUrl.tsx new file mode 100644 index 0000000000..a111d1eebb --- /dev/null +++ b/lib/hooks/useLoginUrl.tsx @@ -0,0 +1,14 @@ +import { useRouter } from 'next/router'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const feature = config.features.account; + +export default function useLoginUrl() { + const router = useRouter(); + return feature.isEnabled ? + feature.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } }) : + undefined; +} diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx new file mode 100644 index 0000000000..b1a0bf5245 --- /dev/null +++ b/lib/hooks/useNavItems.tsx @@ -0,0 +1,309 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/navigation'; + +import config from 'configs/app'; +import { rightLineArrow } from 'lib/html-entities'; +import UserAvatar from 'ui/shared/UserAvatar'; + +interface ReturnType { + mainNavItems: Array; + accountNavItems: Array; + profileItem: NavItem; +} + +export function isGroupItem(item: NavItem | NavGroupItem): item is NavGroupItem { + return 'subItems' in item; +} + +export function isInternalItem(item: NavItem): item is NavItemInternal { + return 'nextRoute' in item; +} + +export default function useNavItems(): ReturnType { + const router = useRouter(); + const pathname = router.pathname; + + return React.useMemo(() => { + let blockchainNavItems: Array | Array> = []; + + const topAccounts: NavItem | null = !config.UI.views.address.hiddenViews?.top_accounts ? { + text: 'Top accounts', + nextRoute: { pathname: '/accounts' as const }, + icon: 'top-accounts', + isActive: pathname === '/accounts', + } : null; + const blocks: NavItem | null = { + text: 'Blocks', + nextRoute: { pathname: '/blocks' as const }, + icon: 'block', + isActive: pathname === '/blocks' || pathname === '/block/[height_or_hash]', + }; + const txs: NavItem | null = { + text: 'Transactions', + nextRoute: { pathname: '/txs' as const }, + icon: 'transactions', + isActive: pathname === '/txs' || pathname === '/tx/[hash]', + }; + const userOps: NavItem | null = config.features.userOps.isEnabled ? { + text: 'User operations', + nextRoute: { pathname: '/ops' as const }, + icon: 'user_op', + isActive: pathname === '/ops' || pathname === '/op/[hash]', + } : null; + + const verifiedContracts: NavItem | null = + { + text: 'Verified contracts', + nextRoute: { pathname: '/verified-contracts' as const }, + icon: 'verified', + isActive: pathname === '/verified-contracts', + }; + const ensLookup = config.features.nameService.isEnabled ? { + text: 'Name services lookup', + nextRoute: { pathname: '/name-domains' as const }, + icon: 'ENS', + isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]', + } : null; + const validators = config.features.validators.isEnabled ? { + text: 'Top validators', + nextRoute: { pathname: '/validators' as const }, + icon: 'validator', + isActive: pathname === '/validators', + } : null; + const rollupDeposits = { + text: `Deposits (L1${ rightLineArrow }L2)`, + nextRoute: { pathname: '/deposits' as const }, + icon: 'arrows/south-east', + isActive: pathname === '/deposits', + }; + const rollupWithdrawals = { + text: `Withdrawals (L2${ rightLineArrow }L1)`, + nextRoute: { pathname: '/withdrawals' as const }, + icon: 'arrows/north-east', + isActive: pathname === '/withdrawals', + }; + const rollupTxnBatches = { + text: 'Txn batches', + nextRoute: { pathname: '/batches' as const }, + icon: 'txn_batches', + isActive: pathname === '/batches', + }; + const rollupOutputRoots = { + text: 'Output roots', + nextRoute: { pathname: '/output-roots' as const }, + icon: 'output_roots', + isActive: pathname === '/output-roots', + }; + const rollupDisputeGames = config.features.faultProofSystem.isEnabled ? { + text: 'Dispute games', + nextRoute: { pathname: '/dispute-games' as const }, + icon: 'games', + isActive: pathname === '/dispute-games', + } : null; + const mudWorlds = config.features.mudFramework.isEnabled ? { + text: 'MUD worlds', + nextRoute: { pathname: '/mud-worlds' as const }, + icon: 'MUD_menu', + isActive: pathname === '/mud-worlds', + } : null; + + const rollupFeature = config.features.rollup; + + if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'arbitrum' || rollupFeature.type === 'zkEvm')) { + blockchainNavItems = [ + [ + txs, + rollupDeposits, + rollupWithdrawals, + ], + [ + blocks, + rollupTxnBatches, + rollupDisputeGames, + rollupFeature.type === 'optimistic' ? rollupOutputRoots : undefined, + ].filter(Boolean), + [ + userOps, + topAccounts, + mudWorlds, + validators, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else if (rollupFeature.isEnabled && rollupFeature.type === 'shibarium') { + blockchainNavItems = [ + [ + txs, + rollupDeposits, + rollupWithdrawals, + ], + [ + blocks, + userOps, + topAccounts, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else if (rollupFeature.isEnabled && rollupFeature.type === 'zkSync') { + blockchainNavItems = [ + [ + txs, + userOps, + blocks, + rollupTxnBatches, + ].filter(Boolean), + [ + topAccounts, + validators, + verifiedContracts, + ensLookup, + ].filter(Boolean), + ]; + } else { + blockchainNavItems = [ + txs, + userOps, + blocks, + topAccounts, + validators, + verifiedContracts, + ensLookup, + config.features.beaconChain.isEnabled && { + text: 'Withdrawals', + nextRoute: { pathname: '/withdrawals' as const }, + icon: 'arrows/north-east', + isActive: pathname === '/withdrawals', + }, + ].filter(Boolean); + } + + const apiNavItems: Array = [ + config.features.restApiDocs.isEnabled ? { + text: 'REST API', + nextRoute: { pathname: '/api-docs' as const }, + icon: 'restAPI', + isActive: pathname === '/api-docs', + } : null, + config.features.graphqlApiDocs.isEnabled ? { + text: 'GraphQL', + nextRoute: { pathname: '/graphiql' as const }, + icon: 'graphQL', + isActive: pathname === '/graphiql', + } : null, + !config.UI.navigation.hiddenLinks?.rpc_api && { + text: 'RPC API', + icon: 'RPC', + url: 'https://docs.blockscout.com/for-users/api/rpc-endpoints', + }, + !config.UI.navigation.hiddenLinks?.eth_rpc_api && { + text: 'Eth RPC API', + icon: 'RPC', + url: ' https://docs.blockscout.com/for-users/api/eth-rpc', + }, + ].filter(Boolean); + + const otherNavItems: Array | Array> = [ + { + text: 'Verify contract', + nextRoute: { pathname: '/contract-verification' as const }, + isActive: pathname.startsWith('/contract-verification'), + }, + config.features.gasTracker.isEnabled && { + text: 'Gas tracker', + nextRoute: { pathname: '/gas-tracker' as const }, + isActive: pathname.startsWith('/gas-tracker'), + }, + config.features.publicTagsSubmission.isEnabled && { + text: 'Submit public tag', + nextRoute: { pathname: '/public-tags/submit' as const }, + isActive: pathname.startsWith('/public-tags/submit'), + }, + ...config.UI.navigation.otherLinks, + ].filter(Boolean); + + const mainNavItems: ReturnType['mainNavItems'] = [ + { + text: 'Blockchain', + icon: 'globe-b', + isActive: blockchainNavItems.flat().some(item => isInternalItem(item) && item.isActive), + subItems: blockchainNavItems, + }, + { + text: 'Tokens', + nextRoute: { pathname: '/tokens' as const }, + icon: 'token', + isActive: pathname.startsWith('/token'), + }, + config.features.marketplace.isEnabled ? { + text: 'DApps', + nextRoute: { pathname: '/apps' as const }, + icon: 'apps', + isActive: pathname.startsWith('/app'), + } : null, + config.features.stats.isEnabled ? { + text: 'Charts & stats', + nextRoute: { pathname: '/stats' as const }, + icon: 'stats', + isActive: pathname === '/stats', + } : null, + apiNavItems.length > 0 && { + text: 'API', + icon: 'restAPI', + isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive), + subItems: apiNavItems, + }, + { + text: 'Other', + icon: 'gear', + isActive: otherNavItems.flat().some(item => isInternalItem(item) && item.isActive), + subItems: otherNavItems, + }, + ].filter(Boolean); + + const accountNavItems: ReturnType['accountNavItems'] = [ + { + text: 'Watch list', + nextRoute: { pathname: '/account/watchlist' as const }, + icon: 'watchlist', + isActive: pathname === '/account/watchlist', + }, + { + text: 'Private tags', + nextRoute: { pathname: '/account/tag-address' as const }, + icon: 'privattags', + isActive: pathname === '/account/tag-address', + }, + { + text: 'API keys', + nextRoute: { pathname: '/account/api-key' as const }, + icon: 'API', + isActive: pathname === '/account/api-key', + }, + { + text: 'Custom ABI', + nextRoute: { pathname: '/account/custom-abi' as const }, + icon: 'ABI', + isActive: pathname === '/account/custom-abi', + }, + config.features.addressVerification.isEnabled && { + text: 'Verified addrs', + nextRoute: { pathname: '/account/verified-addresses' as const }, + icon: 'verified', + isActive: pathname === '/account/verified-addresses', + }, + ].filter(Boolean); + + const profileItem = { + text: 'My profile', + nextRoute: { pathname: '/auth/profile' as const }, + iconComponent: UserAvatar, + isActive: pathname === '/auth/profile', + }; + + return { mainNavItems, accountNavItems, profileItem }; + }, [ pathname ]); +} diff --git a/lib/hooks/useNewTxsSocket.tsx b/lib/hooks/useNewTxsSocket.tsx new file mode 100644 index 0000000000..bedae0049d --- /dev/null +++ b/lib/hooks/useNewTxsSocket.tsx @@ -0,0 +1,85 @@ +import type { NextRouter } from 'next/router'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import useGradualIncrement from 'lib/hooks/useGradualIncrement'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; + +function getSocketParams(router: NextRouter) { + + if ( + router.pathname === '/txs' && + (router.query.tab === 'validated' || router.query.tab === undefined) && + !router.query.block_number && + !router.query.page + ) { + return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const }; + } + + if (router.pathname === '/') { + return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const }; + } + + if ( + router.pathname === '/txs' && + router.query.tab === 'pending' && + !router.query.block_number && + !router.query.page + ) { + return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const }; + } + + return {}; +} + +function assertIsNewTxResponse(response: unknown): response is { transaction: number } { + return typeof response === 'object' && response !== null && 'transaction' in response; +} +function assertIsNewPendingTxResponse(response: unknown): response is { pending_transaction: number } { + return typeof response === 'object' && response !== null && 'pending_transaction' in response; +} + +export default function useNewTxsSocket() { + const router = useRouter(); + const [ num, setNum ] = useGradualIncrement(0); + const [ socketAlert, setSocketAlert ] = React.useState(''); + + const { topic, event } = getSocketParams(router); + + const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => { + if (assertIsNewTxResponse(response)) { + setNum(response.transaction); + } + if (assertIsNewPendingTxResponse(response)) { + setNum(response.pending_transaction); + } + }, [ setNum ]); + + const handleSocketClose = React.useCallback(() => { + setSocketAlert('Connection is lost. Please reload page.'); + }, []); + + const handleSocketError = React.useCallback(() => { + setSocketAlert('An error has occurred while fetching new transactions. Please reload page.'); + }, []); + + const channel = useSocketChannel({ + topic, + onSocketClose: handleSocketClose, + onSocketError: handleSocketError, + isDisabled: !topic, + }); + + useSocketMessage({ + channel, + event, + handler: handleNewTxMessage, + }); + + if (!topic && !event) { + return {}; + } + + return { num, socketAlert }; +} diff --git a/lib/hooks/useNotifyOnNavigation.tsx b/lib/hooks/useNotifyOnNavigation.tsx new file mode 100644 index 0000000000..a3b7a7c92f --- /dev/null +++ b/lib/hooks/useNotifyOnNavigation.tsx @@ -0,0 +1,24 @@ +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +export default function useNotifyOnNavigation() { + const router = useRouter(); + const pathname = usePathname(); + const tab = getQueryParamString(router.query.tab); + + React.useEffect(() => { + if (config.features.metasuites.isEnabled) { + window.postMessage({ source: 'APP_ROUTER', type: 'PATHNAME_CHANGED' }, window.location.origin); + } + }, [ pathname ]); + + React.useEffect(() => { + if (config.features.metasuites.isEnabled) { + window.postMessage({ source: 'APP_ROUTER', type: 'TAB_CHANGED' }, window.location.origin); + } + }, [ tab ]); +} diff --git a/lib/hooks/usePreventFocusAfterModalClosing.tsx b/lib/hooks/usePreventFocusAfterModalClosing.tsx new file mode 100644 index 0000000000..ec9e22fd14 --- /dev/null +++ b/lib/hooks/usePreventFocusAfterModalClosing.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +// prevent set focus on button when closing modal +export default function usePreventFocusAfterModalClosing() { + return React.useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []); +} diff --git a/lib/hooks/useRedirectForInvalidAuthToken.tsx b/lib/hooks/useRedirectForInvalidAuthToken.tsx new file mode 100644 index 0000000000..24b74ace64 --- /dev/null +++ b/lib/hooks/useRedirectForInvalidAuthToken.tsx @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/react'; +import { useQueryClient } from '@tanstack/react-query'; +import React from 'react'; + +import { resourceKey } from 'lib/api/resources'; +import type { ResourceError } from 'lib/api/resources'; +import * as cookies from 'lib/cookies'; +import useLoginUrl from 'lib/hooks/useLoginUrl'; + +export default function useRedirectForInvalidAuthToken() { + const queryClient = useQueryClient(); + + const state = queryClient.getQueryState([ resourceKey('user_info') ]); + const errorStatus = state?.error?.status; + const loginUrl = useLoginUrl(); + + React.useEffect(() => { + if (errorStatus === 401) { + const apiToken = cookies.get(cookies.NAMES.API_TOKEN); + + if (apiToken && loginUrl) { + Sentry.captureException(new Error('Invalid API token'), { tags: { source: 'invalid_api_token' } }); + window.location.assign(loginUrl); + } + } + }, [ errorStatus, loginUrl ]); +} diff --git a/lib/hooks/useTimeAgoIncrement.tsx b/lib/hooks/useTimeAgoIncrement.tsx new file mode 100644 index 0000000000..9d0300056d --- /dev/null +++ b/lib/hooks/useTimeAgoIncrement.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import { DAY, HOUR, MINUTE, SECOND } from 'lib/consts'; +import dayjs from 'lib/date/dayjs'; + +function getUnits(diff: number) { + if (diff < MINUTE) { + return [ SECOND, MINUTE ]; + } + + if (diff < HOUR) { + return [ MINUTE, HOUR ]; + } + + if (diff < DAY) { + return [ HOUR, DAY ]; + } + + return [ DAY, 2 * DAY ]; +} + +function getUpdateParams(ts: string) { + const timeDiff = Date.now() - new Date(ts).getTime(); + const [ unit, higherUnit ] = getUnits(timeDiff); + + if (unit === DAY) { + return { interval: DAY }; + } + + const leftover = unit - timeDiff % unit; + + return { + startTimeout: unit === SECOND ? + 0 : + // here we assume that in current dayjs locale time difference is rounded by Math.round function + // so we have to update displayed value whenever time comes over the middle of the unit interval + // since it will be rounded to the upper bound + (leftover < unit / 2 ? leftover + unit / 2 : leftover - unit / 2) + SECOND, + endTimeout: higherUnit - timeDiff + SECOND, + interval: unit, + }; +} + +export default function useTimeAgoIncrement(ts: string | null, isEnabled?: boolean) { + const [ value, setValue ] = React.useState(ts ? dayjs(ts).fromNow() : null); + + React.useEffect(() => { + if (ts !== null) { + const timeouts: Array = []; + const intervals: Array = []; + + const startIncrement = () => { + const { startTimeout, interval, endTimeout } = getUpdateParams(ts); + if (!startTimeout && !endTimeout) { + return; + } + + let intervalId: number; + + const startTimeoutId = window.setTimeout(() => { + setValue(dayjs(ts).fromNow()); + + intervalId = window.setInterval(() => { + setValue(dayjs(ts).fromNow()); + }, interval); + + intervals.push(intervalId); + }, startTimeout); + + const endTimeoutId = window.setTimeout(() => { + window.clearInterval(intervalId); + startIncrement(); + }, endTimeout); + + timeouts.push(startTimeoutId); + timeouts.push(endTimeoutId); + }; + + isEnabled && startIncrement(); + + !isEnabled && setValue(dayjs(ts).fromNow()); + + return () => { + timeouts.forEach(window.clearTimeout); + intervals.forEach(window.clearInterval); + }; + } + }, [ isEnabled, ts ]); + + return value; +} diff --git a/lib/hooks/useToast.tsx b/lib/hooks/useToast.tsx new file mode 100644 index 0000000000..7afc6f3e11 --- /dev/null +++ b/lib/hooks/useToast.tsx @@ -0,0 +1,28 @@ +import type { UseToastOptions, ToastProps } from '@chakra-ui/react'; +import { createToastFn, useChakra } from '@chakra-ui/react'; +import React from 'react'; + +import Toast from 'ui/shared/chakra/Toast'; + +// there is no toastComponent prop in UseToastOptions type +// but these options will be passed to createRenderToast under the hood +// and it accepts custom toastComponent +const defaultOptions: UseToastOptions & { toastComponent?: React.FC } = { + toastComponent: Toast, + position: 'top-right', + isClosable: true, + containerStyle: { + margin: 3, + marginBottom: 0, + }, + variant: 'subtle', +}; + +export default function useToastModified() { + const { theme } = useChakra(); + + return React.useMemo( + () => createToastFn(theme.direction, defaultOptions), + [ theme.direction ], + ); +} diff --git a/lib/hooks/useUpdateEffect.tsx b/lib/hooks/useUpdateEffect.tsx new file mode 100644 index 0000000000..560d630751 --- /dev/null +++ b/lib/hooks/useUpdateEffect.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { useFirstMountState } from './useFirstMountState'; + +// React effect hook that ignores the first invocation (e.g. on mount). The signature is exactly the same as the useEffect hook. +const useUpdateEffect: typeof React.useEffect = (effect, deps) => { + const isFirstMount = useFirstMountState(); + + React.useEffect(() => { + if (!isFirstMount) { + return effect(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); +}; + +export default useUpdateEffect; diff --git a/lib/hooks/useUpdateValueEffect.tsx b/lib/hooks/useUpdateValueEffect.tsx new file mode 100644 index 0000000000..a7d7da2aa0 --- /dev/null +++ b/lib/hooks/useUpdateValueEffect.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +// run effect only if value is updated since initial mount +const useUpdateValueEffect = (effect: () => void, value: string) => { + const mountedRef = React.useRef(false); + const valueRef = React.useRef(); + const isChangedRef = React.useRef(false); + + React.useEffect(() => { + mountedRef.current = true; + valueRef.current = value; + + return () => { + mountedRef.current = false; + valueRef.current = undefined; + isChangedRef.current = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + if (mountedRef.current && (value !== valueRef.current || isChangedRef.current)) { + isChangedRef.current = true; + return effect(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ value ]); +}; + +export default useUpdateValueEffect; diff --git a/lib/html-entities.ts b/lib/html-entities.ts new file mode 100644 index 0000000000..1163ee9338 --- /dev/null +++ b/lib/html-entities.ts @@ -0,0 +1,26 @@ +// https://symbl.cc/en/ +export const asymp = String.fromCharCode(8776); // ≈ +export const tilde = String.fromCharCode(126); // ~ +export const hellip = String.fromCharCode(8230); // … +export const nbsp = String.fromCharCode(160); // no-break Space +export const thinsp = String.fromCharCode(8201); // thin Space +export const space = String.fromCharCode(32); // space +export const nbdash = String.fromCharCode(8209); // non-breaking hyphen +export const mdash = String.fromCharCode(8212); // em dash +export const ndash = String.fromCharCode(8211); // en dash +export const laquo = String.fromCharCode(171); // « +export const raquo = String.fromCharCode(187); // » +export const middot = String.fromCharCode(183); // · +export const blackCircle = String.fromCharCode(9679); // ● +export const blackRightwardsArrowhead = String.fromCharCode(10148); // ➤ +export const degree = String.fromCharCode(176); // ° +export const times = String.fromCharCode(215); // × +export const disk = String.fromCharCode(8226); // • +export const minus = String.fromCharCode(8722); // − +export const leftLineArrow = String.fromCharCode(8592); // ← +export const rightLineArrow = String.fromCharCode(8594); // → +export const apos = String.fromCharCode(39); // apostrophe ' +export const shift = String.fromCharCode(8679); // upwards white arrow ⇧ +export const cmd = String.fromCharCode(8984); // place of interest sign ⌘ +export const alt = String.fromCharCode(9095); // alternate key symbol ⎇ +export const copy = String.fromCharCode(169); // copyright symbol © diff --git a/lib/isBrowser.ts b/lib/isBrowser.ts new file mode 100644 index 0000000000..e6320b7e17 --- /dev/null +++ b/lib/isBrowser.ts @@ -0,0 +1,3 @@ +export default function isBrowser() { + return typeof window !== 'undefined'; +} diff --git a/lib/isMetaKey.tsx b/lib/isMetaKey.tsx new file mode 100644 index 0000000000..878462bf8d --- /dev/null +++ b/lib/isMetaKey.tsx @@ -0,0 +1,3 @@ +export default function isMetaKey(event: React.KeyboardEvent) { + return event.metaKey || event.getModifierState('Meta') || event.getModifierState('OS'); +} diff --git a/lib/makePrettyLink.ts b/lib/makePrettyLink.ts new file mode 100644 index 0000000000..9e05a2d660 --- /dev/null +++ b/lib/makePrettyLink.ts @@ -0,0 +1,9 @@ +export default function makePrettyLink(url: string | undefined): { url: string; domain: string } | undefined { + try { + const urlObj = new URL(url ?? ''); + return { + url: urlObj.href, + domain: urlObj.hostname, + }; + } catch (error) {} +} diff --git a/lib/metadata/__snapshots__/generate.test.ts.snap b/lib/metadata/__snapshots__/generate.test.ts.snap new file mode 100644 index 0000000000..a664df8d20 --- /dev/null +++ b/lib/metadata/__snapshots__/generate.test.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generates correct metadata for: dynamic route 1`] = ` +{ + "canonical": undefined, + "description": "View transaction 0x12345 on Blockscout (Blockscout) Explorer", + "opengraph": { + "description": "", + "imageUrl": "", + "title": "Blockscout transaction 0x12345 | Blockscout", + }, + "title": "Blockscout transaction 0x12345 | Blockscout", +} +`; + +exports[`generates correct metadata for: dynamic route with API data 1`] = ` +{ + "canonical": undefined, + "description": "0x12345, balances and analytics on the Blockscout (Blockscout) Explorer", + "opengraph": { + "description": "", + "imageUrl": "", + "title": "Blockscout USDT token details | Blockscout", + }, + "title": "Blockscout USDT token details | Blockscout", +} +`; + +exports[`generates correct metadata for: static route 1`] = ` +{ + "canonical": "http://localhost:3000/txs", + "description": "Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.", + "opengraph": { + "description": "", + "imageUrl": "http://localhost:3000/static/og_placeholder.png", + "title": "Blockscout transactions - Blockscout explorer | Blockscout", + }, + "title": "Blockscout transactions - Blockscout explorer | Blockscout", +} +`; diff --git a/lib/metadata/compileValue.ts b/lib/metadata/compileValue.ts new file mode 100644 index 0000000000..63442394ae --- /dev/null +++ b/lib/metadata/compileValue.ts @@ -0,0 +1,16 @@ +export default function compileValue(template: string, params: Record | undefined>) { + const PLACEHOLDER_REGEX = /%(\w+)%/g; + return template.replaceAll(PLACEHOLDER_REGEX, (match, p1) => { + const value = params[p1]; + + if (Array.isArray(value)) { + return value.join(', '); + } + + if (value === undefined) { + return ''; + } + + return value; + }); +} diff --git a/lib/metadata/generate.test.ts b/lib/metadata/generate.test.ts new file mode 100644 index 0000000000..f2cd05dcc1 --- /dev/null +++ b/lib/metadata/generate.test.ts @@ -0,0 +1,47 @@ +import type { ApiData } from './types'; + +import type { Route } from 'nextjs-routes'; + +import generate from './generate'; + +interface TestCase { + title: string; + route: { + pathname: Pathname; + query?: Route['query']; + }; + apiData?: ApiData; +} + +const TEST_CASES = [ + { + title: 'static route', + route: { + pathname: '/txs', + }, + } as TestCase<'/txs'>, + { + title: 'dynamic route', + route: { + pathname: '/tx/[hash]', + query: { hash: '0x12345' }, + }, + } as TestCase<'/tx/[hash]'>, + { + title: 'dynamic route with API data', + route: { + pathname: '/token/[hash]', + query: { hash: '0x12345' }, + }, + apiData: { symbol: 'USDT' }, + } as TestCase<'/token/[hash]'>, +]; + +describe('generates correct metadata for:', () => { + TEST_CASES.forEach((testCase) => { + it(`${ testCase.title }`, () => { + const result = generate(testCase.route, testCase.apiData); + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/lib/metadata/generate.ts b/lib/metadata/generate.ts new file mode 100644 index 0000000000..9282da7fe7 --- /dev/null +++ b/lib/metadata/generate.ts @@ -0,0 +1,37 @@ +import type { ApiData, Metadata } from './types'; +import type { RouteParams } from 'nextjs/types'; + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; +import getNetworkTitle from 'lib/networks/getNetworkTitle'; + +import compileValue from './compileValue'; +import getCanonicalUrl from './getCanonicalUrl'; +import getPageOgType from './getPageOgType'; +import * as templates from './templates'; + +export default function generate(route: RouteParams, apiData: ApiData = null): Metadata { + const params = { + ...route.query, + ...apiData, + network_name: config.chain.name, + network_title: getNetworkTitle(), + }; + + const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); + const description = compileValue(templates.description.make(route.pathname), params); + + const pageOgType = getPageOgType(route.pathname); + + return { + title: title, + description, + opengraph: { + title: title, + description: pageOgType !== 'Regular page' ? config.meta.og.description : '', + imageUrl: pageOgType !== 'Regular page' ? config.meta.og.imageUrl : '', + }, + canonical: getCanonicalUrl(route.pathname), + }; +} diff --git a/lib/metadata/getCanonicalUrl.ts b/lib/metadata/getCanonicalUrl.ts new file mode 100644 index 0000000000..2a868419a8 --- /dev/null +++ b/lib/metadata/getCanonicalUrl.ts @@ -0,0 +1,24 @@ +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const CANONICAL_ROUTES: Array = [ + '/', + '/txs', + '/ops', + '/verified-contracts', + '/name-domains', + '/withdrawals', + '/tokens', + '/stats', + '/api-docs', + '/graphiql', + '/gas-tracker', + '/apps', +]; + +export default function getCanonicalUrl(pathname: Route['pathname']) { + if (CANONICAL_ROUTES.includes(pathname)) { + return config.app.baseUrl + pathname; + } +} diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts new file mode 100644 index 0000000000..a59757301e --- /dev/null +++ b/lib/metadata/getPageOgType.ts @@ -0,0 +1,70 @@ +import type { Route } from 'nextjs-routes'; + +type OGPageType = 'Homepage' | 'Root page' | 'Regular page'; + +const OG_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Root page', + '/txs/kettle/[hash]': 'Regular page', + '/tx/[hash]': 'Regular page', + '/blocks': 'Root page', + '/block/[height_or_hash]': 'Regular page', + '/block/countdown': 'Regular page', + '/block/countdown/[height]': 'Regular page', + '/accounts': 'Root page', + '/address/[hash]': 'Regular page', + '/verified-contracts': 'Root page', + '/contract-verification': 'Root page', + '/address/[hash]/contract-verification': 'Regular page', + '/tokens': 'Root page', + '/token/[hash]': 'Regular page', + '/token/[hash]/instance/[id]': 'Regular page', + '/apps': 'Root page', + '/apps/[id]': 'Regular page', + '/stats': 'Root page', + '/api-docs': 'Regular page', + '/graphiql': 'Regular page', + '/search-results': 'Regular page', + '/auth/profile': 'Root page', + '/account/watchlist': 'Regular page', + '/account/api-key': 'Regular page', + '/account/custom-abi': 'Regular page', + '/account/tag-address': 'Regular page', + '/account/verified-addresses': 'Root page', + '/public-tags/submit': 'Regular page', + '/withdrawals': 'Root page', + '/visualize/sol2uml': 'Regular page', + '/csv-export': 'Regular page', + '/deposits': 'Root page', + '/output-roots': 'Root page', + '/dispute-games': 'Root page', + '/batches': 'Root page', + '/batches/[number]': 'Regular page', + '/blobs/[hash]': 'Regular page', + '/ops': 'Root page', + '/op/[hash]': 'Regular page', + '/404': 'Regular page', + '/name-domains': 'Root page', + '/name-domains/[name]': 'Regular page', + '/validators': 'Root page', + '/gas-tracker': 'Root page', + '/mud-worlds': 'Root page', + + // service routes, added only to make typescript happy + '/login': 'Regular page', + '/sprite': 'Regular page', + '/api/metrics': 'Regular page', + '/api/log': 'Regular page', + '/api/media-type': 'Regular page', + '/api/proxy': 'Regular page', + '/api/csrf': 'Regular page', + '/api/healthz': 'Regular page', + '/api/config': 'Regular page', + '/api/sprite': 'Regular page', + '/auth/auth0': 'Regular page', + '/auth/unverified-email': 'Regular page', +}; + +export default function getPageOgType(pathname: Route['pathname']) { + return OG_TYPE_DICT[pathname]; +} diff --git a/lib/metadata/index.ts b/lib/metadata/index.ts new file mode 100644 index 0000000000..903bd988e8 --- /dev/null +++ b/lib/metadata/index.ts @@ -0,0 +1,3 @@ +export { default as generate } from './generate'; +export { default as update } from './update'; +export * from './types'; diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts new file mode 100644 index 0000000000..335fd7d3a7 --- /dev/null +++ b/lib/metadata/templates/description.ts @@ -0,0 +1,76 @@ +/* eslint-disable max-len */ +import type { Route } from 'nextjs-routes'; + +// equal og:description +// eslint-disable-next-line max-len +const DEFAULT_TEMPLATE = 'Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.'; + +// FIXME all page descriptions will be updated later +const TEMPLATE_MAP: Record = { + '/': DEFAULT_TEMPLATE, + '/txs': DEFAULT_TEMPLATE, + '/txs/kettle/[hash]': DEFAULT_TEMPLATE, + '/tx/[hash]': 'View transaction %hash% on %network_title%', + '/blocks': DEFAULT_TEMPLATE, + '/block/[height_or_hash]': 'View the transactions, token transfers, and uncles for block %height_or_hash%', + '/block/countdown': DEFAULT_TEMPLATE, + '/block/countdown/[height]': DEFAULT_TEMPLATE, + '/accounts': DEFAULT_TEMPLATE, + '/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%', + '/verified-contracts': DEFAULT_TEMPLATE, + '/contract-verification': DEFAULT_TEMPLATE, + '/address/[hash]/contract-verification': 'View the account balance, transactions, and other data for %hash% on the %network_title%', + '/tokens': DEFAULT_TEMPLATE, + '/token/[hash]': '%hash%, balances and analytics on the %network_title%', + '/token/[hash]/instance/[id]': '%hash%, balances and analytics on the %network_title%', + '/apps': DEFAULT_TEMPLATE, + '/apps/[id]': DEFAULT_TEMPLATE, + '/stats': DEFAULT_TEMPLATE, + '/api-docs': DEFAULT_TEMPLATE, + '/graphiql': DEFAULT_TEMPLATE, + '/search-results': DEFAULT_TEMPLATE, + '/auth/profile': DEFAULT_TEMPLATE, + '/account/watchlist': DEFAULT_TEMPLATE, + '/account/api-key': DEFAULT_TEMPLATE, + '/account/custom-abi': DEFAULT_TEMPLATE, + '/account/tag-address': DEFAULT_TEMPLATE, + '/account/verified-addresses': DEFAULT_TEMPLATE, + '/public-tags/submit': 'Propose a new public tag for your address, contract or set of contracts for your dApp. Our team will review and approve your submission. Public tags are incredible tool which helps users identify contracts and addresses.', + '/withdrawals': DEFAULT_TEMPLATE, + '/visualize/sol2uml': DEFAULT_TEMPLATE, + '/csv-export': DEFAULT_TEMPLATE, + '/deposits': DEFAULT_TEMPLATE, + '/output-roots': DEFAULT_TEMPLATE, + '/dispute-games': DEFAULT_TEMPLATE, + '/batches': DEFAULT_TEMPLATE, + '/batches/[number]': DEFAULT_TEMPLATE, + '/blobs/[hash]': DEFAULT_TEMPLATE, + '/ops': DEFAULT_TEMPLATE, + '/op/[hash]': DEFAULT_TEMPLATE, + '/404': DEFAULT_TEMPLATE, + '/name-domains': DEFAULT_TEMPLATE, + '/name-domains/[name]': DEFAULT_TEMPLATE, + '/validators': DEFAULT_TEMPLATE, + '/gas-tracker': DEFAULT_TEMPLATE, + '/mud-worlds': DEFAULT_TEMPLATE, + + // service routes, added only to make typescript happy + '/login': DEFAULT_TEMPLATE, + '/sprite': DEFAULT_TEMPLATE, + '/api/metrics': DEFAULT_TEMPLATE, + '/api/log': DEFAULT_TEMPLATE, + '/api/media-type': DEFAULT_TEMPLATE, + '/api/proxy': DEFAULT_TEMPLATE, + '/api/csrf': DEFAULT_TEMPLATE, + '/api/healthz': DEFAULT_TEMPLATE, + '/api/config': DEFAULT_TEMPLATE, + '/api/sprite': DEFAULT_TEMPLATE, + '/auth/auth0': DEFAULT_TEMPLATE, + '/auth/unverified-email': DEFAULT_TEMPLATE, +}; + +export function make(pathname: Route['pathname']) { + const template = TEMPLATE_MAP[pathname]; + + return template ?? ''; +} diff --git a/lib/metadata/templates/index.ts b/lib/metadata/templates/index.ts new file mode 100644 index 0000000000..159fca123e --- /dev/null +++ b/lib/metadata/templates/index.ts @@ -0,0 +1,2 @@ +export * as title from './title'; +export * as description from './description'; diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts new file mode 100644 index 0000000000..904b821a8e --- /dev/null +++ b/lib/metadata/templates/title.ts @@ -0,0 +1,80 @@ +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; + +const TEMPLATE_MAP: Record = { + '/': '%network_name% blockchain explorer - View %network_name% stats', + '/txs': '%network_name% transactions - %network_name% explorer', + '/txs/kettle/[hash]': '%network_name% kettle %hash% transactions', + '/tx/[hash]': '%network_name% transaction %hash%', + '/blocks': '%network_name% blocks', + '/block/[height_or_hash]': '%network_name% block %height_or_hash%', + '/block/countdown': '%network_name% block countdown index', + '/block/countdown/[height]': '%network_name% block %height% countdown', + '/accounts': '%network_name% top accounts', + '/address/[hash]': '%network_name% address details for %hash%', + '/verified-contracts': 'Verified %network_name% contracts lookup - %network_name% explorer', + '/contract-verification': '%network_name% verify contract', + '/address/[hash]/contract-verification': '%network_name% contract verification for %hash%', + '/tokens': 'Tokens list - %network_name% explorer', + '/token/[hash]': '%network_name% token details', + '/token/[hash]/instance/[id]': '%network_name% NFT instance', + '/apps': '%network_name% DApps - Explore top apps', + '/apps/[id]': '%network_name% marketplace app', + '/stats': '%network_name% stats - %network_name% network insights', + '/api-docs': '%network_name% API docs - %network_name% developer tools', + '/graphiql': 'GraphQL for %network_name% - %network_name% data query', + '/search-results': '%network_name% search result for %q%', + '/auth/profile': '%network_name% - my profile', + '/account/watchlist': '%network_name% - watchlist', + '/account/api-key': '%network_name% - API keys', + '/account/custom-abi': '%network_name% - custom ABI', + '/account/tag-address': '%network_name% - private tags', + '/account/verified-addresses': '%network_name% - my verified addresses', + '/public-tags/submit': '%network_name% - public tag requests', + '/withdrawals': '%network_name% withdrawals - track on %network_name% explorer', + '/visualize/sol2uml': '%network_name% Solidity UML diagram', + '/csv-export': '%network_name% export data to CSV', + '/deposits': '%network_name% deposits (L1 > L2)', + '/output-roots': '%network_name% output roots', + '/dispute-games': '%network_name% dispute games', + '/batches': '%network_name% tx batches (L2 blocks)', + '/batches/[number]': '%network_name% L2 tx batch %number%', + '/blobs/[hash]': '%network_name% blob %hash% details', + '/ops': 'User operations on %network_name% - %network_name% explorer', + '/op/[hash]': '%network_name% user operation %hash%', + '/404': '%network_name% error - page not found', + '/name-domains': '%network_name% name domains - %network_name% explorer', + '/name-domains/[name]': '%network_name% %name% domain details', + '/validators': '%network_name% validators list', + '/gas-tracker': '%network_name% gas tracker - Current gas fees', + '/mud-worlds': '%network_name% MUD worlds list', + + // service routes, added only to make typescript happy + '/login': '%network_name% login', + '/sprite': '%network_name% SVG sprite', + '/api/metrics': '%network_name% node API prometheus metrics', + '/api/log': '%network_name% node API request log', + '/api/media-type': '%network_name% node API media type', + '/api/proxy': '%network_name% node API proxy', + '/api/csrf': '%network_name% node API CSRF token', + '/api/healthz': '%network_name% node API health check', + '/api/config': '%network_name% node API app config', + '/api/sprite': '%network_name% node API SVG sprite content', + '/auth/auth0': '%network_name% authentication', + '/auth/unverified-email': '%network_name% unverified email', +}; + +const TEMPLATE_MAP_ENHANCED: Partial> = { + '/token/[hash]': '%network_name% %symbol% token details', + '/token/[hash]/instance/[id]': '%network_name% token instance for %symbol%', + '/apps/[id]': '%network_name% - %app_name%', + '/address/[hash]': '%network_name% address details for %domain_name%', +}; + +export function make(pathname: Route['pathname'], isEnriched = false) { + const template = (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname]; + const postfix = config.meta.promoteBlockscoutInTitle ? ' | Blockscout' : ''; + + return (template + postfix).trim(); +} diff --git a/lib/metadata/types.ts b/lib/metadata/types.ts new file mode 100644 index 0000000000..fda74301ba --- /dev/null +++ b/lib/metadata/types.ts @@ -0,0 +1,24 @@ +import type { TokenInfo } from 'types/api/token'; + +import type { Route } from 'nextjs-routes'; + +/* eslint-disable @typescript-eslint/indent */ +export type ApiData = +( + Pathname extends '/address/[hash]' ? { domain_name: string } : + Pathname extends '/token/[hash]' ? TokenInfo : + Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } : + Pathname extends '/apps/[id]' ? { app_name: string } : + never +) | null; + +export interface Metadata { + title: string; + description: string; + opengraph: { + title: string; + description?: string; + imageUrl?: string; + }; + canonical: string | undefined; +} diff --git a/lib/metadata/update.ts b/lib/metadata/update.ts new file mode 100644 index 0000000000..123e3ca100 --- /dev/null +++ b/lib/metadata/update.ts @@ -0,0 +1,13 @@ +import type { ApiData } from './types'; +import type { RouteParams } from 'nextjs/types'; + +import type { Route } from 'nextjs-routes'; + +import generate from './generate'; + +export default function update(route: RouteParams, apiData: ApiData) { + const { title, description } = generate(route, apiData); + + window.document.title = title; + window.document.querySelector('meta[name="description"]')?.setAttribute('content', description); +} diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts new file mode 100644 index 0000000000..0c565caed7 --- /dev/null +++ b/lib/mixpanel/getPageType.ts @@ -0,0 +1,68 @@ +import type { Route } from 'nextjs-routes'; + +export const PAGE_TYPE_DICT: Record = { + '/': 'Homepage', + '/txs': 'Transactions', + '/txs/kettle/[hash]': 'Kettle transactions', + '/tx/[hash]': 'Transaction details', + '/blocks': 'Blocks', + '/block/[height_or_hash]': 'Block details', + '/block/countdown': 'Block countdown search', + '/block/countdown/[height]': 'Block countdown', + '/accounts': 'Top accounts', + '/address/[hash]': 'Address details', + '/verified-contracts': 'Verified contracts', + '/contract-verification': 'Contract verification', + '/address/[hash]/contract-verification': 'Contract verification for address', + '/tokens': 'Tokens', + '/token/[hash]': 'Token details', + '/token/[hash]/instance/[id]': 'Token Instance', + '/apps': 'DApps', + '/apps/[id]': 'DApp', + '/stats': 'Stats', + '/api-docs': 'REST API', + '/graphiql': 'GraphQL', + '/search-results': 'Search results', + '/auth/profile': 'Profile', + '/account/watchlist': 'Watchlist', + '/account/api-key': 'API keys', + '/account/custom-abi': 'Custom ABI', + '/account/tag-address': 'Private tags', + '/account/verified-addresses': 'Verified addresses', + '/public-tags/submit': 'Submit public tag', + '/withdrawals': 'Withdrawals', + '/visualize/sol2uml': 'Solidity UML diagram', + '/csv-export': 'Export data to CSV file', + '/deposits': 'Deposits (L1 > L2)', + '/output-roots': 'Output roots', + '/dispute-games': 'Dispute games', + '/batches': 'Tx batches (L2 blocks)', + '/batches/[number]': 'L2 tx batch details', + '/blobs/[hash]': 'Blob details', + '/ops': 'User operations', + '/op/[hash]': 'User operation details', + '/404': '404', + '/name-domains': 'Domains search and resolve', + '/name-domains/[name]': 'Domain details', + '/validators': 'Validators list', + '/gas-tracker': 'Gas tracker', + '/mud-worlds': 'MUD worlds', + + // service routes, added only to make typescript happy + '/login': 'Login', + '/sprite': 'Sprite', + '/api/metrics': 'Node API: Prometheus metrics', + '/api/log': 'Node API: Request log', + '/api/media-type': 'Node API: Media type', + '/api/proxy': 'Node API: Proxy', + '/api/csrf': 'Node API: CSRF token', + '/api/healthz': 'Node API: Health check', + '/api/config': 'Node API: App config', + '/api/sprite': 'Node API: SVG sprite content', + '/auth/auth0': 'Auth', + '/auth/unverified-email': 'Unverified email', +}; + +export default function getPageType(pathname: Route['pathname']) { + return PAGE_TYPE_DICT[pathname] || 'Unknown page'; +} diff --git a/lib/mixpanel/getTabName.ts b/lib/mixpanel/getTabName.ts new file mode 100644 index 0000000000..3ca3b8cae8 --- /dev/null +++ b/lib/mixpanel/getTabName.ts @@ -0,0 +1,5 @@ +import _capitalize from 'lodash/capitalize'; + +export default function getTabName(tab: string) { + return tab !== '' ? _capitalize(tab.replaceAll('_', ' ')) : 'Default'; +} diff --git a/lib/mixpanel/getUuid.ts b/lib/mixpanel/getUuid.ts new file mode 100644 index 0000000000..797034ceae --- /dev/null +++ b/lib/mixpanel/getUuid.ts @@ -0,0 +1,20 @@ +import * as cookies from 'lib/cookies'; +import * as growthBook from 'lib/growthbook/consts'; +import isBrowser from 'lib/isBrowser'; + +export default function getUuid() { + const cookie = cookies.get(cookies.NAMES.UUID); + + if (cookie) { + return cookie; + } + + const uuid = crypto.randomUUID(); + cookies.set(cookies.NAMES.UUID, uuid); + + if (isBrowser()) { + window.localStorage.removeItem(growthBook.STORAGE_KEY); + } + + return uuid; +} diff --git a/lib/mixpanel/index.ts b/lib/mixpanel/index.ts new file mode 100644 index 0000000000..72f7be7285 --- /dev/null +++ b/lib/mixpanel/index.ts @@ -0,0 +1,16 @@ +import getPageType from './getPageType'; +import getUuid from './getUuid'; +import logEvent from './logEvent'; +import useInit from './useInit'; +import useLogPageView from './useLogPageView'; +import * as userProfile from './userProfile'; +export * from './utils'; + +export { + useInit, + useLogPageView, + logEvent, + getPageType, + getUuid, + userProfile, +}; diff --git a/lib/mixpanel/logEvent.ts b/lib/mixpanel/logEvent.ts new file mode 100644 index 0000000000..8a001351ea --- /dev/null +++ b/lib/mixpanel/logEvent.ts @@ -0,0 +1,19 @@ +import mixpanel from 'mixpanel-browser'; + +import config from 'configs/app'; + +import type { EventTypes, EventPayload } from './utils'; + +type TrackFnArgs = Parameters; + +export default function logEvent( + type: EventType, + properties?: EventPayload, + optionsOrCallback?: TrackFnArgs[2], + callback?: TrackFnArgs[3], +) { + if (!config.features.mixpanel.isEnabled) { + return; + } + mixpanel.track(type, properties, optionsOrCallback, callback); +} diff --git a/lib/mixpanel/useInit.tsx b/lib/mixpanel/useInit.tsx new file mode 100644 index 0000000000..23f02332f8 --- /dev/null +++ b/lib/mixpanel/useInit.tsx @@ -0,0 +1,62 @@ +import _capitalize from 'lodash/capitalize'; +import type { Config } from 'mixpanel-browser'; +import mixpanel from 'mixpanel-browser'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { deviceType } from 'react-device-detect'; + +import config from 'configs/app'; +import * as cookies from 'lib/cookies'; +import dayjs from 'lib/date/dayjs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import getUuid from './getUuid'; +import * as userProfile from './userProfile'; + +export default function useMixpanelInit() { + const [ isInited, setIsInited ] = React.useState(false); + const router = useRouter(); + const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug)); + + React.useEffect(() => { + const feature = config.features.mixpanel; + if (!feature.isEnabled) { + return; + } + + const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG); + + const mixpanelConfig: Partial = { + debug: Boolean(debugFlagQuery.current || debugFlagCookie), + }; + const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); + const userId = getUuid(); + + mixpanel.init(feature.projectToken, mixpanelConfig); + mixpanel.register({ + 'Chain id': config.chain.id, + Environment: config.app.isDev ? 'Dev' : 'Prod', + Authorized: isAuth, + 'Viewport width': window.innerWidth, + 'Viewport height': window.innerHeight, + Language: window.navigator.language, + 'Device type': _capitalize(deviceType), + 'User id': userId, + }); + mixpanel.identify(userId); + userProfile.set({ + 'Device Type': _capitalize(deviceType), + ...(isAuth ? { 'With Account': true } : {}), + }); + userProfile.setOnce({ + 'First Time Join': dayjs().toISOString(), + }); + + setIsInited(true); + if (debugFlagQuery.current && !debugFlagCookie) { + cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true'); + } + }, []); + + return isInited; +} diff --git a/lib/mixpanel/useLogPageView.tsx b/lib/mixpanel/useLogPageView.tsx new file mode 100644 index 0000000000..9c3f6a929a --- /dev/null +++ b/lib/mixpanel/useLogPageView.tsx @@ -0,0 +1,39 @@ +import { useColorMode } from '@chakra-ui/react'; +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import getPageType from './getPageType'; +import getTabName from './getTabName'; +import logEvent from './logEvent'; +import { EventTypes } from './utils'; + +export default function useLogPageView(isInited: boolean) { + const router = useRouter(); + const pathname = usePathname(); + + const tab = getQueryParamString(router.query.tab); + const page = getQueryParamString(router.query.page); + const { colorMode } = useColorMode(); + + React.useEffect(() => { + if (!config.features.mixpanel.isEnabled || !isInited) { + return; + } + + logEvent(EventTypes.PAGE_VIEW, { + 'Page type': getPageType(router.pathname), + Tab: getTabName(tab), + Page: page || undefined, + 'Color mode': colorMode, + }); + // these are only deps that should trigger the effect + // in some scenarios page type is not changing (e.g navigation from one address page to another), + // but we still want to log page view + // so we use pathname from 'next/navigation' instead of router.pathname from 'next/router' as deps + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isInited, page, pathname, tab, colorMode ]); +} diff --git a/lib/mixpanel/userProfile.ts b/lib/mixpanel/userProfile.ts new file mode 100644 index 0000000000..774d124723 --- /dev/null +++ b/lib/mixpanel/userProfile.ts @@ -0,0 +1,24 @@ +import mixpanel from 'mixpanel-browser'; + +import type { PickByType } from 'types/utils'; + +interface UserProfileProperties { + 'With Account': boolean; + 'With Connected Wallet': boolean; + 'Device Type': string; + 'First Time Join': string; +} + +type UserProfilePropertiesNumerable = PickByType; + +export function set(props: Partial) { + mixpanel.people.set(props); +} + +export function setOnce(props: Partial) { + mixpanel.people.set_once(props); +} + +export function increment(props: UserProfilePropertiesNumerable) { + mixpanel.people.increment(props); +} diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts new file mode 100644 index 0000000000..f769c3e29a --- /dev/null +++ b/lib/mixpanel/utils.ts @@ -0,0 +1,146 @@ +import type { WalletType } from 'types/client/wallets'; + +export enum EventTypes { + PAGE_VIEW = 'Page view', + SEARCH_QUERY = 'Search query', + LOCAL_SEARCH = 'Local search', + ADD_TO_WALLET = 'Add to wallet', + ACCOUNT_ACCESS = 'Account access', + PRIVATE_TAG = 'Private tag', + VERIFY_ADDRESS = 'Verify address', + VERIFY_TOKEN = 'Verify token', + WALLET_CONNECT = 'Wallet connect', + WALLET_ACTION = 'Wallet action', + CONTRACT_INTERACTION = 'Contract interaction', + CONTRACT_VERIFICATION = 'Contract verification', + QR_CODE = 'QR code', + PAGE_WIDGET = 'Page widget', + TX_INTERPRETATION_INTERACTION = 'Transaction interpretation interaction', + EXPERIMENT_STARTED = 'Experiment started', + FILTERS = 'Filters', + BUTTON_CLICK = 'Button click', + PROMO_BANNER = 'Promo banner', + APP_FEEDBACK = 'App feedback', +} + +/* eslint-disable @typescript-eslint/indent */ +export type EventPayload = +Type extends EventTypes.PAGE_VIEW ? +{ + 'Page type': string; + 'Tab': string; + 'Page'?: string; + 'Color mode': 'light' | 'dark'; +} : +Type extends EventTypes.SEARCH_QUERY ? { + 'Search query': string; + 'Source page type': string; + 'Result URL': string; +} : +Type extends EventTypes.LOCAL_SEARCH ? { + 'Search query': string; + 'Source': 'Marketplace'; +} : +Type extends EventTypes.ADD_TO_WALLET ? ( + { + 'Wallet': WalletType; + 'Target': 'network'; + } | { + 'Wallet': WalletType; + 'Target': 'token'; + 'Token': string; + } +) : +Type extends EventTypes.ACCOUNT_ACCESS ? { + 'Action': 'Auth0 init' | 'Verification email resent' | 'Logged out'; +} : +Type extends EventTypes.PRIVATE_TAG ? { + 'Action': 'Form opened' | 'Submit'; + 'Page type': string; + 'Tag type': 'Address' | 'Tx'; +} : +Type extends EventTypes.VERIFY_ADDRESS ? ( + { + 'Action': 'Form opened' | 'Address entered'; + 'Page type': string; + } | { + 'Action': 'Sign ownership'; + 'Page type': string; + 'Sign method': 'wallet' | 'manual'; + } +) : +Type extends EventTypes.VERIFY_TOKEN ? { + 'Action': 'Form opened' | 'Submit'; +} : +Type extends EventTypes.WALLET_CONNECT ? { + 'Source': 'Header' | 'Smart contracts' | 'Swap button'; + 'Status': 'Started' | 'Connected'; +} : +Type extends EventTypes.WALLET_ACTION ? ( + { + 'Action': 'Open' | 'Address click'; + } | { + 'Action': 'Send Transaction' | 'Sign Message' | 'Sign Typed Data'; + 'Address': string | undefined; + 'AppId': string; + } +) : +Type extends EventTypes.CONTRACT_INTERACTION ? { + 'Method type': 'Read' | 'Write'; + 'Method name': string; +} : +Type extends EventTypes.CONTRACT_VERIFICATION ? { + 'Method': string; + 'Status': 'Method selected' | 'Finished'; +} : +Type extends EventTypes.QR_CODE ? { + 'Page type': string; +} : +Type extends EventTypes.PAGE_WIDGET ? ( + { + 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; + } | { + 'Type': 'Favorite app' | 'More button' | 'Security score' | 'Total contracts' | 'Verified contracts' | 'Analyzed contracts'; + 'Info': string; + 'Source': 'Discovery view' | 'Security view' | 'App modal' | 'App page' | 'Security score popup' | 'Banner'; + } | { + 'Type': 'Security score'; + 'Source': 'Analyzed contracts popup'; + } | { + 'Type': 'Action button'; + 'Info': string; + 'Source': 'Txn' | 'NFT collection' | 'NFT item'; + } | { + 'Type': 'Address tag'; + 'Info': string; + 'URL': string; + } +) : +Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { + 'Type': 'Address click' | 'Token click' | 'Domain click'; +} : +Type extends EventTypes.EXPERIMENT_STARTED ? { + 'Experiment name': string; + 'Variant name': string; + 'Source': 'growthbook'; +} : +Type extends EventTypes.FILTERS ? { + 'Source': 'Marketplace'; + 'Filter name': string; +} : +Type extends EventTypes.BUTTON_CLICK ? { + 'Content': string; + 'Source': string; +} : +Type extends EventTypes.PROMO_BANNER ? { + 'Source': 'Marketplace'; + 'Link': string; +} : +Type extends EventTypes.APP_FEEDBACK ? { + 'Action': 'Rating'; + 'Source': 'Discovery' | 'App modal' | 'App page'; + 'AppId': string; + 'Score': number; +} : +undefined; +/* eslint-enable @typescript-eslint/indent */ diff --git a/lib/monitoring/metrics.ts b/lib/monitoring/metrics.ts new file mode 100644 index 0000000000..f6f6b1a3e7 --- /dev/null +++ b/lib/monitoring/metrics.ts @@ -0,0 +1,33 @@ +import * as promClient from 'prom-client'; + +const metrics = (() => { + // eslint-disable-next-line no-restricted-properties + if (process.env.PROMETHEUS_METRICS_ENABLED !== 'true') { + return; + } + + promClient.register.clear(); + + const socialPreviewBotRequests = new promClient.Counter({ + name: 'social_preview_bot_requests_total', + help: 'Number of incoming requests from social preview bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const searchEngineBotRequests = new promClient.Counter({ + name: 'search_engine_bot_requests_total', + help: 'Number of incoming requests from search engine bots', + labelNames: [ 'route', 'bot' ] as const, + }); + + const apiRequestDuration = new promClient.Histogram({ + name: 'api_request_duration_seconds', + help: 'Duration of requests to API in seconds', + labelNames: [ 'route', 'code' ], + buckets: [ 0.2, 0.5, 1, 3, 10 ], + }); + + return { socialPreviewBotRequests, searchEngineBotRequests, apiRequestDuration }; +})(); + +export default metrics; diff --git a/lib/networks/getNetworkTitle.ts b/lib/networks/getNetworkTitle.ts new file mode 100644 index 0000000000..5bcb602e39 --- /dev/null +++ b/lib/networks/getNetworkTitle.ts @@ -0,0 +1,6 @@ +import config from 'configs/app'; + +// TODO delete when page descriptions is refactored +export default function getNetworkTitle() { + return config.chain.name + (config.chain.shortName ? ` (${ config.chain.shortName })` : '') + ' Explorer'; +} diff --git a/lib/networks/getNetworkValidatorTitle.ts b/lib/networks/getNetworkValidatorTitle.ts new file mode 100644 index 0000000000..7435ee0293 --- /dev/null +++ b/lib/networks/getNetworkValidatorTitle.ts @@ -0,0 +1,5 @@ +import config from 'configs/app'; + +export default function getNetworkValidatorTitle() { + return config.chain.verificationType === 'validation' ? 'validator' : 'miner'; +} diff --git a/lib/networks/networkExplorers.ts b/lib/networks/networkExplorers.ts new file mode 100644 index 0000000000..cfcc828115 --- /dev/null +++ b/lib/networks/networkExplorers.ts @@ -0,0 +1,39 @@ +import _compose from 'lodash/fp/compose'; +import _mapValues from 'lodash/mapValues'; + +import type { NetworkExplorer } from 'types/networks'; + +import config from 'configs/app'; + +// for easy .env update +// const NETWORK_EXPLORERS = JSON.stringify([ +// { +// title: 'Anyblock', +// baseUrl: 'https://explorer.anyblock.tools', +// paths: { +// tx: '/ethereum/ethereum/goerli/transaction', +// address: '/ethereum/ethereum/goerli/address' +// }, +// }, +// { +// title: 'Etherscan', +// baseUrl: 'https://goerli.etherscan.io/', +// paths: { +// tx: '/tx', +// address: '/address', +// }, +// }, +// ]).replaceAll('"', '\''); + +const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str; +const addLeadingSlash = (str: string) => str[0] === '/' ? str : '/' + str; + +const networkExplorers: Array = (() => { + return config.UI.explorers.items.map((explorer) => ({ + ...explorer, + baseUrl: stripTrailingSlash(explorer.baseUrl), + paths: _mapValues(explorer.paths, _compose(stripTrailingSlash, addLeadingSlash)), + })); +})(); + +export default networkExplorers; diff --git a/lib/recentSearchKeywords.ts b/lib/recentSearchKeywords.ts new file mode 100644 index 0000000000..5fe5a6bd71 --- /dev/null +++ b/lib/recentSearchKeywords.ts @@ -0,0 +1,56 @@ +import _uniq from 'lodash/uniq'; + +import isBrowser from './isBrowser'; + +const RECENT_KEYWORDS_LS_KEY = 'recent_search_keywords'; +const MAX_KEYWORDS_NUMBER = 10; + +const parseKeywordsArray = (keywordsStr: string) => { + if (!keywordsStr) { + return []; + } + + try { + const parsedResult = JSON.parse(keywordsStr); + if (Array.isArray(parsedResult)) { + return parsedResult; + } + return []; + } catch (error) { + return []; + } +}; + +export function saveToRecentKeywords(value: string) { + if (!value) { + return; + } + + const keywordsArr = getRecentSearchKeywords(); + const result = _uniq([ value, ...keywordsArr ]).slice(0, MAX_KEYWORDS_NUMBER - 1); + window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result)); +} + +export function getRecentSearchKeywords(input?: string) { + if (!isBrowser()) { + return []; + } + const keywordsStr = window.localStorage.getItem(RECENT_KEYWORDS_LS_KEY) || ''; + const keywordsArr = parseKeywordsArray(keywordsStr) as Array; + if (!input) { + return keywordsArr; + } + + return keywordsArr.filter(kw => kw.includes(input)); +} + +export function removeRecentSearchKeyword(value: string) { + + const keywordsArr = getRecentSearchKeywords(); + const result = keywordsArr.filter(kw => kw !== value); + window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result)); +} + +export function clearRecentSearchKeywords() { + window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, ''); +} diff --git a/lib/regexp.ts b/lib/regexp.ts new file mode 100644 index 0000000000..ab2b3288e3 --- /dev/null +++ b/lib/regexp.ts @@ -0,0 +1,9 @@ +export const URL_PREFIX = /^https?:\/\//i; + +export const IPFS_PREFIX = /^ipfs:\/\//i; + +export const HEX_REGEXP = /^(?:0x)?[\da-fA-F]+$/; + +export const FILE_EXTENSION = /\.([\da-z]+)$/i; + +export const BLOCK_HEIGHT = /^\d+$/; diff --git a/lib/router/getQueryParamString.ts b/lib/router/getQueryParamString.ts new file mode 100644 index 0000000000..a34f2a45ca --- /dev/null +++ b/lib/router/getQueryParamString.ts @@ -0,0 +1,7 @@ +export default function getQueryParamString(param: string | Array | undefined): string { + if (Array.isArray(param)) { + return param.join(','); + } + + return param || ''; +} diff --git a/lib/router/removeQueryParam.ts b/lib/router/removeQueryParam.ts new file mode 100644 index 0000000000..d0a37975c7 --- /dev/null +++ b/lib/router/removeQueryParam.ts @@ -0,0 +1,12 @@ +import type { NextRouter } from 'next/router'; + +export default function removeQueryParam(router: NextRouter, param: string) { + const { pathname, query, asPath } = router; + const newQuery = { ...query }; + delete newQuery[param]; + + const hashIndex = asPath.indexOf('#'); + const hash = hashIndex !== -1 ? asPath.substring(hashIndex) : ''; + + router.replace({ pathname, query: newQuery, hash }, undefined, { shallow: true }); +} diff --git a/lib/router/updateQueryParam.ts b/lib/router/updateQueryParam.ts new file mode 100644 index 0000000000..1dfb1f2a69 --- /dev/null +++ b/lib/router/updateQueryParam.ts @@ -0,0 +1,12 @@ +import type { NextRouter } from 'next/router'; + +export default function updateQueryParam(router: NextRouter, param: string, newValue: string) { + const { pathname, query, asPath } = router; + const newQuery = { ...query }; + newQuery[param] = newValue; + + const hashIndex = asPath.indexOf('#'); + const hash = hashIndex !== -1 ? asPath.substring(hashIndex) : ''; + + router.replace({ pathname, query: newQuery, hash }, undefined, { shallow: true }); +} diff --git a/lib/saveAsCSV.ts b/lib/saveAsCSV.ts new file mode 100644 index 0000000000..0a04b4ddae --- /dev/null +++ b/lib/saveAsCSV.ts @@ -0,0 +1,14 @@ +import { unparse } from 'papaparse'; + +import downloadBlob from 'lib/downloadBlob'; + +export default function saveAsCSV(headerRows: Array, dataRows: Array>, filename: string) { + const csv = unparse([ + headerRows, + ...dataRows, + ]); + + const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' }); + + downloadBlob(blob, filename); +} diff --git a/lib/sentry/config.ts b/lib/sentry/config.ts new file mode 100644 index 0000000000..f619c9346c --- /dev/null +++ b/lib/sentry/config.ts @@ -0,0 +1,108 @@ +import * as Sentry from '@sentry/react'; +import { BrowserTracing } from '@sentry/tracing'; + +import appConfig from 'configs/app'; +import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError'; + +const feature = appConfig.features.sentry; + +export const config: Sentry.BrowserOptions | undefined = (() => { + if (!feature.isEnabled) { + return; + } + + const tracesSampleRate: number | undefined = (() => { + switch (feature.environment) { + case 'development': + return 1; + case 'staging': + return 0.75; + case 'production': + return 0.2; + } + })(); + + return { + environment: feature.environment, + dsn: feature.dsn, + release: feature.release, + enableTracing: feature.enableTracing, + tracesSampleRate, + integrations: feature.enableTracing ? [ new BrowserTracing() ] : undefined, + + // error filtering settings + // were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry + ignoreErrors: [ + // Random plugins/extensions + 'top.GLOBALS', + // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html + 'originalCreateNotification', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'http://tt.epicplay.com', + 'Can\'t find variable: ZiteReader', + 'jigsaw is not defined', + 'ComboSearch is not defined', + 'http://loading.retry.widdit.com/', + 'atomicFindClose', + // Facebook borked + 'fb_xd_fragment', + // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha) + // See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy + 'bmi_SafeAddOnload', + 'EBCallBackMessageReceived', + // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + 'conduitPage', + // Generic error code from errors outside the security sandbox + 'Script error.', + + // Relay and WalletConnect errors + 'The quota has been exceeded', + 'Attempt to connect to relay via', + 'WebSocket connection failed for URL: wss://relay.walletconnect.com', + + // API errors + RESOURCE_LOAD_ERROR_MESSAGE, + ], + denyUrls: [ + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome and other extensions + /extensions\//i, + /^chrome:\/\//i, + /^chrome-extension:\/\//i, + /^moz-extension:\/\//i, + // Other plugins + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, + + // AD fetch failed errors + /czilladx\.com/i, + /coinzilla\.com/i, + /coinzilla\.io/i, + /slise\.xyz/i, + ], + }; +})(); + +export function configureScope(scope: Sentry.Scope) { + if (!feature.isEnabled) { + return; + } + scope.setTag('app_instance', feature.instance); +} + +export function init() { + if (!config) { + return; + } + + Sentry.init(config); + Sentry.configureScope(configureScope); +} diff --git a/lib/setLocale.ts b/lib/setLocale.ts new file mode 100644 index 0000000000..e8a1db107a --- /dev/null +++ b/lib/setLocale.ts @@ -0,0 +1,6 @@ +const old = Number.prototype.toLocaleString; +Number.prototype.toLocaleString = function(locale, ...args) { + return old.call(this, 'en', ...args); +}; + +export {}; diff --git a/lib/settings/colorTheme.ts b/lib/settings/colorTheme.ts new file mode 100644 index 0000000000..c5b7edaa1f --- /dev/null +++ b/lib/settings/colorTheme.ts @@ -0,0 +1,42 @@ +import type { ColorMode } from '@chakra-ui/react'; + +import type { ColorThemeId } from 'types/settings'; + +interface ColorTheme { + id: ColorThemeId; + label: string; + colorMode: ColorMode; + hex: string; + sampleBg: string; +} + +export const COLOR_THEMES: Array = [ + { + id: 'light', + label: 'Light', + colorMode: 'light', + hex: '#FFFFFF', + sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)', + }, + { + id: 'dim', + label: 'Dim', + colorMode: 'dark', + hex: '#232B37', + sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)', + }, + { + id: 'midnight', + label: 'Midnight', + colorMode: 'dark', + hex: '#1B2E48', + sampleBg: 'linear-gradient(148deg, #1B3F71 50%, rgba(255, 255, 255, 0.00) 312.35%)', + }, + { + id: 'dark', + label: 'Dark', + colorMode: 'dark', + hex: '#101112', + sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)', + }, +]; diff --git a/lib/settings/identIcon.ts b/lib/settings/identIcon.ts new file mode 100644 index 0000000000..1159308576 --- /dev/null +++ b/lib/settings/identIcon.ts @@ -0,0 +1,24 @@ +import type { IdenticonType } from 'types/views/address'; + +export const IDENTICONS: Array<{ label: string; id: IdenticonType; sampleBg: string }> = [ + { + label: 'GitHub', + id: 'github', + sampleBg: 'url("/static/identicon_logos/github.png") center / contain no-repeat', + }, + { + label: 'Metamask jazzicon', + id: 'jazzicon', + sampleBg: 'url("/static/identicon_logos/jazzicon.png") center / contain no-repeat', + }, + { + label: 'Ethereum blockies', + id: 'blockie', + sampleBg: 'url("/static/identicon_logos/blockies.png") center / contain no-repeat', + }, + { + label: 'Gradient avatar', + id: 'gradient_avatar', + sampleBg: 'url("/static/identicon_logos/gradient_avatar.png") center / contain no-repeat', + }, +]; diff --git a/lib/shortenString.ts b/lib/shortenString.ts new file mode 100644 index 0000000000..4125ec06f2 --- /dev/null +++ b/lib/shortenString.ts @@ -0,0 +1,11 @@ +export default function shortenString(string: string | null, charNumber: number | undefined = 8) { + if (!string) { + return ''; + } + + if (string.length <= charNumber) { + return string; + } + + return string.slice(0, charNumber - 4) + '...' + string.slice(-4); +} diff --git a/lib/socket/context.tsx b/lib/socket/context.tsx new file mode 100644 index 0000000000..55c3620311 --- /dev/null +++ b/lib/socket/context.tsx @@ -0,0 +1,45 @@ +// https://hexdocs.pm/phoenix/js/ +import type { SocketConnectOption } from 'phoenix'; +import { Socket } from 'phoenix'; +import React, { useEffect, useState } from 'react'; + +export const SocketContext = React.createContext(null); + +interface SocketProviderProps { + children: React.ReactNode; + url?: string; + options?: Partial; +} + +export function SocketProvider({ children, options, url }: SocketProviderProps) { + const [ socket, setSocket ] = useState(null); + + useEffect(() => { + if (!url) { + return; + } + + const socketInstance = new Socket(url, options); + socketInstance.connect(); + setSocket(socketInstance); + + return () => { + socketInstance.disconnect(); + setSocket(null); + }; + }, [ options, url ]); + + return ( + + { children } + + ); +} + +export function useSocket() { + const context = React.useContext(SocketContext); + if (context === undefined) { + throw new Error('useSocket must be used within a SocketProvider'); + } + return context; +} diff --git a/lib/socket/types.ts b/lib/socket/types.ts new file mode 100644 index 0000000000..96ec44ca7e --- /dev/null +++ b/lib/socket/types.ts @@ -0,0 +1,78 @@ +import type { Channel } from 'phoenix'; + +import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address'; +import type { NewBlockSocketResponse } from 'types/api/block'; +import type { SmartContractVerificationResponse } from 'types/api/contract'; +import type { RawTracesResponse } from 'types/api/rawTrace'; +import type { TokenInstanceMetadataSocketMessage } from 'types/api/token'; +import type { TokenTransfer } from 'types/api/tokenTransfer'; +import type { Transaction } from 'types/api/transaction'; +import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2'; + +export type SocketMessageParams = SocketMessage.NewBlock | +SocketMessage.BlocksIndexStatus | +SocketMessage.InternalTxsIndexStatus | +SocketMessage.TxStatusUpdate | +SocketMessage.TxRawTrace | +SocketMessage.NewTx | +SocketMessage.NewPendingTx | +SocketMessage.NewDeposits | +SocketMessage.AddressBalance | +SocketMessage.AddressCurrentCoinBalance | +SocketMessage.AddressTokenBalance | +SocketMessage.AddressTokenBalancesErc20 | +SocketMessage.AddressTokenBalancesErc721 | +SocketMessage.AddressTokenBalancesErc1155 | +SocketMessage.AddressTokenBalancesErc404 | +SocketMessage.AddressCoinBalance | +SocketMessage.AddressTxs | +SocketMessage.AddressTxsPending | +SocketMessage.AddressTokenTransfer | +SocketMessage.AddressChangedBytecode | +SocketMessage.AddressFetchedBytecode | +SocketMessage.SmartContractWasVerified | +SocketMessage.TokenTransfers | +SocketMessage.TokenTotalSupply | +SocketMessage.TokenInstanceMetadataFetched | +SocketMessage.ContractVerification | +SocketMessage.NewZkEvmL2Batch | +SocketMessage.Unknown; + +interface SocketMessageParamsGeneric { + channel: Channel | undefined; + event: Event; + handler: (payload: Payload) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace SocketMessage { + export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; + export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>; + export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>; + export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; + export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>; + export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; + export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; + export type NewDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>; + export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>; + export type AddressCurrentCoinBalance = + SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; + export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; + export type AddressTokenBalancesErc20 = SocketMessageParamsGeneric<'updated_token_balances_erc_20', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc721 = SocketMessageParamsGeneric<'updated_token_balances_erc_721', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc1155 = SocketMessageParamsGeneric<'updated_token_balances_erc_1155', AddressTokensBalancesSocketMessage>; + export type AddressTokenBalancesErc404 = SocketMessageParamsGeneric<'updated_token_balances_erc_404', AddressTokensBalancesSocketMessage>; + export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>; + export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array }>; + export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array }>; + export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array }>; + export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record>; + export type AddressFetchedBytecode = SocketMessageParamsGeneric<'fetched_bytecode', { fetched_bytecode: string }>; + export type SmartContractWasVerified = SocketMessageParamsGeneric<'smart_contract_was_verified', Record>; + export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>; + export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>; + export type TokenInstanceMetadataFetched = SocketMessageParamsGeneric<'fetched_token_instance_metadata', TokenInstanceMetadataSocketMessage>; + export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>; + export type NewZkEvmL2Batch = SocketMessageParamsGeneric<'new_zkevm_confirmed_batch', NewZkEvmBatchSocketResponse>; + export type Unknown = SocketMessageParamsGeneric; +} diff --git a/lib/socket/useSocketChannel.tsx b/lib/socket/useSocketChannel.tsx new file mode 100644 index 0000000000..f0a67bc9b4 --- /dev/null +++ b/lib/socket/useSocketChannel.tsx @@ -0,0 +1,78 @@ +import type { Channel } from 'phoenix'; +import { useEffect, useRef, useState } from 'react'; + +import { useSocket } from './context'; + +const CHANNEL_REGISTRY: Record = {}; + +interface Params { + topic: string | undefined; + params?: object; + isDisabled: boolean; + onJoin?: (channel: Channel, message: unknown) => void; + onSocketClose?: () => void; + onSocketError?: () => void; +} + +export default function useSocketChannel({ topic, params, isDisabled, onJoin, onSocketClose, onSocketError }: Params) { + const socket = useSocket(); + const [ channel, setChannel ] = useState(); + const onCloseRef = useRef(); + const onErrorRef = useRef(); + + const onJoinRef = useRef(onJoin); + onJoinRef.current = onJoin; + + useEffect(() => { + const cleanUpRefs = () => { + const refs = [ onCloseRef.current, onErrorRef.current ].filter(Boolean); + refs.length > 0 && socket?.off(refs); + }; + + if (!isDisabled) { + onCloseRef.current = onSocketClose && socket?.onClose(onSocketClose); + onErrorRef.current = onSocketError && socket?.onError(onSocketError); + } else { + cleanUpRefs(); + } + + return cleanUpRefs; + }, [ onSocketClose, onSocketError, socket, isDisabled ]); + + useEffect(() => { + if (isDisabled && channel) { + channel.leave(); + setChannel(undefined); + } + }, [ channel, isDisabled ]); + + useEffect(() => { + if (socket === null || isDisabled || !topic) { + return; + } + + let ch: Channel; + if (CHANNEL_REGISTRY[topic]) { + ch = CHANNEL_REGISTRY[topic]; + onJoinRef.current?.(ch, ''); + } else { + ch = socket.channel(topic); + CHANNEL_REGISTRY[topic] = ch; + ch.join() + .receive('ok', (message) => onJoinRef.current?.(ch, message)) + .receive('error', () => { + onSocketError?.(); + }); + } + + setChannel(ch); + + return () => { + ch.leave(); + delete CHANNEL_REGISTRY[topic]; + setChannel(undefined); + }; + }, [ socket, topic, params, isDisabled, onSocketError ]); + + return channel; +} diff --git a/lib/socket/useSocketMessage.tsx b/lib/socket/useSocketMessage.tsx new file mode 100644 index 0000000000..fc3e2a96ba --- /dev/null +++ b/lib/socket/useSocketMessage.tsx @@ -0,0 +1,22 @@ +import { useEffect, useRef } from 'react'; + +import type { SocketMessageParams } from 'lib/socket/types'; + +export default function useSocketMessage({ channel, event, handler }: SocketMessageParams) { + const handlerRef = useRef(handler); + handlerRef.current = handler; + + useEffect(() => { + if (channel === undefined || event === undefined) { + return; + } + + const ref = channel.on(event, (message) => { + handlerRef.current?.(message); + }); + + return () => { + channel.off(event, ref); + }; + }, [ channel, event ]); +} diff --git a/lib/stripLeadingSlash.ts b/lib/stripLeadingSlash.ts new file mode 100644 index 0000000000..4eaae23512 --- /dev/null +++ b/lib/stripLeadingSlash.ts @@ -0,0 +1,3 @@ +const stripLeadingSlash = (str: string) => str[0] === '/' ? str.slice(1) : str; + +export default stripLeadingSlash; diff --git a/lib/stripTrailingSlash.ts b/lib/stripTrailingSlash.ts new file mode 100644 index 0000000000..f530a24b04 --- /dev/null +++ b/lib/stripTrailingSlash.ts @@ -0,0 +1,3 @@ +const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str; + +export default stripTrailingSlash; diff --git a/lib/token/metadata/attributesParser.ts b/lib/token/metadata/attributesParser.ts new file mode 100644 index 0000000000..9f1ce7d53c --- /dev/null +++ b/lib/token/metadata/attributesParser.ts @@ -0,0 +1,79 @@ +import _upperFirst from 'lodash/upperFirst'; + +import type { Metadata, MetadataAttributes } from 'types/client/token'; + +import dayjs from 'lib/date/dayjs'; + +function formatValue(value: string | number, display: string | undefined, trait: string | undefined): Pick { + // https://docs.opensea.io/docs/metadata-standards#attributes + switch (display) { + case 'boost_number': { + return { + value: `+${ value } boost`, + }; + } + case 'boost_percentage': { + return { + value: `${ value }% boost`, + }; + } + case 'date': { + return { + value: dayjs(Number(value) * 1000).format('YYYY-MM-DD'), + }; + } + default: { + try { + if (trait?.toLowerCase().includes('url') || value.toString().startsWith('http')) { + const url = new URL(String(value)); + return { + value: url.toString(), + value_type: 'URL', + }; + } + throw new Error(); + } catch (error) { + return { + value: String(value), + }; + } + } + } +} + +export default function attributesParser(attributes: Array): Metadata['attributes'] { + return attributes + .map((item) => { + if (typeof item !== 'object' || !item) { + return; + } + + const value = (() => { + if (!('value' in item)) { + return; + } + switch (typeof item.value) { + case 'string': + case 'number': + return item.value; + case 'boolean': + return String(item.value); + case 'object': + return JSON.stringify(item.value); + } + })(); + + const trait = 'trait_type' in item && typeof item.trait_type === 'string' ? item.trait_type : undefined; + const display = 'display_type' in item && typeof item.display_type === 'string' ? item.display_type : undefined; + + if (value === undefined) { + return; + } + + return { + ...formatValue(value, display, trait), + trait_type: _upperFirst(trait || 'property'), + }; + }) + .filter(Boolean); +} diff --git a/lib/token/metadata/urlParser.ts b/lib/token/metadata/urlParser.ts new file mode 100644 index 0000000000..7bbb31a650 --- /dev/null +++ b/lib/token/metadata/urlParser.ts @@ -0,0 +1,17 @@ +import * as regexp from 'lib/regexp'; + +export default function urlParser(maybeUrl: string): URL | undefined { + try { + return constructUrl(maybeUrl); + } catch (error) {} +} + +function constructUrl(maybeUrl: string) { + if (regexp.IPFS_PREFIX.test(maybeUrl)) { + return new URL(maybeUrl.replace(regexp.IPFS_PREFIX, 'https://ipfs.io/ipfs/')); + } + + if (regexp.URL_PREFIX.test(maybeUrl)) { + return new URL(maybeUrl); + } +} diff --git a/lib/token/parseMetadata.ts b/lib/token/parseMetadata.ts new file mode 100644 index 0000000000..8ecdb3dea4 --- /dev/null +++ b/lib/token/parseMetadata.ts @@ -0,0 +1,30 @@ +import type { TokenInstance } from 'types/api/token'; +import type { Metadata } from 'types/client/token'; + +import attributesParser from './metadata/attributesParser'; + +export default function parseMetadata(raw: TokenInstance['metadata'] | undefined): Metadata | undefined { + if (!raw) { + return; + } + + const parsed: Metadata = {}; + + if ('name' in raw && typeof raw.name === 'string') { + parsed.name = raw.name; + } + + if ('description' in raw && typeof raw.description === 'string') { + parsed.description = raw.description; + } + + if ('attributes' in raw && Array.isArray(raw.attributes)) { + parsed.attributes = attributesParser(raw.attributes); + } + + if (Object.keys(parsed).length === 0) { + return; + } + + return parsed; +} diff --git a/lib/token/tokenTypes.ts b/lib/token/tokenTypes.ts new file mode 100644 index 0000000000..a382e61340 --- /dev/null +++ b/lib/token/tokenTypes.ts @@ -0,0 +1,23 @@ +import type { NFTTokenType, TokenType } from 'types/api/token'; + +import config from 'configs/app'; + +const tokenStandardName = config.chain.tokenStandard; + +export const NFT_TOKEN_TYPES: Record = { + 'ERC-721': `${ tokenStandardName }-721`, + 'ERC-1155': `${ tokenStandardName }-1155`, + 'ERC-404': `${ tokenStandardName }-404`, +}; + +export const TOKEN_TYPES: Record = { + 'ERC-20': `${ tokenStandardName }-20`, + ...NFT_TOKEN_TYPES, +}; + +export const NFT_TOKEN_TYPE_IDS: Array = [ 'ERC-721', 'ERC-1155', 'ERC-404' ]; +export const TOKEN_TYPE_IDS: Array = [ 'ERC-20', ...NFT_TOKEN_TYPE_IDS ]; + +export function getTokenTypeName(typeId: TokenType) { + return TOKEN_TYPES[typeId]; +} diff --git a/lib/tx/getConfirmationDuration.ts b/lib/tx/getConfirmationDuration.ts new file mode 100644 index 0000000000..36d9a9afa9 --- /dev/null +++ b/lib/tx/getConfirmationDuration.ts @@ -0,0 +1,17 @@ +export default function getConfirmationString(durations: Array) { + if (durations.length === 0) { + return ''; + } + + const [ lower, upper ] = durations.map((time) => time / 1_000); + + if (!upper) { + return `Confirmed within ${ lower.toLocaleString() } secs`; + } + + if (lower === 0) { + return `Confirmed within <= ${ upper.toLocaleString() } secs`; + } + + return `Confirmed within ${ lower.toLocaleString() } - ${ upper.toLocaleString() } secs`; +} diff --git a/lib/units.ts b/lib/units.ts new file mode 100644 index 0000000000..546e4308ee --- /dev/null +++ b/lib/units.ts @@ -0,0 +1,11 @@ +import type { Unit } from 'types/unit'; + +import config from 'configs/app'; + +const weiName = config.chain.currency.weiName || 'wei'; + +export const currencyUnits: Record = { + wei: weiName, + gwei: `G${ weiName }`, + ether: config.chain.currency.symbol || 'ETH', +}; diff --git a/lib/validations/address.ts b/lib/validations/address.ts new file mode 100644 index 0000000000..cfca644c41 --- /dev/null +++ b/lib/validations/address.ts @@ -0,0 +1,5 @@ +// maybe it depends on the network?? + +export const ADDRESS_REGEXP = /^0x[a-fA-F\d]{40}$/; + +export const ADDRESS_LENGTH = 42; diff --git a/lib/validations/color.ts b/lib/validations/color.ts new file mode 100644 index 0000000000..1f31d02801 --- /dev/null +++ b/lib/validations/color.ts @@ -0,0 +1,17 @@ +export const COLOR_HEX_REGEXP = /^#[A-Fa-f\d]{3,6}$/; + +export const validator = (value: string | undefined) => { + if (!value || value.length === 0) { + return true; + } + + if (value.length !== 4 && value.length !== 7) { + return 'Invalid length'; + } + + if (!COLOR_HEX_REGEXP.test(value)) { + return 'Invalid hex code'; + } + + return true; +}; diff --git a/lib/validations/email.ts b/lib/validations/email.ts new file mode 100644 index 0000000000..98e0dc6d71 --- /dev/null +++ b/lib/validations/email.ts @@ -0,0 +1,3 @@ +export const EMAIL_REGEXP = /^[\w.%+-]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)+$/; + +export const validator = (value: string) => EMAIL_REGEXP.test(value) ? true : 'Invalid email'; diff --git a/lib/validations/signature.ts b/lib/validations/signature.ts new file mode 100644 index 0000000000..c2aebab741 --- /dev/null +++ b/lib/validations/signature.ts @@ -0,0 +1 @@ +export const SIGNATURE_REGEXP = /^0x[a-fA-F\d]{130}$/; diff --git a/lib/validations/transaction.ts b/lib/validations/transaction.ts new file mode 100644 index 0000000000..804699e554 --- /dev/null +++ b/lib/validations/transaction.ts @@ -0,0 +1,5 @@ +// maybe it depends on the network?? + +export const TRANSACTION_HASH_REGEXP = /^0x[a-fA-F\d]{64}$/; + +export const TRANSACTION_HASH_LENGTH = 66; diff --git a/lib/validations/url.ts b/lib/validations/url.ts new file mode 100644 index 0000000000..b4b30d4e05 --- /dev/null +++ b/lib/validations/url.ts @@ -0,0 +1,12 @@ +export const validator = (value: string | undefined) => { + if (!value) { + return true; + } + + try { + new URL(value); + return true; + } catch (error) { + return 'Incorrect URL'; + } +}; diff --git a/lib/web3/client.ts b/lib/web3/client.ts new file mode 100644 index 0000000000..8188f22869 --- /dev/null +++ b/lib/web3/client.ts @@ -0,0 +1,19 @@ +import { createPublicClient, http } from 'viem'; + +import currentChain from './currentChain'; + +export const publicClient = (() => { + if (currentChain.rpcUrls.default.http.filter(Boolean).length === 0) { + return; + } + + try { + return createPublicClient({ + chain: currentChain, + transport: http(), + batch: { + multicall: true, + }, + }); + } catch (error) {} +})(); diff --git a/lib/web3/currentChain.ts b/lib/web3/currentChain.ts new file mode 100644 index 0000000000..dd3892859f --- /dev/null +++ b/lib/web3/currentChain.ts @@ -0,0 +1,27 @@ +import { type Chain } from 'viem'; + +import config from 'configs/app'; + +const currentChain = { + id: Number(config.chain.id), + name: config.chain.name ?? '', + nativeCurrency: { + decimals: config.chain.currency.decimals, + name: config.chain.currency.name ?? '', + symbol: config.chain.currency.symbol ?? '', + }, + rpcUrls: { + 'default': { + http: [ config.chain.rpcUrl ?? '' ], + }, + }, + blockExplorers: { + 'default': { + name: 'Blockscout', + url: config.app.baseUrl, + }, + }, + testnet: config.chain.isTestnet, +} as const satisfies Chain; + +export default currentChain; diff --git a/lib/web3/useAccount.ts b/lib/web3/useAccount.ts new file mode 100644 index 0000000000..f3dfcd48c8 --- /dev/null +++ b/lib/web3/useAccount.ts @@ -0,0 +1,23 @@ +import type { UseAccountReturnType } from 'wagmi'; +import { useAccount } from 'wagmi'; + +import config from 'configs/app'; + +function useAccountFallback(): UseAccountReturnType { + return { + address: undefined, + addresses: undefined, + chain: undefined, + chainId: undefined, + connector: undefined, + isConnected: false, + isConnecting: false, + isDisconnected: true, + isReconnecting: false, + status: 'disconnected', + }; +} + +const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback; + +export default hook; diff --git a/lib/web3/useAddOrSwitchChain.tsx b/lib/web3/useAddOrSwitchChain.tsx new file mode 100644 index 0000000000..05811de6d7 --- /dev/null +++ b/lib/web3/useAddOrSwitchChain.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import config from 'configs/app'; +import getErrorObj from 'lib/errors/getErrorObj'; + +import useProvider from './useProvider'; + +export default function useAddOrSwitchChain() { + const { wallet, provider } = useProvider(); + + return React.useCallback(async() => { + if (!wallet || !provider) { + return; + } + + const hexadecimalChainId = '0x' + Number(config.chain.id).toString(16); + + try { + return await provider.request({ + method: 'wallet_switchEthereumChain', + params: [ { chainId: hexadecimalChainId } ], + }); + } catch (error) { + + const errorObj = getErrorObj(error); + const code = errorObj && 'code' in errorObj ? errorObj.code : undefined; + + // This error code indicates that the chain has not been added to Wallet. + if (code === 4902) { + const params = [ { + chainId: hexadecimalChainId, + chainName: config.chain.name, + nativeCurrency: { + name: config.chain.currency.name, + symbol: config.chain.currency.symbol, + decimals: config.chain.currency.decimals, + }, + rpcUrls: [ config.chain.rpcUrl ], + blockExplorerUrls: [ config.app.baseUrl ], + } ] as never; + // in wagmi types for wallet_addEthereumChain method is not provided + // eslint-disable-next-line @typescript-eslint/no-explicit-any + + return await provider.request({ + method: 'wallet_addEthereumChain', + params: params, + }); + } + + throw error; + } + }, [ provider, wallet ]); +} diff --git a/lib/web3/useProvider.tsx b/lib/web3/useProvider.tsx new file mode 100644 index 0000000000..43cc7aa8fa --- /dev/null +++ b/lib/web3/useProvider.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import type { WalletType } from 'types/client/wallets'; +import type { WalletProvider } from 'types/web3'; + +import config from 'configs/app'; + +const feature = config.features.web3Wallet; + +export default function useProvider() { + const [ provider, setProvider ] = React.useState(); + const [ wallet, setWallet ] = React.useState(); + + const initializeProvider = React.useMemo(() => async() => { + if (!feature.isEnabled) { + return; + } + + if (!('ethereum' in window && window.ethereum)) { + if (feature.wallets.includes('metamask') && window.navigator.userAgent.includes('Firefox')) { + const { WindowPostMessageStream } = (await import('@metamask/post-message-stream')); + const { initializeProvider } = (await import('@metamask/providers')); + + // workaround for MetaMask in Firefox + // Firefox blocks MetaMask injection script because of our CSP for 'script-src' + // so we have to inject it manually while the issue is not fixed + // https://github.com/MetaMask/metamask-extension/issues/3133#issuecomment-1025641185 + const metamaskStream = new WindowPostMessageStream({ + name: 'metamask-inpage', + target: 'metamask-contentscript', + }); + + // this will initialize the provider and set it as window.ethereum + initializeProvider({ + connectionStream: metamaskStream as never, + shouldShimWeb3: true, + }); + } else { + return; + } + } + + // have to check again in case provider was not set as window.ethereum in the previous step for MM in FF + // and also it makes typescript happy + if (!('ethereum' in window && window.ethereum)) { + return; + } + + // if user has multiple wallets installed, they all are injected in the window.ethereum.providers array + // if user has only one wallet, the provider is injected in the window.ethereum directly + const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ]; + + for (const wallet of feature.wallets) { + const provider = providers.find((provider) => { + return ( + // some wallets (e.g TokenPocket, Liquality, etc) try to look like MetaMask but they are not (not even close) + // so we have to check in addition the presence of the provider._events property + // found this hack in wagmi repo + // https://github.com/wagmi-dev/wagmi/blob/399b9eab8cfd698b49bfaa8456598dad9b597e0e/packages/connectors/src/types.ts#L65 + // for now it's the only way to distinguish them + (wallet === 'metamask' && provider.isMetaMask && Boolean(provider._events)) || + (wallet === 'coinbase' && provider.isCoinbaseWallet) || + (wallet === 'token_pocket' && provider.isTokenPocket) + ); + }); + + if (provider) { + setProvider(provider); + setWallet(wallet); + break; + } + } + }, []); + + React.useEffect(() => { + initializeProvider(); + }, [ initializeProvider ]); + + return { provider, wallet }; +} diff --git a/lib/web3/wagmiConfig.ts b/lib/web3/wagmiConfig.ts new file mode 100644 index 0000000000..22abb95a81 --- /dev/null +++ b/lib/web3/wagmiConfig.ts @@ -0,0 +1,46 @@ +import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; +import { http } from 'viem'; +import { createConfig, type CreateConfigParameters } from 'wagmi'; + +import config from 'configs/app'; +import currentChain from 'lib/web3/currentChain'; +const feature = config.features.blockchainInteraction; + +const wagmiConfig = (() => { + const chains: CreateConfigParameters['chains'] = [ currentChain ]; + + if (!feature.isEnabled) { + const wagmiConfig = createConfig({ + chains: [ currentChain ], + transports: { + [currentChain.id]: http(config.chain.rpcUrl || `${ config.api.endpoint }/api/eth-rpc`), + }, + ssr: true, + batch: { multicall: { wait: 100 } }, + }); + + return wagmiConfig; + } + + const wagmiConfig = defaultWagmiConfig({ + chains, + multiInjectedProviderDiscovery: true, + transports: { + [currentChain.id]: http(), + }, + projectId: feature.walletConnect.projectId, + metadata: { + name: `${ config.chain.name } explorer`, + description: `${ config.chain.name } explorer`, + url: config.app.baseUrl, + icons: [ config.UI.navigation.icon.default ].filter(Boolean), + }, + enableEmail: true, + ssr: true, + batch: { multicall: { wait: 100 } }, + }); + + return wagmiConfig; +})(); + +export default wagmiConfig; diff --git a/lib/web3/wallets.ts b/lib/web3/wallets.ts new file mode 100644 index 0000000000..6bcb257e67 --- /dev/null +++ b/lib/web3/wallets.ts @@ -0,0 +1,16 @@ +import type { WalletType, WalletInfo } from 'types/client/wallets'; + +export const WALLETS_INFO: Record, WalletInfo> = { + metamask: { + name: 'MetaMask', + icon: 'wallets/metamask', + }, + coinbase: { + name: 'Coinbase Wallet', + icon: 'wallets/coinbase', + }, + token_pocket: { + name: 'TokenPocket', + icon: 'wallets/token-pocket', + }, +}; diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000000..2f017d9cd5 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,43 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +import generateCspPolicy from 'nextjs/csp/generateCspPolicy'; +import * as middlewares from 'nextjs/middlewares/index'; + +const cspPolicy = generateCspPolicy(); + +export function middleware(req: NextRequest) { + const isPageRequest = req.headers.get('accept')?.includes('text/html'); + const start = Date.now(); + + if (!isPageRequest) { + return; + } + + const accountResponse = middlewares.account(req); + if (accountResponse) { + return accountResponse; + } + + const res = NextResponse.next(); + + middlewares.colorTheme(req, res); + + const end = Date.now(); + + res.headers.append('Content-Security-Policy', cspPolicy); + res.headers.append('Server-Timing', `middleware;dur=${ end - start }`); + res.headers.append('Docker-ID', process.env.HOSTNAME || ''); + + return res; +} + +/** + * Configure which routes should pass through the Middleware. + */ +export const config = { + matcher: [ '/', '/:notunderscore((?!_next).+)' ], + // matcher: [ + // '/((?!.*\\.|api\\/|node-api\\/).*)', // exclude all static + api + node-api routes + // ], +}; diff --git a/mocks/account/verifiedAddresses.ts b/mocks/account/verifiedAddresses.ts new file mode 100644 index 0000000000..ce9141119d --- /dev/null +++ b/mocks/account/verifiedAddresses.ts @@ -0,0 +1,164 @@ +import type { TokenInfoApplication, TokenInfoApplications, VerifiedAddress, VerifiedAddressResponse } from 'types/api/account'; +import type { AddressValidationResponseSuccess } from 'ui/addressVerification/types'; + +export const SIGNATURE = '0x96491e0cd1b99c14951552361b7f6ff64f41651b5d1c12501914342c8a6847e21e08726c3505e11ba2af9a40ac0b05c8d113e7fd1f74594224b9c7276ebb3a661b'; + +export const VERIFIED_ADDRESS: Record = { + NEW_ITEM: { + userId: '1', + chainId: '99', + contractAddress: '0xF822070D07067D1519490dBf49448a7E30EE9ea5', + verifiedDate: '2022-09-01', + metadata: { + tokenName: 'Test Token', + tokenSymbol: 'TT', + }, + }, + ITEM_1: { + userId: '1', + chainId: '99', + contractAddress: '0xd0e3010d1ecdbd17aae178b2bf36eb413d8a7441', + verifiedDate: '2022-08-01', + metadata: { + tokenName: 'My Token', + tokenSymbol: 'MYT', + }, + }, + ITEM_2: { + userId: '1', + chainId: '99', + contractAddress: '0xa8FCe579a11E551635b9c9CB915BEcd873C51254', + verifiedDate: '2022-09-23', + metadata: { + tokenName: 'Cat Token', + tokenSymbol: 'CATT', + }, + }, +}; + +export const ADDRESS_CHECK_RESPONSE = { + SUCCESS: { + status: 'SUCCESS', + result: { + // eslint-disable-next-line max-len + signingMessage: '[eth-goerli.blockscout.com] [2023-04-18 18:47:40] I, hereby verify that I am the owner/creator of the address [0xf822070d07067d1519490dbf49448a7e30ee9ea5]', + contractCreator: '0xd0e3010d1ecdbd17aae178b2bf36eb413d8a7441', + contractOwner: '0xa8FCe579a11E551635b9c9CB915BEcd873C51254', + }, + }, + SOURCE_CODE_NOT_VERIFIED_ERROR: { + status: 'SOURCE_CODE_NOT_VERIFIED_ERROR', + }, +}; + +export const ADDRESS_VERIFY_RESPONSE: Record = { + SUCCESS: { + status: 'SUCCESS', + result: { + verifiedAddress: VERIFIED_ADDRESS.NEW_ITEM, + }, + }, + INVALID_SIGNER_ERROR: { + status: 'INVALID_SIGNER_ERROR', + invalidSigner: { + signer: '0xF822070D07067D1519490dBf49448a7E30EE9ea5', + }, + }, +}; + +export const VERIFIED_ADDRESS_RESPONSE: Record = { + DEFAULT: { + verifiedAddresses: [ + VERIFIED_ADDRESS.ITEM_1, + VERIFIED_ADDRESS.ITEM_2, + ], + }, +}; + +export const TOKEN_INFO_APPLICATION_BASE = { + id: '1', + tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress, + status: 'APPROVED', + updatedAt: '2022-11-08 12:47:10.149148Z', + requesterName: 'Tom', + requesterEmail: 'tom@example.com', + projectName: 'My project', + projectWebsite: 'http://example.com', + projectEmail: 'token@example.com', + iconUrl: 'https://placekitten.com/100', + projectDescription: 'description', + projectSector: 'DeFi', + comment: '', + docs: 'https://example.com/docs', + github: 'https://github.com', + telegram: 'https://telegram.com', + linkedin: 'https://linkedin.com', + discord: 'https://discord.com', + slack: 'https://slack.com', + twitter: 'https://twitter.com', + openSea: 'https://opensea.com', + facebook: 'https://facebook.com', + medium: 'https://medium.com', + reddit: 'https://reddit.com', + support: 'support@example.com', + coinMarketCapTicker: 'https://coinmarketcap.com', + coinGeckoTicker: 'https://coingecko.com', + defiLlamaTicker: 'https://defillama.com', +}; + +export const TOKEN_INFO_APPLICATION: Record = { + APPROVED: { + ...TOKEN_INFO_APPLICATION_BASE, + tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress, + id: '1', + status: 'APPROVED', + updatedAt: '2022-11-08 12:47:10.149148Z', + }, + IN_PROCESS: { + ...TOKEN_INFO_APPLICATION_BASE, + tokenAddress: VERIFIED_ADDRESS.ITEM_2.contractAddress, + id: '2', + status: 'IN_PROCESS', + updatedAt: '2022-11-10 08:11:10.149148Z', + }, + UPDATED_ITEM: { + ...TOKEN_INFO_APPLICATION_BASE, + tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress, + id: '1', + status: 'IN_PROCESS', + updatedAt: '2022-11-11 05:11:10.149148Z', + }, +}; + +export const TOKEN_INFO_APPLICATIONS_RESPONSE: Record = { + DEFAULT: { + submissions: [ + TOKEN_INFO_APPLICATION.APPROVED, + TOKEN_INFO_APPLICATION.IN_PROCESS, + ], + }, + FOR_UPDATE: { + submissions: [ + { + ...TOKEN_INFO_APPLICATION.APPROVED, + status: 'UPDATE_REQUIRED', + }, + TOKEN_INFO_APPLICATION.IN_PROCESS, + ], + }, +}; + +export const TOKEN_INFO_FORM_CONFIG = { + projectSectors: [ + 'Infra & Dev tooling', + 'DeFi', + 'Data', + 'Bridge', + 'NFT', + 'Payments', + 'Faucet', + 'DAO', + 'Games', + 'Wallet', + ], +}; diff --git a/mocks/ad/textAd.ts b/mocks/ad/textAd.ts new file mode 100644 index 0000000000..0e5bb6feae --- /dev/null +++ b/mocks/ad/textAd.ts @@ -0,0 +1,9 @@ +export const duck = { + ad: { + name: 'Hello utia!', + description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!', + thumbnail: 'http://localhost:3100/utia.jpg', + url: 'https://test.url', + cta_button: 'Click me!', + }, +}; diff --git a/mocks/address/address.ts b/mocks/address/address.ts new file mode 100644 index 0000000000..e58097c2f5 --- /dev/null +++ b/mocks/address/address.ts @@ -0,0 +1,159 @@ +import type { Address } from 'types/api/address'; +import type { AddressParam } from 'types/api/addressParams'; + +import { publicTag, privateTag, watchlistName } from 'mocks/address/tag'; +import { tokenInfo } from 'mocks/tokens/tokenInfo'; + +export const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; + +export const withName: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: null, +}; + +export const withEns: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', +}; + +export const withNameTag: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', + metadata: { + reputation: null, + tags: [ + { tagType: 'name', name: 'Mrs. Duckie', slug: 'mrs-duckie', ordinal: 0, meta: null }, + ], + }, +}; + +export const withoutName: AddressParam = { + hash: hash, + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: null, +}; + +export const token: Address = { + hash: hash, + implementations: null, + is_contract: true, + is_verified: false, + name: null, + private_tags: [], + watchlist_names: [], + watchlist_address_id: null, + public_tags: [], + token: tokenInfo, + block_number_balance_updated_at: 8201413, + coin_balance: '1', + creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98', + creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72', + exchange_rate: '0.04311', + has_decompiled_code: false, + has_logs: false, + has_token_transfers: true, + has_tokens: true, + has_validated_blocks: false, + ens_domain_name: null, +}; + +export const eoa: Address = { + block_number_balance_updated_at: 30811263, + coin_balance: '2782650189688719421432220500', + creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', + creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', + exchange_rate: '0.04311', + has_decompiled_code: false, + has_logs: true, + has_token_transfers: false, + has_tokens: true, + has_validated_blocks: false, + hash: hash, + implementations: [], + is_contract: false, + is_verified: false, + name: null, + private_tags: [ publicTag ], + public_tags: [ privateTag ], + token: null, + watchlist_names: [ watchlistName ], + watchlist_address_id: 42, + ens_domain_name: null, +}; + +export const contract: Address = { + block_number_balance_updated_at: 30811263, + coin_balance: '27826501896887194214322205', + creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', + creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', + exchange_rate: '0.04311', + has_decompiled_code: false, + has_logs: true, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: false, + hash: hash, + implementations: [ + { address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' }, + ], + is_contract: true, + is_verified: true, + name: 'EternalStorageProxy', + private_tags: [ publicTag ], + public_tags: [ privateTag ], + token: null, + watchlist_names: [ watchlistName ], + watchlist_address_id: 42, + ens_domain_name: null, +}; + +export const validator: Address = { + block_number_balance_updated_at: 30811932, + coin_balance: '22910462800601256910890', + creation_tx_hash: null, + creator_address_hash: null, + exchange_rate: '0.00432018', + has_decompiled_code: false, + has_logs: false, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: true, + hash: hash, + implementations: [], + is_contract: false, + is_verified: false, + name: 'Kiryl Ihnatsyeu', + private_tags: [], + public_tags: [], + token: null, + watchlist_names: [], + watchlist_address_id: null, + ens_domain_name: null, +}; diff --git a/mocks/address/coinBalanceHistory.ts b/mocks/address/coinBalanceHistory.ts new file mode 100644 index 0000000000..cc78d75602 --- /dev/null +++ b/mocks/address/coinBalanceHistory.ts @@ -0,0 +1,70 @@ +import type { AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryChart } from 'types/api/address'; + +export const base: AddressCoinBalanceHistoryItem = { + block_number: 30367643, + block_timestamp: '2022-12-11T17:55:20Z', + delta: '-5568096000000000', + transaction_hash: null, + value: '107014805905725000000', +}; + +export const baseResponse: AddressCoinBalanceHistoryResponse = { + items: [ + { + block_number: 30367643, + block_timestamp: '2022-10-11T17:55:20Z', + delta: '-2105682233848856', + transaction_hash: null, + value: '10102109526582662088', + }, + { + block_number: 30367234, + block_timestamp: '2022-10-01T17:55:20Z', + delta: '1933020674364000', + transaction_hash: null, + value: '10143933697708939226', + }, + { + block_number: 30363402, + block_timestamp: '2022-09-03T17:55:20Z', + delta: '-1448410607186694', + transaction_hash: null, + value: '10142485287101752532', + }, + ], + next_page_params: null, +}; + +export const chartResponse: AddressCoinBalanceHistoryChart = { + items: [ + { + date: '2022-11-02', + value: '128238612887883515', + }, + { + date: '2022-11-03', + value: '199807583157570922', + }, + { + date: '2022-11-04', + value: '114487912907005778', + }, + { + date: '2022-11-05', + value: '219533112907005778', + }, + { + date: '2022-11-06', + value: '116487912907005778', + }, + { + date: '2022-11-07', + value: '199807583157570922', + }, + { + date: '2022-11-08', + value: '216488112907005778', + }, + ], + days: 10, +}; diff --git a/mocks/address/counters.ts b/mocks/address/counters.ts new file mode 100644 index 0000000000..9a676e3246 --- /dev/null +++ b/mocks/address/counters.ts @@ -0,0 +1,22 @@ +import type { AddressCounters } from 'types/api/address'; + +export const forContract: AddressCounters = { + gas_usage_count: '319340525', + token_transfers_count: '0', + transactions_count: '5462', + validations_count: '0', +}; + +export const forToken: AddressCounters = { + gas_usage_count: '247479698', + token_transfers_count: '1', + transactions_count: '8474', + validations_count: '0', +}; + +export const forValidator: AddressCounters = { + gas_usage_count: '91675762951', + token_transfers_count: '0', + transactions_count: '820802', + validations_count: '1726416', +}; diff --git a/mocks/address/implementations.ts b/mocks/address/implementations.ts new file mode 100644 index 0000000000..1d77032284 --- /dev/null +++ b/mocks/address/implementations.ts @@ -0,0 +1,11 @@ +export const multiple = [ + { address: '0xA84d24bD8ACE4d349C5f8c5DeeDd8bc071Ce5e2b', name: null }, + { address: '0xc9e91eDeA9DC16604022e4E5b437Df9c64EdB05A', name: 'Diamond' }, + { address: '0x2041832c62C0F89426b48B5868146C0b1fcd23E7', name: null }, + { address: '0x5f7DC6ECcF05594429671F83cc0e42EE18bC0974', name: 'VariablePriceFacet' }, + { address: '0x7abC92E242e88e4B0d6c5Beb4Df80e94D2c8A78c', name: null }, + { address: '0x84178a0c58A860eCCFB7E3aeA64a09543062A356', name: 'MultiSaleFacet' }, + { address: '0x33aD95537e63e9f09d96dE201e10715Ed40D9400', name: 'SVGTemplatesFacet' }, + { address: '0xfd86Aa7f902185a8Df9859c25E4BF52D3DaDd9FA', name: 'ERC721AReceiverFacet' }, + { address: '0x6945a35df18e59Ce09fec4B6cD3C4F9cFE6369de', name: null }, +]; diff --git a/mocks/address/tabCounters.ts b/mocks/address/tabCounters.ts new file mode 100644 index 0000000000..3853ffab4d --- /dev/null +++ b/mocks/address/tabCounters.ts @@ -0,0 +1,11 @@ +import type { AddressTabsCounters } from 'types/api/address'; + +export const base: AddressTabsCounters = { + internal_txs_count: 13, + logs_count: 51, + token_balances_count: 3, + token_transfers_count: 3, + transactions_count: 51, + validations_count: 42, + withdrawals_count: 11, +}; diff --git a/mocks/address/tag.ts b/mocks/address/tag.ts new file mode 100644 index 0000000000..bb1b066675 --- /dev/null +++ b/mocks/address/tag.ts @@ -0,0 +1,18 @@ +import type { AddressTag, WatchlistName } from 'types/api/addressParams'; + +export const privateTag: AddressTag = { + label: 'my-private-tag', + display_name: 'my private tag', + address_hash: '0x', +}; + +export const publicTag: AddressTag = { + label: 'some-public-tag', + display_name: 'some public tag', + address_hash: '0x', +}; + +export const watchlistName: WatchlistName = { + label: 'watchlist-name', + display_name: 'watchlist name', +}; diff --git a/mocks/address/tokens.ts b/mocks/address/tokens.ts new file mode 100644 index 0000000000..c494c9bbe3 --- /dev/null +++ b/mocks/address/tokens.ts @@ -0,0 +1,209 @@ +import type { AddressCollectionsResponse, AddressNFTsResponse, AddressTokenBalance, AddressTokensResponse } from 'types/api/address'; + +import * as tokens from 'mocks/tokens/tokenInfo'; +import * as tokenInstance from 'mocks/tokens/tokenInstance'; + +export const erc20a: AddressTokenBalance = { + token: tokens.tokenInfoERC20a, + token_id: null, + value: '1169321234567891234567891', + token_instance: null, +}; + +export const erc20b: AddressTokenBalance = { + token: tokens.tokenInfoERC20b, + token_id: null, + value: '872500000000', + token_instance: null, +}; + +export const erc20c: AddressTokenBalance = { + token: tokens.tokenInfoERC20c, + token_id: null, + value: '9852000000000000000000', + token_instance: null, +}; + +export const erc20d: AddressTokenBalance = { + token: tokens.tokenInfoERC20d, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + +export const erc20LongSymbol: AddressTokenBalance = { + token: tokens.tokenInfoERC20LongSymbol, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + +export const erc20BigAmount: AddressTokenBalance = { + token: { + ...tokens.tokenInfoERC20LongSymbol, + exchange_rate: '4200000000', + name: 'DuckDuckGoose Stable Coin', + }, + token_id: null, + value: '39000000000000000000', + token_instance: null, +}; + +export const erc721a: AddressTokenBalance = { + token: tokens.tokenInfoERC721a, + token_id: null, + value: '51', + token_instance: null, +}; + +export const erc721b: AddressTokenBalance = { + token: tokens.tokenInfoERC721b, + token_id: null, + value: '1', + token_instance: null, +}; + +export const erc721c: AddressTokenBalance = { + token: tokens.tokenInfoERC721c, + token_id: null, + value: '5', + token_instance: null, +}; + +export const erc721LongSymbol: AddressTokenBalance = { + token: tokens.tokenInfoERC721LongSymbol, + token_id: null, + value: '5', + token_instance: null, +}; + +export const erc1155a: AddressTokenBalance = { + token: tokens.tokenInfoERC1155a, + token_id: '42', + token_instance: tokenInstance.base, + value: '24', +}; + +export const erc1155b: AddressTokenBalance = { + token: tokens.tokenInfoERC1155b, + token_id: '100010000000001', + token_instance: tokenInstance.base, + value: '11', +}; + +export const erc1155withoutName: AddressTokenBalance = { + token: tokens.tokenInfoERC1155WithoutName, + token_id: '64532245', + token_instance: tokenInstance.base, + value: '42', +}; + +export const erc1155LongId: AddressTokenBalance = { + token: tokens.tokenInfoERC1155b, + token_id: '483200961027732618117991942553110860267520', + token_instance: tokenInstance.base, + value: '42', +}; + +export const erc404a: AddressTokenBalance = { + token: tokens.tokenInfoERC404, + token_id: '42', + token_instance: tokenInstance.base, + value: '240000000000000', +}; + +export const erc404b: AddressTokenBalance = { + token: tokens.tokenInfoERC404, + token_instance: null, + value: '11', + token_id: null, +}; + +export const erc20List: AddressTokensResponse = { + items: [ + erc20a, + erc20b, + erc20c, + ], + next_page_params: null, +}; + +export const erc721List: AddressTokensResponse = { + items: [ + erc721a, + erc721b, + erc721c, + ], + next_page_params: null, +}; + +export const erc1155List: AddressTokensResponse = { + items: [ + erc1155withoutName, + erc1155a, + erc1155b, + ], + next_page_params: null, +}; + +export const erc404List: AddressTokensResponse = { + items: [ + erc404a, + erc404b, + ], + next_page_params: null, +}; + +export const nfts: AddressNFTsResponse = { + items: [ + { + ...tokenInstance.base, + token: tokens.tokenInfoERC1155a, + token_type: 'ERC-1155', + value: '11', + }, + { + ...tokenInstance.unique, + token: tokens.tokenInfoERC721a, + token_type: 'ERC-721', + value: '1', + }, + { + ...tokenInstance.unique, + token: tokens.tokenInfoERC404, + token_type: 'ERC-404', + value: '11000', + }, + ], + next_page_params: null, +}; + +const nftInstance = { + ...tokenInstance.base, + token_type: 'ERC-1155', + value: '11', +}; + +export const collections: AddressCollectionsResponse = { + items: [ + { + token: tokens.tokenInfoERC1155a, + amount: '100', + token_instances: Array(5).fill(nftInstance), + }, + { + token: tokens.tokenInfoERC20LongSymbol, + amount: '100', + token_instances: Array(5).fill(nftInstance), + }, + { + token: tokens.tokenInfoERC1155WithoutName, + amount: '1', + token_instances: [ nftInstance ], + }, + ], + next_page_params: { + token_contract_address_hash: '123', + token_type: 'ERC-1155', + }, +}; diff --git a/mocks/apps/app.html b/mocks/apps/app.html new file mode 100644 index 0000000000..c7c675b977 --- /dev/null +++ b/mocks/apps/app.html @@ -0,0 +1,32 @@ + + + + + Mock HTML Content + + + +

Full view app

+ + diff --git a/mocks/apps/apps.ts b/mocks/apps/apps.ts new file mode 100644 index 0000000000..2f748c625a --- /dev/null +++ b/mocks/apps/apps.ts @@ -0,0 +1,29 @@ +/* eslint-disable max-len */ +export const apps = [ + { + author: 'Hop', + id: 'hop-exchange', + title: 'Hop', + logo: 'https://www.gitbook.com/cdn-cgi/image/width=288,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FfhJJGsR3RSfFmRoxfvqk%252FHop.png%3Falt%3Dmedia%26token%3D8107e45c-546c-4771-bbfe-e86bb0fe8c1a', + categories: [ 'Bridge' ], + shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + site: 'https://help.hop.exchange/hc/en-us/articles/4405172445197-What-is-Hop-Protocol-', + description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + external: true, + url: 'https://goerli.hop.exchange/send?token=ETH&sourceNetwork=ethereum', + github: [ 'https://github.com/hop-protocol/hop', 'https://github.com/hop-protocol/hop-ui' ], + discord: 'https://discord.gg/hopprotocol', + twitter: 'https://twitter.com/HopProtocol', + }, + { + author: 'Blockscout', + id: 'token-approval-tracker', + title: 'Token Approval Tracker', + logo: 'https://approval-tracker.apps.blockscout.com/icon-192.png', + categories: [ 'Infra & Dev tooling' ], + shortDescription: 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', + site: 'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', + description: 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', + url: 'https://approval-tracker.apps.blockscout.com/', + }, +]; diff --git a/mocks/apps/ratings.ts b/mocks/apps/ratings.ts new file mode 100644 index 0000000000..3eeca5b693 --- /dev/null +++ b/mocks/apps/ratings.ts @@ -0,0 +1,12 @@ +import { apps } from './apps'; + +export const ratings = { + records: [ + { + fields: { + appId: apps[0].id, + rating: 4.3, + }, + }, + ], +}; diff --git a/mocks/apps/securityReports.ts b/mocks/apps/securityReports.ts new file mode 100644 index 0000000000..33457ddf2f --- /dev/null +++ b/mocks/apps/securityReports.ts @@ -0,0 +1,60 @@ +import { apps } from './apps'; + +export const securityReports = [ + { + appName: apps[0].id, + doc: 'http://docs.li.fi/smart-contracts/deployments#mainnet', + chainsData: { + '1': { + overallInfo: { + verifiedNumber: 1, + totalContractsNumber: 1, + solidityScanContractsNumber: 1, + securityScore: 87.5, + issueSeverityDistribution: { + critical: 4, + gas: 1, + high: 0, + informational: 4, + low: 2, + medium: 0, + }, + }, + contractsData: [ + { + address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + isVerified: true, + solidityScanReport: { + connection_id: '', + contract_address: '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + contract_chain: 'optimism', + contract_platform: 'blockscout', + contract_url: 'http://optimism.blockscout.com/address/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + contractname: 'LiFiDiamond', + is_quick_scan: true, + node_reference_id: null, + request_type: 'threat_scan', + scanner_reference_url: 'http://solidityscan.com/quickscan/0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE/blockscout/eth?ref=blockscout', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 4, + low: 2, + medium: 0, + }, + lines_analyzed_count: 72, + scan_time_taken: 1, + score: '4.38', + score_v2: '87.50', + threat_score: '100.00', + }, + }, + }, + ], + }, + }, + }, +]; diff --git a/mocks/arbitrum/deposits.ts b/mocks/arbitrum/deposits.ts new file mode 100644 index 0000000000..8bc7f290e8 --- /dev/null +++ b/mocks/arbitrum/deposits.ts @@ -0,0 +1,29 @@ +import type { ArbitrumL2MessagesResponse } from 'types/api/arbitrumL2'; + +export const baseResponse: ArbitrumL2MessagesResponse = { + items: [ + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181920, + origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123456, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'initiated', + }, + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181921, + origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123400, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'relayed', + }, + ], + next_page_params: { + items_count: 50, + id: 123, + direction: 'to-rollup', + }, +}; diff --git a/mocks/arbitrum/txnBatch.ts b/mocks/arbitrum/txnBatch.ts new file mode 100644 index 0000000000..ec86fa2944 --- /dev/null +++ b/mocks/arbitrum/txnBatch.ts @@ -0,0 +1,11 @@ +import type { ArbitrumL2TxnBatch } from 'types/api/arbitrumL2'; + +import { finalized } from './txnBatches'; + +export const batchData: ArbitrumL2TxnBatch = { + ...finalized, + after_acc: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb', + before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc', + start_block: 1245209, + end_block: 1245490, +}; diff --git a/mocks/arbitrum/txnBatches.ts b/mocks/arbitrum/txnBatches.ts new file mode 100644 index 0000000000..5d61f7a4ec --- /dev/null +++ b/mocks/arbitrum/txnBatches.ts @@ -0,0 +1,36 @@ +import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatchesResponse } from 'types/api/arbitrumL2'; + +export const finalized: ArbitrumL2TxnBatchesItem = { + number: 12345, + blocks_count: 12345, + transactions_count: 10000, + commitment_transaction: { + block_number: 12345, + timestamp: '2022-04-17T08:51:58.000000Z', + hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + status: 'finalized', + }, +}; + +export const unfinalized: ArbitrumL2TxnBatchesItem = { + number: 12344, + blocks_count: 10000, + transactions_count: 103020, + commitment_transaction: { + block_number: 12340, + timestamp: '2022-04-17T08:51:58.000000Z', + hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + status: 'unfinalized', + }, +}; + +export const baseResponse: ArbitrumL2TxnBatchesResponse = { + items: [ + finalized, + unfinalized, + ], + next_page_params: { + items_count: 50, + number: 123, + }, +}; diff --git a/mocks/arbitrum/withdrawals.ts b/mocks/arbitrum/withdrawals.ts new file mode 100644 index 0000000000..a825e18de6 --- /dev/null +++ b/mocks/arbitrum/withdrawals.ts @@ -0,0 +1,29 @@ +import type { ArbitrumL2MessagesResponse } from 'types/api/arbitrumL2'; + +export const baseResponse: ArbitrumL2MessagesResponse = { + items: [ + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181920, + origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123456, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'sent', + }, + { + completion_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + id: 181921, + origination_address: '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a', + origination_transaction_block_number: 123400, + origination_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'confirmed', + }, + ], + next_page_params: { + items_count: 50, + id: 123, + direction: 'from-rollup', + }, +}; diff --git a/mocks/blobs/blobs.ts b/mocks/blobs/blobs.ts new file mode 100644 index 0000000000..24d25b465f --- /dev/null +++ b/mocks/blobs/blobs.ts @@ -0,0 +1,36 @@ +import type { Blob, TxBlobs } from 'types/api/blobs'; + +export const base1: Blob = { + blob_data: '0x004242004242004242004242004242004242', + hash: '0x016316f61a259aa607096440fc3eeb90356e079be01975d2fb18347bd50df33c', + kzg_commitment: '0xa95caabd009e189b9f205e0328ff847ad886e4f8e719bd7219875fbb9688fb3fbe7704bb1dfa7e2993a3dea8d0cf767d', + kzg_proof: '0x89cf91c4c8be6f2a390d4262425f79dffb74c174fb15a210182184543bf7394e5a7970a774ee8e0dabc315424c22df0f', + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x970d8c45c713a50a1fa351b00ca29a8890cac474c59cc8eee4eddec91a1729f0' }, + ], +}; + +export const base2: Blob = { + blob_data: '0x89504E470D0A1A0A0000000D494844520000003C0000003C0403', + hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd1', + kzg_commitment: '0x89b0d8ac715ee134135471994a161ef068a784f51982fcd7161aa8e3e818eb83017ccfbfc30c89b796a2743d77554e2f', + kzg_proof: '0x8255a6c6a236483814b8e68992e70f3523f546866a9fed6b8e0ecfef314c65634113b8aa02d6c5c6e91b46e140f17a07', + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' }, + ], +}; + +export const withoutData: Blob = { + blob_data: null, + hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd3', + kzg_commitment: null, + kzg_proof: null, + transaction_hashes: [ + { block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' }, + ], +}; + +export const txBlobs: TxBlobs = { + items: [ base1, base2, withoutData ], + next_page_params: null, +}; diff --git a/mocks/blocks/block.ts b/mocks/blocks/block.ts new file mode 100644 index 0000000000..56b3b8339e --- /dev/null +++ b/mocks/blocks/block.ts @@ -0,0 +1,264 @@ +/* eslint-disable max-len */ +import type { RpcBlock } from 'viem'; + +import type { Block, BlocksResponse } from 'types/api/block'; + +export const base: Block = { + base_fee_per_gas: '10000000000', + burnt_fees: '5449200000000000', + burnt_fees_percentage: 20.292245650793845, + difficulty: '340282366920938463463374607431768211454', + extra_data: 'TODO', + gas_limit: '12500000', + gas_target_percentage: -91.28128, + gas_used: '544920', + gas_used_percentage: 4.35936, + hash: '0xccc75136de485434d578b73df66537c06b34c3c9b12d085daf95890c914fc2bc', + height: 30146364, + miner: { + hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41', + implementations: null, + is_contract: false, + is_verified: null, + name: 'Alex Emelyanov', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + nonce: '0x0000000000000000', + parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f', + priority_fee: '23211757500000000', + rewards: [ + { + reward: '500000000000000000', + type: 'POA Mania Reward', + }, + { + reward: '1026853607510000000', + type: 'Validator Reward', + }, + { + reward: '500000000000000000', + type: 'Emission Reward', + }, + ], + size: 2448, + state_root: 'TODO', + timestamp: '2022-11-11T11:59:35Z', + total_difficulty: '10258276095980170141167591583995189665817672619', + tx_count: 5, + tx_fees: '26853607500000000', + type: 'block', + uncles_hashes: [], +}; + +export const genesis: Block = { + base_fee_per_gas: null, + burnt_fees: null, + burnt_fees_percentage: null, + difficulty: '131072', + extra_data: 'TODO', + gas_limit: '6700000', + gas_target_percentage: -100, + gas_used: '0', + gas_used_percentage: 0, + hash: '0x39f02c003dde5b073b3f6e1700fc0b84b4877f6839bb23edadd3d2d82a488634', + height: 0, + miner: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + nonce: '0x0000000000000000', + parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000', + priority_fee: null, + rewards: [], + size: 533, + state_root: 'TODO', + timestamp: '2017-12-16T00:13:24.000000Z', + total_difficulty: '131072', + tx_count: 0, + tx_fees: '0', + type: 'block', + uncles_hashes: [], +}; + +export const base2: Block = { + ...base, + height: base.height - 1, + size: 592, + miner: { + hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165', + implementations: null, + is_contract: false, + is_verified: null, + name: 'Kiryl Ihnatsyeu', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + timestamp: '2022-11-11T11:46:05Z', + tx_count: 253, + gas_target_percentage: 23.6433, + gas_used: '6333342', + gas_used_percentage: 87.859504, + burnt_fees: '232438000000000000', + burnt_fees_percentage: 65.3333333333334, + rewards: [ + { + reward: '500000000000000000', + type: 'Chore Reward', + }, + { + reward: '1017432850000000000', + type: 'Miner Reward', + }, + { + reward: '500000000000000000', + type: 'Emission Reward', + }, + ], +}; + +export const rootstock: Block = { + ...base, + bitcoin_merged_mining_coinbase_transaction: '0x0000000000000080a1219cea298d65d545b56abafe7c5421edfaf084cf9e374bb23ea985ebd86b206088ac0000000000000000266a24aa21a9edb2ac3022ad2a5327449f029b6aa3d2e55605061b5d8171b30abf5b330d1959c900000000000000002a6a52534b424c4f434b3a481d071e57c6c47cb8eb716295a7079b15859962abf35e32f107b21f003f0bb900000000', + bitcoin_merged_mining_header: '0x000000204a7e42cadf8b5b0a094755c5a13298e596d61f361c6d31171a00000000000000970e51977cd6f82bab9ed62e678c8d8ca664af9d5c3b5cea39d5d4337c7abedae334c9649fc63e1982a84aaa', + bitcoin_merged_mining_merkle_proof: '0x09f386e5e6feb20706a1b5d0817eae96f0ebb0d713eeefe6d5625afc6fd87fcdfe8cc9118bb49e32db87f8e928dcb13dd327b526ced76fb9de0115a5dca8d2a9657c929360ad07418fc7e1a3120da27e0002470d0c98c9b8b5b2835e64e379421d2469204533307bf0c5a087d93fd1dfb3aaea3ee83099928860f6cca891cf59d73c4e3c6053ea4b385dce39067e87c28805ddd89c4ff10500401bec7c248f749ad6f0933e6ad270e447d01711aca1cc26d7989ee59e1431fd2fd5d058edca6d', + hash_for_merged_mining: '0x481d071e57c6c47cb8eb716295a7079b15859962abf35e32f107b21f003f0bb9', + minimum_gas_price: '59240000', +}; + +export const withBlobTxs: Block = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '393216', + burnt_blob_fees: '8461393325064192', + excess_blob_gas: '79429632', + blob_tx_count: 1, +}; + +export const withWithdrawals: Block = { + ...base, + withdrawals_count: 2, +}; + +export const baseListResponse: BlocksResponse = { + items: [ + base, + base2, + ], + next_page_params: null, +}; + +export const rpcBlockBase: RpcBlock = { + difficulty: '0x37fcc04bef8', + extraData: '0x476574682f76312e302e312d38326566323666362f6c696e75782f676f312e34', + gasLimit: '0x2fefd8', + gasUsed: '0x0', + hash: '0xfbafb4b7b6f6789338d15ff046f40dc608a42b1a33b093e109c6d7a36cd76f61', + logsBloom: '0x0', + miner: '0xe6a7a1d47ff21b6321162aea7c6cb457d5476bca', + mixHash: '0x038956b9df89d0c1f980fd656d045e912beafa515cff7d7fd3c5f34ffdcb9e4b', + nonce: '0xd8d3392f340bbb22', + number: '0x1869f', + parentHash: '0x576fd45e598c9f86835f50fe2c6e6d11df2d4c4b01f19e4241b7e793d852f9e4', + receiptsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x225', + stateRoot: '0x32356228651d64cc5e6e7be87a556ecdbf40e876251dc867ba9e4bb82a0124a3', + timestamp: '0x55d19741', + totalDifficulty: '0x259e89748daae17', + transactions: [ + '0x0e70849f10e22fe2e53fe6755f86a572aa6bb2fc472f0b87d9e561efa1fc2e1f', + '0xae5624c77f06d0164301380afa7780ebe49debe77eb3d5167004d69bd188a09f', + ], + transactionsRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + uncles: [], + baseFeePerGas: null, + blobGasUsed: `0x0`, + excessBlobGas: `0x0`, + sealFields: [], + withdrawals: [ + { address: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amount: '0x12128cd', index: '0x3216bbb', validatorIndex: '0x4dca3' }, + { address: '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f', amount: '0x12027dd', index: '0x3216bbc', validatorIndex: '0x4dca4' }, + ], +}; + +export const rpcBlockWithTxsInfo: RpcBlock = { + ...rpcBlockBase, + transactions: [ + { + accessList: [ + { + address: '0x7af661a6463993e05a171f45d774cf37e761c83f', + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000007', + '0x000000000000000000000000000000000000000000000000000000000000000c', + '0x0000000000000000000000000000000000000000000000000000000000000008', + '0x0000000000000000000000000000000000000000000000000000000000000006', + '0x0000000000000000000000000000000000000000000000000000000000000009', + '0x000000000000000000000000000000000000000000000000000000000000000a', + ], + }, + { + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + storageKeys: [ + '0x0d726f311404f8052d44e7004a6ffb747709a6d3666a62ce3f5aad13374680ab', + '0x1a824a6850dcbd9223afea4418727593881e2911ed2e734272a263153159fe26', + '0xfae3a383c82daf853bbd8bbcd21280410599b135c274c01354ea7d3a5e09f43c', + ], + }, + ], + blockHash: '0xeb37ebc94e31773e5c5703073fd3911b2ab596f099d00d18b55ae3ac8203c1d5', + blockNumber: '0x136058d', + chainId: '0x1', + from: '0x111527f1386c6725a2f5986230f3060bdcac041f', + gas: '0xf4240', + gasPrice: '0x1780b2ff9', + hash: '0x0e70849f10e22fe2e53fe6755f86a572aa6bb2fc472f0b87d9e561efa1fc2e1f', + input: '0x258d7af661a6463993e05a171f45d774cf37e761c83f402ab3277301b3574863a151d042dc870fb1b3f0c72cbbdd53a85898f62415fe124406f6608d8802269d1283cdb2a5a329649e5cb4cdcee91ab6', + // maxFeePerGas: '0x3ac1bf7ee', + // maxPriorityFeePerGas: '0x0', + nonce: '0x127b2', + r: '0x3c47223f880a3fb7b1eca368d9d7320d2278f0b679109d9ed0af4080ee386f23', + s: '0x587a441f9472b312ff302d7132547aa250ea06c6203c76831d56a46ec188e664', + to: '0x000000d40b595b94918a28b27d1e2c66f43a51d3', + transactionIndex: '0x0', + type: '0x1', + v: '0x1', + value: '0x31', + yParity: '0x1', + }, + { + accessList: [], + blockHash: '0xeb37ebc94e31773e5c5703073fd3911b2ab596f099d00d18b55ae3ac8203c1d5', + blockNumber: '0x136058d', + chainId: '0x1', + from: '0xe25d2cb47b606bb6fd9272125457a7230e26f956', + gas: '0x47bb0', + gasPrice: '0x1ba875cb6', + hash: '0xae5624c77f06d0164301380afa7780ebe49debe77eb3d5167004d69bd188a09f', + input: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006696237b00000000000000000000000000000000000000000000000000000000000000040b080604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000b1a2bc2ec500000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000000006d1aaedfab0f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d422990000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d42299000000000000000000000000000000fee13a103a10d593b9ae06b3e05f2e7e1c00000000000000000000000000000000000000000000000000000000000000190000000000000000000000000000000000000000000000000000000000000060000000000000000000000000d84d4e8e1e8f268e027c29fa4d48c4b7e4d42299000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000006cd4db3c8c8d', + // maxFeePerGas: '0x23493c9cd', + // maxPriorityFeePerGas: '0x427c2cbd', + nonce: '0x32b', + r: '0x6566181b3cfd01702b24a2124ea7698b8cc815c7f37d1ea55800f176ca7a94cf', + s: '0x34f8dd837f37746ccd18f4fa71e05de98a2212f1c931f740598e491518616bb3', + to: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + transactionIndex: '0x1', + type: '0x1', + v: '0x1', + value: '0xb1a2bc2ec50000', + yParity: '0x1', + }, + ], +}; diff --git a/mocks/config/footerLinks.ts b/mocks/config/footerLinks.ts new file mode 100644 index 0000000000..5ec3bdf9bf --- /dev/null +++ b/mocks/config/footerLinks.ts @@ -0,0 +1,93 @@ +import type { CustomLinksGroup } from 'types/footerLinks'; + +export const FOOTER_LINKS: Array = [ + { + title: 'Company', + links: [ + { + text: 'Advertise', + url: 'https://coinzilla.com/', + }, + { + text: 'Staking', + url: '', + }, + { + text: 'Contact us', + url: '', + }, + { + text: 'Brand assets', + url: '', + }, + { + text: 'Term of service', + url: '', + }, + ], + }, + { + title: 'Community', + links: [ + { + text: 'API docs', + url: '', + }, + { + text: 'Knowledge base', + url: '', + }, + { + text: 'Network status', + url: '', + }, + { + text: 'Learn Alphabet', + url: '', + }, + ], + }, + { + title: 'Product', + links: [ + { + text: 'Stake Alphabet', + url: '', + }, + { + text: 'Build token', + url: '', + }, + { + text: 'Build DAPPS', + url: '', + }, + { + text: 'NFT marketplace', + url: '', + }, + { + text: 'Become validator', + url: '', + }, + + ], + }, + { + title: 'Partners', + links: [ + { + text: 'MetaDock', + url: 'https://blocksec.com/metadock', + }, + { + text: 'Sourcify', + url: 'https://sourcify.dev/', + }, + { + text: 'DRPC', + url: 'https://drpc.org?ref=559183', + }, + ], + }, +]; diff --git a/mocks/config/network.ts b/mocks/config/network.ts new file mode 100644 index 0000000000..a1c9075425 --- /dev/null +++ b/mocks/config/network.ts @@ -0,0 +1,17 @@ +import type { FeaturedNetwork } from 'types/networks'; + +const FEATURED_NETWORKS: Array = [ + { title: 'Gnosis Chain', url: 'https://blockscout.com/xdai/mainnet', group: 'Mainnets', isActive: true }, + { title: 'Arbitrum on xDai', url: 'https://blockscout.com/xdai/aox', group: 'Mainnets' }, + { title: 'Ethereum', url: 'https://blockscout.com/eth/mainnet', group: 'Mainnets' }, + { title: 'Ethereum Classic', url: 'https://blockscout.com/etx/mainnet', group: 'Mainnets', icon: 'https://localhost:3000/my-logo.png' }, + { title: 'POA', url: 'https://blockscout.com/poa/core', group: 'Mainnets' }, + { title: 'RSK', url: 'https://blockscout.com/rsk/mainnet', group: 'Mainnets' }, + { title: 'Gnosis Chain Testnet', url: 'https://blockscout.com/xdai/testnet', group: 'Testnets' }, + { title: 'POA Sokol', url: 'https://blockscout.com/poa/sokol', group: 'Testnets' }, + { title: 'ARTIS Σ1', url: 'https://blockscout.com/artis/sigma1', group: 'Other' }, + { title: 'LUKSO L14', url: 'https://blockscout.com/lukso/l14', group: 'Other' }, + { title: 'Astar', url: 'https://blockscout.com/astar', group: 'Other' }, +]; + +export const FEATURED_NETWORKS_MOCK = JSON.stringify(FEATURED_NETWORKS); diff --git a/mocks/contract/audits.ts b/mocks/contract/audits.ts new file mode 100644 index 0000000000..a2a6644229 --- /dev/null +++ b/mocks/contract/audits.ts @@ -0,0 +1,16 @@ +import type { SmartContractSecurityAudits } from 'types/api/contract'; + +export const contractAudits: SmartContractSecurityAudits = { + items: [ + { + audit_company_name: 'OpenZeppelin', + audit_publish_date: '2023-03-01', + audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit', + }, + { + audit_company_name: 'OpenZeppelin', + audit_publish_date: '2023-03-01', + audit_report_url: 'https://blog.openzeppelin.com/eip-4337-ethereum-account-abstraction-incremental-audit', + }, + ], +}; diff --git a/mocks/contract/info.ts b/mocks/contract/info.ts new file mode 100644 index 0000000000..ae11557f84 --- /dev/null +++ b/mocks/contract/info.ts @@ -0,0 +1,132 @@ +/* eslint-disable max-len */ +import type { SmartContract } from 'types/api/contract'; + +export const verified: SmartContract = { + abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ], + can_be_visualized_via_sol2uml: true, + compiler_version: 'v0.5.16+commit.9c3226ce', + constructor_args: 'constructor_args', + creation_bytecode: 'creation_bytecode', + deployed_bytecode: 'deployed_bytecode', + compiler_settings: { + evmVersion: 'london', + remappings: [ + '@openzeppelin/=node_modules/@openzeppelin/', + ], + }, + evm_version: 'default', + is_verified: true, + is_blueprint: false, + name: 'WPOA', + optimization_enabled: true, + optimization_runs: 1500, + source_code: 'source_code', + verified_at: '2021-08-03T10:40:41.679421Z', + decoded_constructor_args: [ + [ '0xc59615da2da226613b1c78f0c6676cac497910bc', { internalType: 'address', name: '_token', type: 'address' } ], + [ '1800', { internalType: 'uint256', name: '_duration', type: 'uint256' } ], + [ '900000000', { internalType: 'uint256', name: '_totalSupply', type: 'uint256' } ], + ], + external_libraries: [ + { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' }, + { address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' }, + ], + language: 'solidity', + license_type: 'gnu_gpl_v3', + is_self_destructed: false, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + file_path: '', + additional_sources: [], + verified_twin_address_hash: null, + minimal_proxy_address_hash: null, +}; + +export const certified: SmartContract = { + ...verified, + certified: true, +}; + +export const withMultiplePaths: SmartContract = { + ...verified, + file_path: './simple_storage.sol', + additional_sources: [ + { + file_path: '/contracts/protocol/libraries/logic/GenericLogic.sol', + source_code: '// SPDX-License-Identifier: GPL-3.0 \n pragma solidity >=0.7.0 <0.9.0; \n contract Storage {\n //2112313123; \nuint256 number; \n function store(uint256 num) public {\nnumber = num;\n}\n function retrieve() public view returns (uint256)\n {\nreturn number;\n}\n}', + }, + ], +}; + +export const verifiedViaSourcify: SmartContract = { + ...verified, + is_verified_via_sourcify: true, + is_fully_verified: false, + is_partially_verified: true, + sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/', +}; + +export const verifiedViaEthBytecodeDb: SmartContract = { + ...verified, + is_verified_via_eth_bytecode_db: true, +}; + +export const withTwinAddress: SmartContract = { + ...verified, + is_verified: false, + verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', +}; + +export const withProxyAddress: SmartContract = { + ...verified, + is_verified: false, + verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', + minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', +}; + +export const selfDestructed: SmartContract = { + ...verified, + is_self_destructed: true, +}; + +export const withChangedByteCode: SmartContract = { + ...verified, + is_changed_bytecode: true, + is_blueprint: true, +}; + +export const nonVerified: SmartContract = { + is_verified: false, + is_blueprint: false, + creation_bytecode: 'creation_bytecode', + deployed_bytecode: 'deployed_bytecode', + is_self_destructed: false, + abi: null, + compiler_version: null, + evm_version: null, + optimization_enabled: null, + optimization_runs: null, + name: null, + verified_at: null, + is_verified_via_eth_bytecode_db: null, + is_changed_bytecode: null, + is_verified_via_sourcify: null, + is_fully_verified: null, + is_partially_verified: null, + sourcify_repo_url: null, + source_code: null, + constructor_args: null, + decoded_constructor_args: null, + can_be_visualized_via_sol2uml: null, + file_path: '', + additional_sources: [], + external_libraries: null, + verified_twin_address_hash: null, + minimal_proxy_address_hash: null, + language: null, + license_type: null, +}; diff --git a/mocks/contract/methods.ts b/mocks/contract/methods.ts new file mode 100644 index 0000000000..5fa714921c --- /dev/null +++ b/mocks/contract/methods.ts @@ -0,0 +1,134 @@ +import type { SmartContractMethodRead, SmartContractMethodWrite } from 'ui/address/contract/methods/types'; + +export const read: Array = [ + { + constant: true, + inputs: [ + { internalType: 'address', name: 'wallet', type: 'address' }, + ], + method_id: '70a08231', + name: 'FLASHLOAN_PREMIUM_TOTAL', + outputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + method_id: '06fdde03', + name: 'name', + outputs: [ + { internalType: 'string', name: '', type: 'string' }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, +]; + +export const write: Array = [ + { + payable: true, + stateMutability: 'payable', + type: 'fallback', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'guy', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'setReserveInterestRateStrategyAddress', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x01', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'src', type: 'address' }, + { internalType: 'address', name: 'dst', type: 'address' }, + ], + name: 'transferFrom', + outputs: [ + { internalType: 'bool', name: '', type: 'bool' }, + ], + payable: true, + stateMutability: 'payable', + type: 'function', + method_id: '0x02', + }, + { + stateMutability: 'payable', + type: 'receive', + }, + { + constant: false, + inputs: [], + name: 'pause', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x03', + }, + { + constant: false, + inputs: [ + { name: '_from', type: 'address' }, + { name: '_to', type: 'address' }, + { name: '_tokenId', type: 'uint256' }, + { name: '_data', type: 'bytes' }, + ], + name: 'safeTransferFrom', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x04', + }, + { + constant: false, + inputs: [ + { name: '_tokenId', type: 'uint256' }, + { name: '_hash', type: 'bytes32' }, + { name: '_keepRequestToken', type: 'bool' }, + { name: '_newOwner', type: 'address' }, + { name: '_signature', type: 'bytes' }, + ], + name: 'requestToken', + outputs: [ + { name: 'reward', type: 'uint256' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + method_id: '0x05', + }, + { + constant: false, + inputs: [ + { name: '_tokenId', type: 'uint256' }, + { name: '_imprint', type: 'bytes32' }, + { name: '_uri', type: 'string' }, + { name: '_initialKey', type: 'address' }, + { name: '_tokenRecoveryTimestamp', type: 'uint256' }, + { name: '_initialKeyIsRequestKey', type: 'bool' }, + ], + name: 'hydrateToken', + outputs: [ + { name: '', type: 'uint256' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + is_invalid: true, + }, +]; diff --git a/mocks/contract/solidityscanReport.ts b/mocks/contract/solidityscanReport.ts new file mode 100644 index 0000000000..41cbc785f2 --- /dev/null +++ b/mocks/contract/solidityscanReport.ts @@ -0,0 +1,70 @@ +import type { SolidityscanReport } from 'types/api/contract'; + +export const solidityscanReportAverage: SolidityscanReport = { + scan_report: { + contractname: 'foo', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 0, + low: 2, + medium: 0, + }, + lines_analyzed_count: 18, + scan_time_taken: 1, + score: '3.61', + score_v2: '72.22', + threat_score: '94.74', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; + +export const solidityscanReportGreat: SolidityscanReport = { + scan_report: { + contractname: 'foo', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 0, + high: 0, + informational: 0, + low: 0, + medium: 0, + }, + lines_analyzed_count: 18, + scan_time_taken: 1, + score: '3.61', + score_v2: '100', + threat_score: '94.74', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; + +export const solidityscanReportLow: SolidityscanReport = { + scan_report: { + contractname: 'foo', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 2, + gas: 1, + high: 3, + informational: 0, + low: 2, + medium: 10, + }, + lines_analyzed_count: 18, + scan_time_taken: 1, + score: '3.61', + score_v2: '22.22', + threat_score: '94.74', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; diff --git a/mocks/contracts/counters.ts b/mocks/contracts/counters.ts new file mode 100644 index 0000000000..fb33262271 --- /dev/null +++ b/mocks/contracts/counters.ts @@ -0,0 +1,8 @@ +import type { VerifiedContractsCounters } from 'types/api/contracts'; + +export const verifiedContractsCountersMock: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '0', +}; diff --git a/mocks/contracts/index.ts b/mocks/contracts/index.ts new file mode 100644 index 0000000000..86a0a0855c --- /dev/null +++ b/mocks/contracts/index.ts @@ -0,0 +1,59 @@ +import type { VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts'; + +export const contract1: VerifiedContract = { + address: { + hash: '0xef490030ac0d53B70E304b6Bc5bF657dc6780bEB', + implementations: null, + is_contract: true, + is_verified: null, + name: 'MockERC20', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + coin_balance: '2346534676900000008', + compiler_version: 'v0.8.17+commit.8df45f5f', + has_constructor_args: false, + language: 'solidity', + market_cap: null, + optimization_enabled: false, + tx_count: 7334224, + verified_at: '2022-09-16T18:49:29.605179Z', + license_type: 'mit', +}; + +export const contract2: VerifiedContract = { + address: { + hash: '0xB2218bdEbe8e90f80D04286772B0968ead666942', + implementations: null, + is_contract: true, + is_verified: null, + name: 'EternalStorageProxyWithSomeExternalLibrariesAndEvenMore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + certified: true, + coin_balance: '9078234570352343999', + compiler_version: 'v0.3.1+commit.0463ea4c', + has_constructor_args: true, + language: 'vyper', + market_cap: null, + optimization_enabled: true, + tx_count: 440, + verified_at: '2021-09-07T20:01:56.076979Z', + license_type: 'bsd_3_clause', +}; + +export const baseResponse: VerifiedContractsResponse = { + items: [ + contract1, + contract2, + ], + next_page_params: { + items_count: '50', + smart_contract_id: '172', + }, +}; diff --git a/mocks/ens/domain.ts b/mocks/ens/domain.ts new file mode 100644 index 0000000000..6cf446b833 --- /dev/null +++ b/mocks/ens/domain.ts @@ -0,0 +1,136 @@ +import * as bens from '@blockscout/bens-types'; + +const domainTokenA: bens.Token = { + id: '97352314626701792030827861137068748433918254309635329404916858191911576754327', + contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85', + type: bens.TokenType.NATIVE_DOMAIN_TOKEN, +}; +const domainTokenB = { + id: '423546333', + contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea86', + type: bens.TokenType.WRAPPED_DOMAIN_TOKEN, +}; + +export const protocolA: bens.ProtocolInfo = { + id: 'ens', + short_name: 'ENS', + title: 'Ethereum Name Service', + description: 'The Ethereum Name Service (ENS) is a distributed, open, and extensible naming system based on the Ethereum blockchain.', + tld_list: [ + 'eth', + 'xyz', + ], + icon_url: 'https://i.imgur.com/GOfUwCb.jpeg', + docs_url: 'https://docs.ens.domains/', + deployment_blockscout_base_url: 'http://localhost:3200/', +}; + +export const protocolB: bens.ProtocolInfo = { + id: 'duck', + short_name: 'DUCK', + title: 'Duck Name Service', + description: '"Duck Name Service" is a cutting-edge blockchain naming service, providing seamless naming for crypto and decentralized applications. 🦆', + tld_list: [ + 'duck', + 'quack', + ], + icon_url: 'https://localhost:3000/duck.jpg', + docs_url: 'https://docs.duck.domains/', + deployment_blockscout_base_url: '', +}; + +export const ensDomainA: bens.DetailedDomain = { + id: '0xb140bf9645e54f02ed3c1bcc225566b515a98d1688f10494a5c3bc5b447936a7', + tokens: [ + domainTokenA, + domainTokenB, + ], + name: 'cat.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + registration_date: '2021-06-27T13:34:44.000Z', + expiry_date: '2025-03-01T14:20:24.000Z', + other_addresses: { + ETH: 'fe6ab8a0dafe7d41adf247c210451c264155c9b0', + GNO: 'DDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', + NEAR: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near', + }, + protocol: protocolA, + resolver_address: { + hash: '0xD578780f1dA7404d9CC0eEbC9D684c140CC4b638', + }, + resolved_with_wildcard: true, + stored_offchain: true, + wrapped_owner: { + hash: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401', + }, +}; + +export const ensDomainB: bens.DetailedDomain = { + id: '0x632ac7bec8e883416b371b36beaa822f4784208c99d063ee030020e2bd09b885', + tokens: [ domainTokenA ], + name: 'kitty.kitty.kitty.cat.eth', + resolved_address: undefined, + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: undefined, + registration_date: '2023-08-13T13:01:12.000Z', + expiry_date: undefined, + other_addresses: {}, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; + +export const ensDomainC: bens.DetailedDomain = { + id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ad', + tokens: [ domainTokenA ], + name: 'duck.duck.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: undefined, + registration_date: '2022-04-24T07:34:44.000Z', + expiry_date: '2022-11-01T13:10:36.000Z', + other_addresses: {}, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; + +export const ensDomainD: bens.DetailedDomain = { + id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ae', + tokens: [ domainTokenA ], + name: '🦆.duck.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + owner: undefined, + wrapped_owner: undefined, + registration_date: '2022-04-24T07:34:44.000Z', + expiry_date: '2027-09-23T13:10:36.000Z', + other_addresses: {}, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; diff --git a/mocks/ens/events.ts b/mocks/ens/events.ts new file mode 100644 index 0000000000..d13fbcbe9a --- /dev/null +++ b/mocks/ens/events.ts @@ -0,0 +1,19 @@ +import type * as bens from '@blockscout/bens-types'; + +export const ensDomainEventA: bens.DomainEvent = { + transaction_hash: '0x86c66b9fad66e4f20d42a6eed4fe12a0f48a274786fd85e9d4aa6c60e84b5874', + timestamp: '2021-06-27T13:34:44.000000Z', + from_address: { + hash: '0xaa96a50a2f67111262fe24576bd85bb56ec65016', + }, + action: '0xf7a16963', +}; + +export const ensDomainEventB: bens.DomainEvent = { + transaction_hash: '0x150bf7d5cd42457dd9c799ddd9d4bf6c30c703e1954a88c6d4b668b23fe0fbf8', + timestamp: '2022-11-02T14:20:24.000000Z', + from_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + action: 'register', +}; diff --git a/mocks/l2deposits/deposits.ts b/mocks/l2deposits/deposits.ts new file mode 100644 index 0000000000..b94c2dc657 --- /dev/null +++ b/mocks/l2deposits/deposits.ts @@ -0,0 +1,33 @@ +export const data = { + items: [ + { + l1_block_number: 8382841, + l1_block_timestamp: '2022-05-27T01:13:48.000000Z', + l1_tx_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + l1_tx_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + l2_tx_gas_limit: '2156928', + l2_tx_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + l1_block_timestamp: '2022-05-27T01:13:48.000000Z', + l1_tx_hash: '0xa280f18cc72f9ad904087eb262c236048e935ad184a85bbd042d544c172c10bf', + l1_tx_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + l2_tx_gas_limit: '1216064', + l2_tx_hash: '0xaaaeb47a78b5c42d870f8d831a683a7cefe1b031a992170b28b43b82bd08318c', + }, + { + l1_block_number: 8382834, + l1_block_timestamp: '2022-06-27T01:11:48.000000Z', + l1_tx_hash: '0xfca8cc5440bffa8b975873c02bba3ff3380dd75fbc3260d10179e282cf72d6d4', + l1_tx_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + l2_tx_gas_limit: '405824', + l2_tx_hash: '0xa0604ebf2614ad708aeefa83f766fb25928dadb5ffb2f45028f5b4f1fa4d9358', + }, + ], + next_page_params: { + items_count: 50, + l1_block_number: 8382363, + tx_hash: '0x2012f0ce966ce6573e7826e9235f227edf5a2f8382b8d646c979f85a77e15c05', + }, +}; diff --git a/mocks/l2disputeGames/disputeGames.ts b/mocks/l2disputeGames/disputeGames.ts new file mode 100644 index 0000000000..f522775cac --- /dev/null +++ b/mocks/l2disputeGames/disputeGames.ts @@ -0,0 +1,26 @@ +export const data = { + items: [ + { + contract_address: '0x5cbe1b88b6357e6a8f0821bea72cc0b88c231f1c', + created_at: '2022-05-27T01:13:48.000000Z', + game_type: 0, + index: 6662, + l2_block_number: 12542890, + resolved_at: null, + status: 'In progress', + }, + { + contract_address: '0x5cbe1b88b6357e6a8f0821bea72cc0b88c231f1c', + created_at: '2022-05-27T01:13:48.000000Z', + game_type: 0, + index: 6662, + l2_block_number: 12542890, + resolved_at: '2022-05-27T01:13:48.000000Z', + status: 'Defender wins', + }, + ], + next_page_params: { + items_count: 50, + index: 8382363, + }, +}; diff --git a/mocks/l2outputRoots/outputRoots.ts b/mocks/l2outputRoots/outputRoots.ts new file mode 100644 index 0000000000..9f21572297 --- /dev/null +++ b/mocks/l2outputRoots/outputRoots.ts @@ -0,0 +1,32 @@ +export const outputRootsData = { + items: [ + { + l1_block_number: 8456113, + l1_timestamp: '2022-02-08T12:08:48.000000Z', + l1_tx_hash: '0x19455a53758d5de89070164ff09c40d93f1b4447e721090f03aa150f6159265a', + l2_block_number: 5214988, + l2_output_index: 9926, + output_root: '0xa7de9bd3986ce5ca8de9f0ab6c7473f4cebe225fb13b57cc5c8472de84a8bab3', + }, + { + l1_block_number: 8456099, + l1_timestamp: '2022-02-08T12:05:24.000000Z', + l1_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b', + l2_block_number: 5214868, + l2_output_index: 9925, + output_root: '0x4ec2822d2f7b4f834d693d88f8a4cf15899882915980a21756d29cfd9f9f3898', + }, + { + l1_block_number: 8456078, + l1_timestamp: '2022-02-08T12:00:48.000000Z', + l1_tx_hash: '0x4238988b0959e41a7b09cef67f58698e05e3bcc29b8d2f60e6c77dc68c91f16e', + l2_block_number: 5214748, + l2_output_index: 9924, + output_root: '0x78b2e13c20f4bbfb4a008127edaaf25aa476f933669edd4856305bf4ab64a92b', + }, + ], + next_page_params: { + index: 9877, + items_count: 50, + }, +}; diff --git a/mocks/l2txnBatches/txnBatches.ts b/mocks/l2txnBatches/txnBatches.ts new file mode 100644 index 0000000000..f3c086c855 --- /dev/null +++ b/mocks/l2txnBatches/txnBatches.ts @@ -0,0 +1,33 @@ +export const txnBatchesData = { + items: [ + { + l1_tx_hashes: [ + '0x5bc94d02b65743dfaa9e10a2d6e175aff2a05cce2128c8eaf848bd84ab9325c5', + '0x92a51bc623111dbb91f243e3452e60fab6f090710357f9d9b75ac8a0f67dfd9d', + ], + l1_timestamp: '2023-02-24T10:16:12.000000Z', + l2_block_number: 5902836, + tx_count: 0, + }, + { + l1_tx_hashes: [ + '0xc45f846ee28ce9ba116ce2d378d3dd00b55d324b833b3ecd4241c919c572c4aa', + ], + l1_timestamp: '2023-02-24T10:16:00.000000Z', + l2_block_number: 5902835, + tx_count: 0, + }, + { + l1_tx_hashes: [ + '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8', + ], + l1_timestamp: '2023-02-24T10:16:00.000000Z', + l2_block_number: 5902834, + tx_count: 0, + }, + ], + next_page_params: { + block_number: 5902834, + items_count: 50, + }, +}; diff --git a/mocks/l2withdrawals/withdrawals.ts b/mocks/l2withdrawals/withdrawals.ts new file mode 100644 index 0000000000..046b467402 --- /dev/null +++ b/mocks/l2withdrawals/withdrawals.ts @@ -0,0 +1,50 @@ +import type { OptimisticL2WithdrawalsResponse } from 'types/api/optimisticL2'; + +export const data: OptimisticL2WithdrawalsResponse = { + items: [ + { + challenge_period_end: null, + from: { + hash: '0x67aab90c548b284be30b05c376001b4db90b87ba', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l1_tx_hash: '0x1a235bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136dca684', + l2_timestamp: '2022-02-15T12:50:02.000000Z', + l2_tx_hash: '0x918cd8c5c24c17e06cd02b0379510c4ad56324bf153578fb9caaaa2fe4e7dc35', + msg_nonce: 396, + msg_nonce_version: 1, + status: 'Ready to prove', + }, + { + challenge_period_end: null, + from: null, + l1_tx_hash: null, + l2_timestamp: null, + l2_tx_hash: '0x2f117bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136def593', + msg_nonce: 391, + msg_nonce_version: 1, + status: 'Ready to prove', + }, + { + challenge_period_end: '2022-11-11T12:50:02.000000Z', + from: null, + l1_tx_hash: null, + l2_timestamp: null, + l2_tx_hash: '0xe14b1f46838176702244a5343629bcecf728ca2d9881d47b4ce46e00c387d7e3', + msg_nonce: 390, + msg_nonce_version: 1, + status: 'Ready for relay', + }, + ], + next_page_params: { + items_count: 50, + nonce: '1766847064778384329583297500742918515827483896875618958121606201292620123', + }, +}; diff --git a/mocks/metadata/address.ts b/mocks/metadata/address.ts new file mode 100644 index 0000000000..f708fcb319 --- /dev/null +++ b/mocks/metadata/address.ts @@ -0,0 +1,92 @@ +/* eslint-disable max-len */ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +export const nameTag: AddressMetadataTagApi = { + slug: 'quack-quack', + name: 'Quack quack', + tagType: 'name', + ordinal: 99, + meta: null, +}; + +export const customNameTag: AddressMetadataTagApi = { + slug: 'unicorn-uproar', + name: 'Unicorn Uproar', + tagType: 'name', + ordinal: 777, + meta: { + tagUrl: 'https://example.com', + bgColor: 'linear-gradient(45deg, deeppink, deepskyblue)', + textColor: '#FFFFFF', + }, +}; + +export const genericTag: AddressMetadataTagApi = { + slug: 'duck-owner', + name: 'duck owner 🦆', + tagType: 'generic', + ordinal: 55, + meta: { + bgColor: 'rgba(255,243,12,90%)', + }, +}; + +export const infoTagWithLink: AddressMetadataTagApi = { + slug: 'goosegang', + name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG', + tagType: 'classifier', + ordinal: 11, + meta: { + tagUrl: 'https://example.com', + }, +}; + +export const tagWithTooltip: AddressMetadataTagApi = { + slug: 'blockscout-heroes', + name: 'BlockscoutHeroes', + tagType: 'classifier', + ordinal: 42, + meta: { + tooltipDescription: 'The Blockscout team, EVM blockchain aficionados, illuminate Ethereum networks with unparalleled insight and prowess, leading the way in blockchain exploration! 🚀🔎', + tooltipIcon: 'https://localhost:3100/icon.svg', + tooltipTitle: 'Blockscout team member', + tooltipUrl: 'https://blockscout.com', + }, +}; + +export const protocolTag: AddressMetadataTagApi = { + slug: 'aerodrome', + name: 'Aerodrome', + tagType: 'protocol', + ordinal: 0, + meta: null, +}; + +export const protocolTagWithMeta: AddressMetadataTagApi = { + slug: 'uniswap', + name: 'Uniswap', + tagType: 'protocol', + ordinal: 0, + meta: { + appID: 'uniswap', + appMarketplaceURL: 'https://example.com', + appLogoURL: 'https://localhost:3100/icon.svg', + appActionButtonText: 'Swap', + textColor: '#FFFFFF', + bgColor: '#FF007A', + }, +}; + +export const warpcastTag: AddressMetadataTagApi = { + slug: 'warpcast-account', + name: 'Farcaster', + tagType: 'protocol', + ordinal: 0, + meta: { + bgColor: '#8465CB', + tagIcon: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2032%2029%22%3E%3Cpath%20d%3D%22M%205.507%200.072%20L%2026.097%200.072%20L%2026.097%204.167%20L%2031.952%204.167%20L%2030.725%208.263%20L%2029.686%208.263%20L%2029.686%2024.833%20C%2030.207%2024.833%2030.63%2025.249%2030.63%2025.763%20L%2030.63%2026.88%20L%2030.819%2026.88%20C%2031.341%2026.88%2031.764%2027.297%2031.764%2027.811%20L%2031.764%2028.928%20L%2021.185%2028.928%20L%2021.185%2027.811%20C%2021.185%2027.297%2021.608%2026.88%2022.13%2026.88%20L%2022.319%2026.88%20L%2022.319%2025.763%20C%2022.319%2025.316%2022.639%2024.943%2023.065%2024.853%20L%2023.045%2015.71%20C%2022.711%2012.057%2019.596%209.194%2015.802%209.194%20C%2012.008%209.194%208.893%2012.057%208.559%2015.71%20L%208.539%2024.845%20C%209.043%2024.919%209.663%2025.302%209.663%2025.763%20L%209.663%2026.88%20L%209.852%2026.88%20C%2010.373%2026.88%2010.796%2027.297%2010.796%2027.811%20L%2010.796%2028.928%20L%200.218%2028.928%20L%200.218%2027.811%20C%200.218%2027.297%200.641%2026.88%201.162%2026.88%20L%201.351%2026.88%20L%201.351%2025.763%20C%201.351%2025.249%201.774%2024.833%202.296%2024.833%20L%202.296%208.263%20L%201.257%208.263%20L%200.029%204.167%20L%205.507%204.167%20L%205.507%200.072%20Z%22%20fill%3D%22rgb(255%2C%20255%2C%20255)%22%3E%3C%2Fpath%3E%3Cpath%20d%3D%22M%2026.097%200.072%20L%2026.166%200.072%20L%2026.166%200.004%20L%2026.097%200.004%20Z%20M%205.507%200.072%20L%205.507%200.004%20L%205.438%200.004%20L%205.438%200.072%20Z%20M%2026.097%204.167%20L%2026.028%204.167%20L%2026.028%204.235%20L%2026.097%204.235%20Z%20M%2031.952%204.167%20L%2032.019%204.187%20L%2032.045%204.099%20L%2031.952%204.099%20L%2031.952%204.167%20Z%20M%2030.725%208.263%20L%2030.725%208.331%20L%2030.776%208.331%20L%2030.791%208.282%20Z%20M%2029.686%208.263%20L%2029.686%208.195%20L%2029.617%208.195%20L%2029.617%208.263%20Z%20M%2029.686%2024.833%20L%2029.617%2024.833%20L%2029.617%2024.901%20L%2029.686%2024.901%20Z%20M%2030.63%2026.88%20L%2030.561%2026.88%20L%2030.561%2026.948%20L%2030.63%2026.948%20Z%20M%2031.764%2028.928%20L%2031.764%2028.996%20L%2031.832%2028.996%20L%2031.832%2028.928%20Z%20M%2021.185%2028.928%20L%2021.116%2028.928%20L%2021.116%2028.996%20L%2021.185%2028.996%20Z%20M%2022.319%2026.88%20L%2022.319%2026.948%20L%2022.388%2026.948%20L%2022.388%2026.88%20Z%20M%2023.065%2024.853%20L%2023.08%2024.919%20L%2023.134%2024.908%20L%2023.134%2024.853%20Z%20M%2023.045%2015.71%20L%2023.114%2015.71%20L%2023.114%2015.707%20L%2023.113%2015.704%20Z%20M%208.559%2015.71%20L%208.49%2015.704%20L%208.49%2015.707%20L%208.49%2015.71%20Z%20M%208.539%2024.845%20L%208.47%2024.845%20L%208.469%2024.904%20L%208.528%2024.913%20Z%20M%209.663%2026.88%20L%209.594%2026.88%20L%209.594%2026.948%20L%209.663%2026.948%20Z%20M%2010.796%2028.928%20L%2010.796%2028.996%20L%2010.865%2028.996%20L%2010.865%2028.928%20Z%20M%200.218%2028.928%20L%200.149%2028.928%20L%200.149%2028.996%20L%200.218%2028.996%20Z%20M%201.351%2026.88%20L%201.351%2026.948%20L%201.42%2026.948%20L%201.42%2026.88%20Z%20M%202.296%2024.833%20L%202.296%2024.901%20L%202.365%2024.901%20L%202.365%2024.833%20Z%20M%202.296%208.263%20L%202.365%208.263%20L%202.365%208.195%20L%202.296%208.195%20Z%20M%201.257%208.263%20L%201.191%208.282%20L%201.205%208.331%20L%201.257%208.331%20Z%20M%200.029%204.167%20L%200.029%204.1%20L%20-0.063%204.1%20L%20-0.037%204.187%20L%200.029%204.167%20Z%20M%205.507%204.167%20L%205.507%204.235%20L%205.576%204.235%20L%205.576%204.167%20Z%20M%2026.097%200.004%20L%205.507%200.004%20L%205.507%200.139%20L%2026.097%200.139%20Z%20M%2026.166%204.167%20L%2026.166%200.072%20L%2026.028%200.072%20L%2026.028%204.167%20L%2026.166%204.167%20Z%20M%2031.952%204.099%20L%2026.097%204.099%20L%2026.097%204.235%20L%2031.952%204.235%20Z%20M%2030.791%208.282%20L%2032.019%204.187%20L%2031.886%204.148%20L%2030.658%208.244%20Z%20M%2029.686%208.331%20L%2030.725%208.331%20L%2030.725%208.195%20L%2029.686%208.195%20Z%20M%2029.755%2024.833%20L%2029.755%208.263%20L%2029.617%208.263%20L%2029.617%2024.833%20Z%20M%2030.699%2025.763%20C%2030.699%2025.212%2030.245%2024.765%2029.686%2024.765%20L%2029.686%2024.9%20C%2030.169%2024.9%2030.561%2025.287%2030.561%2025.763%20Z%20M%2030.699%2026.88%20L%2030.699%2025.763%20L%2030.561%2025.763%20L%2030.561%2026.88%20Z%20M%2030.819%2026.813%20L%2030.63%2026.813%20L%2030.63%2026.948%20L%2030.819%2026.948%20Z%20M%2031.832%2027.811%20C%2031.832%2027.26%2031.379%2026.813%2030.819%2026.813%20L%2030.819%2026.948%20C%2031.303%2026.948%2031.695%2027.335%2031.695%2027.811%20Z%20M%2031.832%2028.928%20L%2031.832%2027.811%20L%2031.695%2027.811%20L%2031.695%2028.928%20Z%20M%2026.097%2028.996%20L%2031.764%2028.996%20L%2031.764%2028.86%20L%2026.097%2028.86%20Z%20M%2023.074%2028.996%20L%2026.097%2028.996%20L%2026.097%2028.86%20L%2023.074%2028.86%20Z%20M%2021.185%2028.996%20L%2023.074%2028.996%20L%2023.074%2028.86%20L%2021.185%2028.86%20Z%20M%2021.116%2027.811%20L%2021.116%2028.928%20L%2021.254%2028.928%20L%2021.254%2027.811%20Z%20M%2022.13%2026.813%20C%2021.57%2026.813%2021.116%2027.26%2021.116%2027.811%20L%2021.254%2027.811%20C%2021.254%2027.335%2021.646%2026.948%2022.13%2026.948%20Z%20M%2022.319%2026.813%20L%2022.13%2026.813%20L%2022.13%2026.948%20L%2022.319%2026.948%20Z%20M%2022.25%2025.763%20L%2022.25%2026.88%20L%2022.388%2026.88%20L%2022.388%2025.763%20Z%20M%2023.051%2024.787%20C%2022.593%2024.883%2022.25%2025.284%2022.25%2025.763%20L%2022.388%2025.763%20C%2022.388%2025.349%2022.684%2025.003%2023.08%2024.919%20Z%20M%2022.976%2015.71%20L%2022.996%2024.853%20L%2023.134%2024.853%20L%2023.114%2015.71%20Z%20M%2015.802%209.262%20C%2019.559%209.262%2022.645%2012.098%2022.976%2015.716%20L%2023.113%2015.704%20C%2022.776%2012.016%2019.632%209.126%2015.802%209.126%20Z%20M%208.628%2015.716%20C%208.959%2012.098%2012.044%209.262%2015.802%209.262%20L%2015.802%209.126%20C%2011.972%209.126%208.828%2012.016%208.49%2015.704%20Z%20M%208.608%2024.845%20L%208.628%2015.71%20L%208.49%2015.71%20L%208.47%2024.845%20Z%20M%209.732%2025.763%20C%209.732%2025.502%209.557%2025.273%209.331%2025.105%20C%209.104%2024.935%208.812%2024.817%208.549%2024.778%20L%208.528%2024.912%20C%208.769%2024.948%209.039%2025.057%209.248%2025.213%20C%209.459%2025.37%209.594%2025.563%209.594%2025.763%20Z%20M%209.732%2026.88%20L%209.732%2025.763%20L%209.594%2025.763%20L%209.594%2026.88%20Z%20M%209.852%2026.813%20L%209.663%2026.813%20L%209.663%2026.948%20L%209.852%2026.948%20Z%20M%2010.865%2027.811%20C%2010.865%2027.26%2010.411%2026.813%209.852%2026.813%20L%209.852%2026.948%20C%2010.335%2026.948%2010.727%2027.335%2010.727%2027.811%20Z%20M%2010.865%2028.928%20L%2010.865%2027.811%20L%2010.727%2027.811%20L%2010.727%2028.928%20Z%20M%208.529%2028.996%20L%2010.796%2028.996%20L%2010.796%2028.86%20L%208.529%2028.86%20Z%20M%208.372%2028.996%20L%208.529%2028.996%20L%208.529%2028.86%20L%208.372%2028.86%20Z%20M%205.507%2028.996%20L%208.372%2028.996%20L%208.372%2028.86%20L%205.507%2028.86%20Z%20M%200.218%2028.996%20L%205.507%2028.996%20L%205.507%2028.86%20L%200.218%2028.86%20Z%20M%200.149%2027.811%20L%200.149%2028.928%20L%200.287%2028.928%20L%200.287%2027.811%20Z%20M%201.162%2026.813%20C%200.603%2026.813%200.149%2027.26%200.149%2027.811%20L%200.287%2027.811%20C%200.287%2027.335%200.679%2026.948%201.162%2026.948%20Z%20M%201.351%2026.813%20L%201.162%2026.813%20L%201.162%2026.948%20L%201.351%2026.948%20Z%20M%201.282%2025.763%20L%201.282%2026.88%20L%201.42%2026.88%20L%201.42%2025.763%20Z%20M%202.296%2024.765%20C%201.736%2024.765%201.282%2025.212%201.282%2025.763%20L%201.42%2025.763%20C%201.42%2025.287%201.812%2024.9%202.296%2024.9%20Z%20M%202.227%208.263%20L%202.227%2024.833%20L%202.365%2024.833%20L%202.365%208.263%20Z%20M%201.257%208.331%20L%202.296%208.331%20L%202.296%208.195%20L%201.257%208.195%20Z%20M%20-0.037%204.187%20L%201.191%208.282%20L%201.323%208.244%20L%200.095%204.148%20Z%20M%205.507%204.099%20L%200.029%204.099%20L%200.029%204.235%20L%205.507%204.235%20L%205.507%204.099%20Z%20M%205.438%200.072%20L%205.438%204.167%20L%205.576%204.167%20L%205.576%200.072%20Z%22%20fill%3D%22rgb(255%2C255%2C255)%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E', tagUrl: 'https://warpcast.com/mbj357', + textColor: '#FFFFFF', + tooltipDescription: 'This address is linked to a Farcaster account', + warpcastHandle: 'duckYduck', + }, +}; diff --git a/mocks/metadata/appActionButton.ts b/mocks/metadata/appActionButton.ts new file mode 100644 index 0000000000..47938638cd --- /dev/null +++ b/mocks/metadata/appActionButton.ts @@ -0,0 +1,38 @@ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +const appID = 'uniswap'; +const appMarketplaceURL = 'https://example.com'; +export const appLogoURL = 'https://localhost:3100/icon.svg'; +const appActionButtonText = 'Swap'; +const textColor = '#FFFFFF'; +const bgColor = '#FF007A'; + +export const buttonWithoutStyles: AddressMetadataTagApi['meta'] = { + appID, + appMarketplaceURL, + appLogoURL, + appActionButtonText, +}; + +export const linkWithoutStyles: AddressMetadataTagApi['meta'] = { + appMarketplaceURL, + appLogoURL, + appActionButtonText, +}; + +export const buttonWithStyles: AddressMetadataTagApi['meta'] = { + appID, + appMarketplaceURL, + appLogoURL, + appActionButtonText, + textColor, + bgColor, +}; + +export const linkWithStyles: AddressMetadataTagApi['meta'] = { + appMarketplaceURL, + appLogoURL, + appActionButtonText, + textColor, + bgColor, +}; diff --git a/mocks/metadata/publicTagTypes.ts b/mocks/metadata/publicTagTypes.ts new file mode 100644 index 0000000000..80bf92acba --- /dev/null +++ b/mocks/metadata/publicTagTypes.ts @@ -0,0 +1,34 @@ +export const publicTagTypes = { + tagTypes: [ + { + id: '96f9db76-02fc-477d-a003-640a0c5e7e15', + type: 'name' as const, + description: 'Alias for the address', + }, + { + id: 'e75f396e-f52a-44c9-8790-a1dbae496b72', + type: 'generic' as const, + description: 'Group classification for the address', + }, + { + id: '11a2d4f3-412e-4eb7-b663-86c6f48cdec3', + type: 'information' as const, + description: 'Tags with custom data for the address, e.g. additional link to project, or classification details, or minor account details', + }, + { + id: 'd37443d4-748f-4314-a4a0-283b666e9f29', + type: 'classifier' as const, + description: 'E.g. "ERC20", "Contract", "CEX", "DEX", "NFT"', + }, + { + id: 'ea9d0f91-9b46-44ff-be70-128bac468f6f', + type: 'protocol' as const, + description: 'Special tag type for protocol-related contracts, e.g. for bridges', + }, + { + id: 'd2600acb-473c-445f-ac72-ed6fef53e06a', + type: 'note' as const, + description: 'Short general-purpose description for the address', + }, + ], +}; diff --git a/mocks/mud/mudTables.ts b/mocks/mud/mudTables.ts new file mode 100644 index 0000000000..b5f0186c91 --- /dev/null +++ b/mocks/mud/mudTables.ts @@ -0,0 +1,94 @@ +/* eslint-disable max-len */ +import type { AddressMudRecord, AddressMudRecords, AddressMudRecordsItem, AddressMudTables } from 'types/api/address'; +import type { MudWorldSchema, MudWorldTable } from 'types/api/mudWorlds'; + +export const table1: MudWorldTable = { + table_full_name: 'tb.store.Tables', + table_id: '0x746273746f72650000000000000000005461626c657300000000000000000000', + table_name: 'Tables', + table_namespace: 'store', + table_type: 'onchain', +}; + +export const table2: MudWorldTable = { + table_full_name: 'ot.world.FunctionSignatur', + table_id: '0x6f74776f726c6400000000000000000046756e6374696f6e5369676e61747572', + table_name: 'FunctionSignatur', + table_namespace: 'world', + table_type: 'offchain', +}; + +export const schema1: MudWorldSchema = { + key_names: [ 'moduleAddress', 'argumentsHash' ], + key_types: [ 'address', 'bytes32' ], + value_names: [ 'fieldLayout', 'keySchema', 'valueSchema', 'abiEncodedKeyNames', 'abiEncodedFieldNames' ], + value_types: [ 'bytes32', 'bytes32', 'bytes32', 'bytes', 'bytes' ], +}; + +export const schema2: MudWorldSchema = { + key_names: [], + key_types: [], + value_names: [ 'value' ], + value_types: [ 'address' ], +}; + +export const mudTables: AddressMudTables = { + items: [ + { + table: table1, + schema: schema1, + }, + { + table: table2, + schema: schema2, + }, + ], + next_page_params: { + items_count: 50, + table_id: '1', + }, +}; + +const record: AddressMudRecordsItem = { + decoded: { + abiEncodedFieldNames: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000006706c617965720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000974696d657374616d700000000000000000000000000000000000000000000000', + abiEncodedKeyNames: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026964000000000000000000000000000000000000000000000000000000000000', + goldCosts: [ '100000', '150000', '200000', '250000', '400000', '550000', '700000' ], + prototypeIds: [ + '0x53776f7264736d616e0000000000000000000000000000000000000000000000', + '0x50696b656d616e00000000000000000000000000000000000000000000000000', + '0x50696b656d616e00000000000000000000000000000000000000000000000000', + '0x4172636865720000000000000000000000000000000000000000000000000000', + '0x4b6e696768740000000000000000000000000000000000000000000000000000', + ], + keySchema: '0x002001001f000000000000000000000000000000000000000000000000000000', + tableId: '0x6f74000000000000000000000000000044726177557064617465000000000000', + valueSchema: '0x00540300611f1f00000000000000000000000000000000000000000000000000', + }, + id: '0x007a651a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007', + is_deleted: false, + timestamp: '2024-05-09T15:14:32.000000Z', +}; + +export const mudRecords: AddressMudRecords = { + items: [ record, record ], + next_page_params: { + items_count: 50, + key0: '1', + key1: '2', + key_bytes: '3', + }, + schema: { + key_names: [ 'tableId' ], + key_types: [ 'bytes32' ], + value_names: [ 'prototypeIds', 'goldCosts', 'keySchema', 'valueSchema', 'abiEncodedKeyNames', 'abiEncodedFieldNames' ], + value_types: [ 'bytes32[]', 'int32[]', 'bytes32', 'bytes32', 'bytes32', 'bytes', 'bytes' ], + }, + table: table1, +}; + +export const mudRecord: AddressMudRecord = { + record, + schema: mudRecords.schema, + table: table1, +}; diff --git a/mocks/mud/mudWorlds.ts b/mocks/mud/mudWorlds.ts new file mode 100644 index 0000000000..2d8218e884 --- /dev/null +++ b/mocks/mud/mudWorlds.ts @@ -0,0 +1,27 @@ +import type { MudWorldsResponse } from 'types/api/mudWorlds'; + +import { withName, withoutName } from 'mocks/address/address'; + +export const mudWorlds: MudWorldsResponse = { + items: [ + { + address: withName, + coin_balance: '300000000000000000', + tx_count: 3938, + }, + { + address: withoutName, + coin_balance: '0', + tx_count: 0, + }, + { + address: withoutName, + coin_balance: '0', + tx_count: 0, + }, + ], + next_page_params: { + items_count: 50, + world: '0x18f01f12ca21b6fc97b917c3e32f671f8a933caa', + }, +}; diff --git a/mocks/noves/transaction.ts b/mocks/noves/transaction.ts new file mode 100644 index 0000000000..6feb72a564 --- /dev/null +++ b/mocks/noves/transaction.ts @@ -0,0 +1,103 @@ +import type { NovesResponseData } from 'types/api/noves'; + +import type { TokensData } from 'ui/tx/assetFlows/utils/getTokensData'; + +export const hash = '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53'; + +export const transaction: NovesResponseData = { + accountAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + chain: 'eth-goerli', + classificationData: { + description: 'Called function \'stake\' on contract 0xef326CdAdA59D3A740A76bB5f4F88Fb2.', + protocol: { + name: null, + }, + received: [], + sent: [ + { + action: 'sent', + actionFormatted: 'Sent', + amount: '3000', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + { + action: 'paidGas', + actionFormatted: 'Paid Gas', + amount: '0.000395521502109448', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: null, + name: 'Validators', + }, + token: { + address: 'ETH', + decimals: 18, + name: 'ETH', + symbol: 'ETH', + }, + }, + ], + source: { + type: null, + }, + type: 'unclassified', + typeFormatted: 'Unclassified', + }, + rawTransactionData: { + blockNumber: 10388918, + fromAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + gas: 275079, + gasPrice: 1500000008, + timestamp: 1705488588, + toAddress: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + transactionFee: { + amount: '395521502109448', + token: { + decimals: 18, + symbol: 'ETH', + }, + }, + transactionHash: '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53', + }, + txTypeVersion: 2, +}; + +export const tokenData: TokensData = { + nameList: [ 'PQR-Test', 'ETH' ], + symbolList: [ 'PQR' ], + idList: [], + byName: { + 'PQR-Test': { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + ETH: { name: 'ETH', symbol: null, address: '', id: undefined }, + }, + bySymbol: { + PQR: { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + 'null': { name: 'ETH', symbol: null, address: '', id: undefined }, + }, +}; diff --git a/mocks/search/index.ts b/mocks/search/index.ts new file mode 100644 index 0000000000..34015b5157 --- /dev/null +++ b/mocks/search/index.ts @@ -0,0 +1,162 @@ +import type { + SearchResultToken, + SearchResultBlock, + SearchResultAddressOrContract, + SearchResultTx, + SearchResultLabel, + SearchResult, + SearchResultUserOp, + SearchResultBlob, + SearchResultDomain, +} from 'types/api/search'; + +export const token1: SearchResultToken = { + address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + name: 'Toms NFT', + symbol: 'TNT', + token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B', + type: 'token' as const, + icon_url: 'http://localhost:3000/token-icon.png', + token_type: 'ERC-721', + total_supply: '10000001', + exchange_rate: null, + is_verified_via_admin_panel: true, + is_smart_contract_verified: true, +}; + +export const token2: SearchResultToken = { + address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + name: 'TomToken', + symbol: 'pdE1B', + token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9', + type: 'token' as const, + icon_url: null, + token_type: 'ERC-20', + total_supply: '10000001', + exchange_rate: '1.11', + is_verified_via_admin_panel: false, + is_smart_contract_verified: false, +}; + +export const block1: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', + block_number: 8198536, + type: 'block' as const, + timestamp: '2022-12-11T17:55:20Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1', +}; + +export const block2: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', + block_number: 8198536, + block_type: 'reorg', + type: 'block' as const, + timestamp: '2022-12-11T18:55:20Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', +}; + +export const block3: SearchResultBlock = { + block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3', + block_number: 8198536, + block_type: 'uncle', + type: 'block' as const, + timestamp: '2022-12-11T18:11:11Z', + url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3', +}; + +export const address1: SearchResultAddressOrContract = { + address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const address2: SearchResultAddressOrContract = { + address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + ens_info: { + address_hash: '0x1234567890123456789012345678901234567890', + expiry_date: '2022-12-11T17:55:20Z', + name: 'utko.eth', + names_count: 1, + }, +}; + +export const contract1: SearchResultAddressOrContract = { + address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'Unknown contract in this network', + type: 'contract' as const, + is_smart_contract_verified: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const contract2: SearchResultAddressOrContract = { + address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'Super utko', + type: 'contract' as const, + is_smart_contract_verified: true, + certified: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const label1: SearchResultLabel = { + address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', + name: 'utko', + type: 'label' as const, + is_smart_contract_verified: true, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', +}; + +export const tx1: SearchResultTx = { + tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', + type: 'transaction' as const, + timestamp: '2022-12-11T17:55:20Z', + url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', +}; + +export const userOp1: SearchResultUserOp = { + timestamp: '2024-01-11T14:15:48.000000Z', + type: 'user_operation', + user_operation_hash: '0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', + url: '/op/0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61', +}; + +export const blob1: SearchResultBlob = { + blob_hash: '0x0108dd3e414da9f3255f7a831afa606e8dfaea93d082dfa9b15305583cbbdbbe', + type: 'blob' as const, + timestamp: null, +}; + +export const domain1: SearchResultDomain = { + address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + ens_info: { + address_hash: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + expiry_date: '2039-09-01T07:36:18.000Z', + name: 'vitalik.eth', + names_count: 1, + }, + is_smart_contract_verified: false, + name: null, + type: 'ens_domain', + url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', +}; + +export const baseResponse: SearchResult = { + items: [ + token1, + token2, + block1, + address1, + contract1, + tx1, + blob1, + domain1, + ], + next_page_params: null, +}; diff --git a/mocks/shibarium/deposits.ts b/mocks/shibarium/deposits.ts new file mode 100644 index 0000000000..7081042cd8 --- /dev/null +++ b/mocks/shibarium/deposits.ts @@ -0,0 +1,61 @@ +import type { ShibariumDepositsResponse } from 'types/api/shibarium'; + +export const data: ShibariumDepositsResponse = { + items: [ + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l1_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + ], + next_page_params: { + items_count: 50, + block_number: 8382363, + }, +}; diff --git a/mocks/shibarium/withdrawals.ts b/mocks/shibarium/withdrawals.ts new file mode 100644 index 0000000000..6c1e875264 --- /dev/null +++ b/mocks/shibarium/withdrawals.ts @@ -0,0 +1,61 @@ +import type { ShibariumWithdrawalsResponse } from 'types/api/shibarium'; + +export const data: ShibariumWithdrawalsResponse = { + items: [ + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + { + l2_block_number: 8382841, + timestamp: '2022-05-27T01:13:48.000000Z', + l1_transaction_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e', + user: { + hash: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + l2_transaction_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667', + }, + ], + next_page_params: { + items_count: 50, + block_number: 8382363, + }, +}; diff --git a/mocks/stats/daily_txs.ts b/mocks/stats/daily_txs.ts new file mode 100644 index 0000000000..afb2dcca58 --- /dev/null +++ b/mocks/stats/daily_txs.ts @@ -0,0 +1,149 @@ +export const base = { + chart_data: [ + { + date: '2022-11-28', + tx_count: 26815, + }, + { + date: '2022-11-27', + tx_count: 34784, + }, + { + date: '2022-11-26', + tx_count: 77527, + }, + { + date: '2022-11-25', + tx_count: 39687, + }, + { + date: '2022-11-24', + tx_count: 40752, + }, + { + date: '2022-11-23', + tx_count: 32569, + }, + { + date: '2022-11-22', + tx_count: 34449, + }, + { + date: '2022-11-21', + tx_count: 106047, + }, + { + date: '2022-11-20', + tx_count: 107713, + }, + { + date: '2022-11-19', + tx_count: 96311, + }, + { + date: '2022-11-18', + tx_count: 30828, + }, + { + date: '2022-11-17', + tx_count: 27422, + }, + { + date: '2022-11-16', + tx_count: 75898, + }, + { + date: '2022-11-15', + tx_count: 84084, + }, + { + date: '2022-11-14', + tx_count: 62266, + }, + { + date: '2022-11-13', + tx_count: 22338, + }, + { + date: '2022-11-12', + tx_count: 86764, + }, + { + date: '2022-11-11', + tx_count: 79493, + }, + { + date: '2022-11-10', + tx_count: 92887, + }, + { + date: '2022-11-09', + tx_count: 43691, + }, + { + date: '2022-11-08', + tx_count: 74197, + }, + { + date: '2022-11-07', + tx_count: 58131, + }, + { + date: '2022-11-06', + tx_count: 62477, + }, + { + date: '2022-11-05', + tx_count: 82897, + }, + { + date: '2022-11-04', + tx_count: 91725, + }, + { + date: '2022-11-03', + tx_count: 83667, + }, + { + date: '2022-11-02', + tx_count: 63743, + }, + { + date: '2022-11-01', + tx_count: 152059, + }, + { + date: '2022-10-31', + tx_count: 62519, + }, + { + date: '2022-10-30', + tx_count: 48569, + }, + { + date: '2022-10-29', + tx_count: 36789, + }, + ], +}; + +export const partialData = { + chart_data: [ + { date: '2022-11-28', tx_count: 26815 }, + { date: '2022-11-27', tx_count: 34784 }, + { date: '2022-11-26', tx_count: 77527 }, + { date: '2022-11-25', tx_count: null }, + { date: '2022-11-24', tx_count: null }, + { date: '2022-11-23', tx_count: null }, + { date: '2022-11-22', tx_count: 63433 }, + { date: '2022-11-21', tx_count: null }, + ], +}; + +export const noData = { + chart_data: [ + { date: '2022-11-25', tx_count: null }, + { date: '2022-11-24', tx_count: null }, + { date: '2022-11-23', tx_count: null }, + ], +}; diff --git a/mocks/stats/index.ts b/mocks/stats/index.ts new file mode 100644 index 0000000000..0f0d408e7a --- /dev/null +++ b/mocks/stats/index.ts @@ -0,0 +1,90 @@ +import _mapValues from 'lodash/mapValues'; + +import type { HomeStats } from 'types/api/stats'; + +export const base: HomeStats = { + average_block_time: 6212.0, + coin_price: '0.00199678', + coin_price_change_percentage: -7.42, + coin_image: 'http://localhost:3100/utia.jpg', + gas_prices: { + average: { + fiat_price: '1.39', + price: 23.75, + time: 12030.25, + base_fee: 2.22222, + priority_fee: 12.424242, + }, + fast: { + fiat_price: '1.74', + price: 29.72, + time: 8763.25, + base_fee: 4.44444, + priority_fee: 22.242424, + }, + slow: { + fiat_price: '1.35', + price: 23.04, + time: 20100.25, + base_fee: 1.11111, + priority_fee: 7.8909, + }, + }, + gas_price_updated_at: '2022-11-11T11:09:49.051171Z', + gas_prices_update_in: 300000, + gas_used_today: '4108680603', + market_cap: '330809.96443288102524', + network_utilization_percentage: 1.55372064, + static_gas_price: '10', + total_addresses: '19667249', + total_blocks: '30215608', + total_gas_used: '0', + total_transactions: '82258122', + transactions_today: '26815', + tvl: '1767425.102766552', +}; + +export const withBtcLocked: HomeStats = { + ...base, + rootstock_locked_btc: '3337493406696977561374', +}; + +export const withoutFiatPrices: HomeStats = { + ...base, + gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null), +}; + +export const withoutGweiPrices: HomeStats = { + ...base, + gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null), +}; + +export const withoutBothPrices: HomeStats = { + ...base, + gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), +}; + +export const withoutGasInfo: HomeStats = { + ...base, + gas_prices: null, +}; + +export const withSecondaryCoin: HomeStats = { + ...base, + secondary_coin_price: '3.398', +}; + +export const noChartData: HomeStats = { + ...base, + transactions_today: null, + coin_price: null, + market_cap: null, + tvl: null, +}; + +export const indexingStatus = { + finished_indexing_blocks: false, + indexed_blocks_ratio: '0.1', + finished_indexing: true, + indexed_internal_transactions_ratio: '1', +}; diff --git a/mocks/stats/line.ts b/mocks/stats/line.ts new file mode 100644 index 0000000000..799cff4c45 --- /dev/null +++ b/mocks/stats/line.ts @@ -0,0 +1,161 @@ +import type * as stats from '@blockscout/stats-types'; + +export const averageGasPrice: stats.LineChart = { + chart: [ + { + date: '2023-12-22', + value: '37.7804422597599', + is_approximate: false, + }, + { + date: '2023-12-23', + value: '25.84889883009387', + is_approximate: false, + }, + { + date: '2023-12-24', + value: '25.818463227198574', + is_approximate: false, + }, + { + date: '2023-12-25', + value: '26.045513050051298', + is_approximate: false, + }, + { + date: '2023-12-26', + value: '21.42600692652399', + is_approximate: false, + }, + { + date: '2023-12-27', + value: '31.066730409846656', + is_approximate: false, + }, + { + date: '2023-12-28', + value: '33.63955781902089', + is_approximate: false, + }, + { + date: '2023-12-29', + value: '28.064736756058384', + is_approximate: false, + }, + { + date: '2023-12-30', + value: '23.074500869678175', + is_approximate: false, + }, + { + date: '2023-12-31', + value: '17.651005734615133', + is_approximate: false, + }, + { + date: '2024-01-01', + value: '14.906085174476441', + is_approximate: false, + }, + { + date: '2024-01-02', + value: '22.28459059038656', + is_approximate: false, + }, + { + date: '2024-01-03', + value: '39.8311646806592', + is_approximate: false, + }, + { + date: '2024-01-04', + value: '26.09989322256083', + is_approximate: false, + }, + { + date: '2024-01-05', + value: '22.821996688111998', + is_approximate: false, + }, + { + date: '2024-01-06', + value: '20.32680041262083', + is_approximate: false, + }, + { + date: '2024-01-07', + value: '32.535045831809704', + is_approximate: false, + }, + { + date: '2024-01-08', + value: '27.443477102139482', + is_approximate: false, + }, + { + date: '2024-01-09', + value: '20.7911332558055', + is_approximate: false, + }, + { + date: '2024-01-10', + value: '42.10740192523919', + is_approximate: false, + }, + { + date: '2024-01-11', + value: '35.75215680343582', + is_approximate: false, + }, + { + date: '2024-01-12', + value: '27.430414798093253', + is_approximate: false, + }, + { + date: '2024-01-13', + value: '20.170934096589875', + is_approximate: false, + }, + { + date: '2024-01-14', + value: '38.79660984371034', + is_approximate: false, + }, + { + date: '2024-01-15', + value: '26.140740484554204', + is_approximate: false, + }, + { + date: '2024-01-16', + value: '36.708543184194156', + is_approximate: false, + }, + { + date: '2024-01-17', + value: '40.325438794298876', + is_approximate: false, + }, + { + date: '2024-01-18', + value: '37.55145309930694', + is_approximate: false, + }, + { + date: '2024-01-19', + value: '33.271450114434664', + is_approximate: false, + }, + { + date: '2024-01-20', + value: '19.303304377685638', + is_approximate: false, + }, + { + date: '2024-01-21', + value: '14.375908594704976', + is_approximate: false, + }, + ], +}; diff --git a/mocks/stats/lines.ts b/mocks/stats/lines.ts new file mode 100644 index 0000000000..9f8870249f --- /dev/null +++ b/mocks/stats/lines.ts @@ -0,0 +1,144 @@ +import type * as stats from '@blockscout/stats-types'; + +export const base: stats.LineCharts = { + sections: [ + { + id: 'accounts', + title: 'Accounts', + charts: [ + { + id: 'accountsGrowth', + title: 'Accounts growth', + description: 'Cumulative accounts number per period', + units: undefined, + }, + { + id: 'activeAccounts', + title: 'Active accounts', + description: 'Active accounts number per period', + units: undefined, + }, + { + id: 'newAccounts', + title: 'New accounts', + description: 'New accounts number per day', + units: undefined, + }, + ], + }, + { + id: 'transactions', + title: 'Transactions', + charts: [ + { + id: 'averageTxnFee', + title: 'Average transaction fee', + description: 'The average amount in ETH spent per transaction', + units: 'ETH', + }, + { + id: 'newTxns', + title: 'New transactions', + description: 'New transactions number', + units: undefined, + }, + { + id: 'txnsFee', + title: 'Transactions fees', + description: 'Amount of tokens paid as fees', + units: 'ETH', + }, + { + id: 'txnsGrowth', + title: 'Transactions growth', + description: 'Cumulative transactions number', + units: undefined, + }, + { + id: 'txnsSuccessRate', + title: 'Transactions success rate', + description: 'Successful transactions rate per day', + units: undefined, + }, + ], + }, + { + id: 'blocks', + title: 'Blocks', + charts: [ + { + id: 'averageBlockRewards', + title: 'Average block rewards', + description: 'Average amount of distributed reward in tokens per day', + units: 'ETH', + }, + { + id: 'averageBlockSize', + title: 'Average block size', + description: 'Average size of blocks in bytes', + units: 'Bytes', + }, + { + id: 'newBlocks', + title: 'New blocks', + description: 'New blocks number', + units: undefined, + }, + ], + }, + { + id: 'tokens', + title: 'Tokens', + charts: [ + { + id: 'newNativeCoinTransfers', + title: 'New ETH transfers', + description: 'New token transfers number for the period', + units: undefined, + }, + ], + }, + { + id: 'gas', + title: 'Gas', + charts: [ + { + id: 'averageGasLimit', + title: 'Average gas limit', + description: 'Average gas limit per block for the period', + units: undefined, + }, + { + id: 'averageGasPrice', + title: 'Average gas price', + description: 'Average gas price for the period (Gwei)', + units: 'Gwei', + }, + { + id: 'gasUsedGrowth', + title: 'Gas used growth', + description: 'Cumulative gas used for the period', + units: undefined, + }, + ], + }, + { + id: 'contracts', + title: 'Contracts', + charts: [ + { + id: 'newVerifiedContracts', + title: 'New verified contracts', + description: 'New verified contracts number for the period', + units: undefined, + }, + { + id: 'verifiedContractsGrowth', + title: 'Verified contracts growth', + description: 'Cumulative number verified contracts for the period', + units: undefined, + }, + ], + }, + ], +}; diff --git a/mocks/tokens/tokenHolders.ts b/mocks/tokens/tokenHolders.ts new file mode 100644 index 0000000000..89f2595482 --- /dev/null +++ b/mocks/tokens/tokenHolders.ts @@ -0,0 +1,39 @@ +import type { TokenHolders } from 'types/api/token'; + +import { withName, withoutName } from 'mocks/address/address'; + +export const tokenHoldersERC20: TokenHolders = { + items: [ + { + address: withName, + value: '107014805905725000000', + }, + { + address: withoutName, + value: '207014805905725000000', + }, + ], + next_page_params: { + value: '50', + items_count: 50, + }, +}; + +export const tokenHoldersERC1155: TokenHolders = { + items: [ + { + address: withName, + value: '107014805905725000000', + token_id: '12345', + }, + { + address: withoutName, + value: '207014805905725000000', + token_id: '12345', + }, + ], + next_page_params: { + value: '50', + items_count: 50, + }, +}; diff --git a/mocks/tokens/tokenInfo.ts b/mocks/tokens/tokenInfo.ts new file mode 100644 index 0000000000..a22c7537e5 --- /dev/null +++ b/mocks/tokens/tokenInfo.ts @@ -0,0 +1,212 @@ +import type { TokenCounters, TokenInfo } from 'types/api/token'; + +export const tokenInfo: TokenInfo = { + address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', + circulating_market_cap: '117629601.61913824', + decimals: '18', + exchange_rate: '2.0101', + holders: '46554', + name: 'ARIANEE', + symbol: 'ARIA', + type: 'ERC-20' as const, + total_supply: '1235', + icon_url: 'http://localhost:3000/token-icon.png', +}; + +export const tokenCounters: TokenCounters = { + token_holders_count: '8838883', + transfers_count: '88282281', +}; + +export const tokenInfoERC20a: TokenInfo<'ERC-20'> = { + address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456', + circulating_market_cap: '117268489.23970924', + decimals: '18', + exchange_rate: null, + holders: '23', + name: 'hyfi.token', + symbol: 'HyFi', + total_supply: '369000000000000000000000000', + type: 'ERC-20' as const, + icon_url: 'http://localhost:3000/token-icon.png', +}; + +export const tokenInfoERC20b: TokenInfo<'ERC-20'> = { + address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7', + circulating_market_cap: '115060192.36105014', + decimals: '6', + exchange_rate: '0.982', + holders: '17', + name: 'USD Coin', + symbol: 'USDC', + total_supply: '900000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC20c: TokenInfo<'ERC-20'> = { + address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A8', + circulating_market_cap: null, + decimals: '18', + exchange_rate: '1328.89', + holders: '17', + name: 'Ethereum', + symbol: 'ETH', + total_supply: '1000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC20d: TokenInfo<'ERC-20'> = { + address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9196', + circulating_market_cap: null, + decimals: '18', + exchange_rate: null, + holders: '102625', + name: 'Zeta', + symbol: 'ZETA', + total_supply: '2100000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = { + address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9197', + circulating_market_cap: '112855875.75888918', + decimals: '18', + exchange_rate: '1328.89', + holders: '102625', + name: 'Zeta', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + total_supply: '2100000000000000000000000000', + type: 'ERC-20' as const, + icon_url: null, +}; + +export const tokenInfoERC721a: TokenInfo<'ERC-721'> = { + address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '7', + name: 'HyFi Athena', + symbol: 'HYFI_ATHENA', + total_supply: '105', + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC721b: TokenInfo<'ERC-721'> = { + address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '2', + name: 'World Of Women Galaxy', + symbol: 'WOWG', + total_supply: null, + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC721c: TokenInfo<'ERC-721'> = { + address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '12', + name: 'Puma', + symbol: 'PUMA', + total_supply: null, + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = { + address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4993', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '12', + name: 'Puma', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + total_supply: null, + type: 'ERC-721' as const, + icon_url: null, +}; + +export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = { + address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '22', + name: 'HyFi Membership', + symbol: 'HYFI_MEMBERSHIP', + total_supply: '482', + type: 'ERC-1155' as const, + icon_url: null, +}; + +export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = { + address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '100', + name: 'WinkyVerse Collections', + symbol: 'WVC', + total_supply: '4943', + type: 'ERC-1155' as const, + icon_url: null, +}; + +export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = { + address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8a', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '22', + name: null, + symbol: null, + total_supply: '482', + type: 'ERC-1155' as const, + icon_url: null, +}; + +export const tokenInfoERC404: TokenInfo<'ERC-404'> = { + address: '0xB5C457dDB4cE3312a6C5a2b056a1652bd542a208', + circulating_market_cap: '0.0', + decimals: '18', + exchange_rate: '1484.13', + holders: '81', + icon_url: null, + name: 'OMNI404', + symbol: 'O404', + total_supply: '6482275000000000000', + type: 'ERC-404' as const, +}; + +export const bridgedTokenA: TokenInfo<'ERC-20'> = { + ...tokenInfoERC20a, + is_bridged: true, + origin_chain_id: '1', + bridge_type: 'omni', + foreign_address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8b', +}; + +export const bridgedTokenB: TokenInfo<'ERC-20'> = { + ...tokenInfoERC20b, + is_bridged: true, + origin_chain_id: '56', + bridge_type: 'omni', + foreign_address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefd', +}; + +export const bridgedTokenC: TokenInfo<'ERC-20'> = { + ...tokenInfoERC20d, + is_bridged: true, + origin_chain_id: '99', + bridge_type: 'amb', + foreign_address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4994', +}; diff --git a/mocks/tokens/tokenInstance.ts b/mocks/tokens/tokenInstance.ts new file mode 100644 index 0000000000..712c466b7d --- /dev/null +++ b/mocks/tokens/tokenInstance.ts @@ -0,0 +1,184 @@ +/* eslint-disable max-len */ +import type { TokenInstance } from 'types/api/token'; + +import * as addressMock from '../address/address'; + +export const base: TokenInstance = { + animation_url: null, + external_app_url: 'https://duck.nft/get-your-duck-today', + id: '32925298983216553915666621415831103694597106215670571463977478984525997408266', + image_url: 'https://example.com/image.jpg', + is_unique: false, + holder_address_hash: null, + metadata: { + attributes: [ + { + trait_type: 'skin', + value: '0', + }, + { + trait_type: 'eye', + value: '2', + }, + { + trait_type: 'nose', + value: '6', + }, + { + trait_type: 'spectacles', + value: '4', + }, + { + trait_type: 'hair', + value: '12', + }, + { + trait_type: 'shirt', + value: '1', + }, + { + trait_type: 'earrings', + value: '4', + }, + { + trait_type: 'mouth', + value: '5', + }, + { + trait_type: 'eventURL', + value: 'https://twitter.com/lilnounsdao?s=21&t=xAihrtwPd6avwdsQqeMXCw', + }, + { + trait_type: 'p1', + value: '57775', + }, + { + trait_type: 'p2', + value: '57772', + }, + { + display_type: 'number', + trait_type: 'difficulty', + value: 84, + }, + { + display_type: 'number', + trait_type: 'items', + value: 3, + }, + ], + description: '**GENESIS #188848**, **22a5f8bbb1602995** :: *84th* generation of *#57772 and #57775* :: **eGenetic Hash Code (eDNA)** = *3c457cc7f60f7853* :: [Click here for full biography.](https://vipsland.com/nft/collections/genesis/188848) :: crafted by [vipsland](https://vipsland.com/)', + external_url: 'https://vipsland.com/nft/collections/genesis/188848', + image: 'https://i.seadn.io/gcs/files/1ee1c5e1ead058322615e3206abb8ba3.png?w=500&auto=format', + name: 'GENESIS #188848, 22a5f8bbb1602995. Blockchain pixel PFP NFT + "on music video" trait inspired by God', + }, + owner: addressMock.withName, +}; + +export const withRichMetadata: TokenInstance = { + ...base, + metadata: { + background_color: '000000', + chain: 'MATIC', + chain_address: '0x66edbdb80001da74cbf3e6c01ba91154f6e2fb7c', + name: 'Carmelo Anthony', + total_nfts: 0, + animation_url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/dee8734bbefb0d63d6156b6fa0e1385822480589daa1862cbd37a94f6bc2ba3a', + series_key: 'Series', + nft_id: 'c746af09-8dcb-4cec-aa8a-5ff02fffc3f1', + description: 'All-Conference and All-American honors await Carmelo Anthony during his Freshman season for Syracuse. However, Anthony must first defeat a worthy opponent in Georgetown with a double-double effort of 30 points and 15 rebounds.\n \n\n© Syracuse University', + immutable_uri: 'https://nftu.com/nft-content/metadata/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/7741920', + contract_address: '0x63cf7b3d5808cb190aa301b55aafd6b4bb95efbb', + is_pack: false, + pack_open_locked_until: '2022-03-05T16:58:30.998Z', + rarity_key: 'Rarity', + images: { + png: { + primary: { + url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/0c66645c4e119f9c5def80273b768138d797f00583f557065a50bb0dd491e8e3', + cid: 'Qmf9hHAP884ZwYngk3VdVU7rhKDToykTy24WmcoegapnG8', + }, + secondary: { + more: { + deeper: { + jpeg: { + url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/0c66645c4e119f9c5def80273b768138d797f00583f557065a50bb0dd491e8e3/pfp_3.png', + }, + }, + }, + }, + }, + mp4: { + primary: { + url: 'https://nftu.com/nft-content/media/PAPAYA/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/dee8734bbefb0d63d6156b6fa0e1385822480589daa1862cbd37a94f6bc2ba3a', + cid: 'QmPGMksnyQemncHKQ67zGiuTAsnFi8HTJkY9ebQ6eVVQLv', + }, + }, + 'default': 'mp4', + webp: [ + 'QmPGMksnyQemncHKQ67zGiuTAsnFi8HTJkY9ebQ6eVVQLv', + 'https://nftu.com/nft/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/949', + { + label: 'fancy label', + data: [ + { + name: 'John', + email: 'john@foo.com', + }, + { + name: 'Mary', + email: 'mary@foo.com', + }, + [ 1, 2 ], + ], + }, + [ + { + address: 'unknown', + age: 523, + gender: 'male', + }, + { + address: 'bar', + age: 24, + gender: 'https://nftu.com/nft/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/949', + }, + ], + ], + }, + royalty_amount: 1000, + rarity: 'Premium', + set_key: 'Set', + external_url: 'https://nftu.com/nft/92ee5f5c-bce9-4d64-8a25-c7e1e6305572/949', + attributes: [ + { + value: 'NCAABB', + trait_type: 'Sport', + }, + { + value: 'Player', + trait_type: 'Type', + }, + { + value: '15', + trait_type: 'Player Jersey Number', + display_type: 'number', + }, + ], + tags: [ 'foo', 123, true ], + token_id: '7741920', + serial_total: 1100, + blockchain_state: 'BURNING', + image: 'ipfs://dee8734bbefb0d63d6156b6fa0e1385822480589daa1862cbd37a94f6bc2ba3a', + revealed_nfts: null, + nft_data_id: '92ee5f5c-bce9-4d64-8a25-c7e1e6305572', + series: 'Tip-Off', + immutable_cid: 'QmVigZH1P3D6QWvp2SWVreTPKmDvUYUidNzcUrcYzATpyJ', + status: null, + }, +}; + +export const unique: TokenInstance = { + ...base, + is_unique: true, +}; diff --git a/mocks/tokens/tokenTransfer.ts b/mocks/tokens/tokenTransfer.ts new file mode 100644 index 0000000000..b88cd499ff --- /dev/null +++ b/mocks/tokens/tokenTransfer.ts @@ -0,0 +1,243 @@ +import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer'; + +export const erc20: TokenTransfer = { + from: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51', + implementations: null, + is_contract: true, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', + circulating_market_cap: '117629601.61913824', + decimals: '18', + exchange_rate: '42', + holders: '46554', + name: 'ARIANEE', + symbol: 'ARIA', + type: 'ERC-20', + total_supply: '0', + icon_url: null, + }, + total: { + decimals: '18', + value: '31567373703130350', + }, + tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + type: 'token_transfer', + timestamp: '2022-10-10T14:34:30.000000Z', + block_hash: '1', + log_index: '1', + method: 'updateSmartAsset', +}; + +export const erc721: TokenTransfer = { + from: { + hash: '0x621C2a125ec4A6D8A7C7A655A18a2868d35eb43C', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + to: { + hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + token: { + address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '63090', + name: 'Arianee Smart-Asset', + symbol: 'AriaSA', + type: 'ERC-721', + total_supply: '0', + icon_url: null, + }, + total: { + token_id: '875879856', + }, + tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc', + type: 'token_transfer', + timestamp: '2022-10-10T14:34:30.000000Z', + block_hash: '1', + log_index: '1', + method: 'updateSmartAsset', +}; + +export const erc1155A: TokenTransfer = { + from: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address: '0xF56b7693E4212C584de4a83117f805B8E89224CB', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '1', + name: null, + symbol: 'MY_SYMBOL_IS_VERY_LONG', + type: 'ERC-1155', + total_supply: '0', + icon_url: null, + }, + total: { + token_id: '123', + value: '42', + decimals: null, + }, + tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', + type: 'token_minting', + timestamp: '2022-10-10T14:34:30.000000Z', + block_hash: '1', + log_index: '1', +}; + +export const erc1155B: TokenTransfer = { + ...erc1155A, + token: { + ...erc1155A.token, + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '12345678', value: '100000000000000000000', decimals: null }, +}; + +export const erc1155C: TokenTransfer = { + ...erc1155A, + token: { + ...erc1155A.token, + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null }, +}; + +export const erc1155D: TokenTransfer = { + ...erc1155A, + token: { + ...erc1155A.token, + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '456', value: '42', decimals: null }, +}; + +export const erc404A: TokenTransfer = { + from: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + to: { + hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + token: { + address: '0xF56b7693E4212C584de4a83117f805B8E89224CB', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '1', + name: null, + symbol: 'MY_SYMBOL_IS_VERY_LONG', + type: 'ERC-404', + total_supply: '0', + icon_url: null, + }, + total: { + value: '42000000000000000000000000', + decimals: '18', + token_id: null, + }, + tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', + type: 'token_transfer', + method: 'swap', + timestamp: '2022-10-10T14:34:30.000000Z', + block_hash: '1', + log_index: '1', +}; + +export const erc404B: TokenTransfer = { + ...erc404A, + token: { + ...erc404A.token, + name: 'SastanaNFT', + symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', + }, + total: { token_id: '4625304364899952' }, +}; + +export const mixTokens: TokenTransferResponse = { + items: [ + erc20, + erc721, + erc1155A, + erc1155B, + erc1155C, + erc1155D, + erc404A, + erc404B, + ], + next_page_params: null, +}; diff --git a/mocks/txs/decodedInputData.ts b/mocks/txs/decodedInputData.ts new file mode 100644 index 0000000000..bbb1274258 --- /dev/null +++ b/mocks/txs/decodedInputData.ts @@ -0,0 +1,49 @@ +import type { DecodedInput } from 'types/api/decodedInput'; + +export const withoutIndexedFields: DecodedInput = { + method_call: 'CreditSpended(uint256 _type, uint256 _quantity)', + method_id: '58cdf94a', + parameters: [ + { + name: '_type', + type: 'uint256', + value: '3', + }, + { + name: '_quantity', + type: 'uint256', + value: '1', + }, + ], +}; + +export const withIndexedFields: DecodedInput = { + method_call: 'Transfer(address indexed from, address indexed to, uint256 value)', + method_id: 'ddf252ad', + parameters: [ + { + indexed: true, + name: 'from', + type: 'address', + value: '0xd789a607ceac2f0e14867de4eb15b15c9ffb5859', + }, + { + indexed: true, + name: 'to', + type: 'address', + value: '0x7d20a8d54f955b4483a66ab335635ab66e151c51', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + value: '31567373703130350', + }, + { + indexed: true, + name: 'inputArray', + type: 'uint256[2][2]', + value: [ [ '1', '1' ], [ '1', '1' ] ], + }, + ], +}; diff --git a/mocks/txs/internalTxs.ts b/mocks/txs/internalTxs.ts new file mode 100644 index 0000000000..23fc4bf027 --- /dev/null +++ b/mocks/txs/internalTxs.ts @@ -0,0 +1,80 @@ +import type { InternalTransaction, InternalTransactionsResponse } from 'types/api/internalTransaction'; + +export const base: InternalTransaction = { + block: 29611822, + created_contract: null, + error: null, + from: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + gas_limit: '757586', + index: 1, + success: true, + timestamp: '2022-10-10T14:43:05.000000Z', + to: { + hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeCreditHistory', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61', + type: 'call', + value: '42000000000000000000', +}; + +export const typeStaticCall: InternalTransaction = { + ...base, + type: 'staticcall', + to: { + ...base.to, + name: null, + }, + gas_limit: '63424243', + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff62', +}; + +export const withContractCreated: InternalTransaction = { + ...base, + type: 'delegatecall', + to: null, + from: { + ...base.from, + name: null, + }, + created_contract: { + hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a', + implementations: null, + is_contract: true, + is_verified: null, + name: 'Shavuha token', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + value: '1420000000000000000', + gas_limit: '5433', + transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff63', +}; + +export const baseResponse: InternalTransactionsResponse = { + items: [ + base, + typeStaticCall, + withContractCreated, + ], + next_page_params: null, +}; diff --git a/mocks/txs/state.ts b/mocks/txs/state.ts new file mode 100644 index 0000000000..d409f4679a --- /dev/null +++ b/mocks/txs/state.ts @@ -0,0 +1,195 @@ +import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges'; + +export const mintToken: TxStateChange = { + address: { + hash: '0x0000000000000000000000000000000000000000', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: null, + balance_before: null, + change: [ + { + direction: 'from', + total: { + token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065', + }, + }, + ], + is_miner: false, + token: { + address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '9191', + name: 'ParaSpace Derivative Token MOONBIRD', + symbol: 'nMOONBIRD', + total_supply: '10645', + type: 'ERC-721', + icon_url: null, + }, + type: 'token' as const, +}; + +export const receiveMintedToken: TxStateChange = { + address: { + hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '1', + balance_before: '0', + change: [ + { + direction: 'to', + total: { + token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065', + }, + }, + ], + is_miner: false, + token: { + address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '9191', + name: 'ParaSpace Derivative Token MOONBIRD', + symbol: 'nMOONBIRD', + total_supply: '10645', + type: 'ERC-721', + icon_url: null, + }, + type: 'token' as const, +}; + +export const transfer1155Token: TxStateChange = { + address: { + hash: '0x51243E83Db20F8FC2761D894067A2A9eb7B158DE', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '1', + balance_before: '0', + change: '1', + is_miner: false, + token: { + address: '0x56Cc277717106E528A9FcC2CD342Ff98db758041', + circulating_market_cap: null, + decimals: null, + exchange_rate: null, + holders: '50413', + icon_url: null, + name: null, + symbol: null, + total_supply: null, + type: 'ERC-1155', + }, + token_id: '1', + type: 'token' as const, +}; + +export const receiveCoin: TxStateChange = { + address: { + hash: '0x8dC847Af872947Ac18d5d63fA646EB65d4D99560', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '443787514723917012805', + balance_before: '443787484997510408745', + change: '29726406604060', + is_miner: true, + token: null, + type: 'coin' as const, +}; + +export const sendCoin: TxStateChange = { + address: { + hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + balance_after: '828282622733717191', + balance_before: '832127467556437753', + change: '-3844844822720562', + is_miner: false, + token: null, + type: 'coin' as const, +}; + +export const sendERC20Token: TxStateChange = { + address: { + hash: '0x7f6479df95Aa3036a3BE02DB6300ea201ABd9981', + ens_domain_name: null, + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + balance_after: '6814903154', + balance_before: '9814903154', + change: '-3000000000', + is_miner: false, + token: { + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + circulating_market_cap: '82978861367.28714', + decimals: '6', + exchange_rate: '0.992839', + holders: null, + icon_url: 'https://gateway.tkn.xyz/ipfs/bafybeihrrubjya5nnwgqdm6mfqisxfnv76tl3yd452lkmgomn5n64gzbxu/', + name: 'Tether USD', + symbol: 'USDT', + total_supply: '39030615894320966', + type: 'ERC-20' as const, + }, + type: 'token' as const, +}; + +export const baseResponse: TxStateChanges = { + items: [ + mintToken, + receiveMintedToken, + sendCoin, + receiveCoin, + transfer1155Token, + sendERC20Token, + ], + next_page_params: { + items_count: 50, + state_changes: null, + }, +}; diff --git a/mocks/txs/stats.ts b/mocks/txs/stats.ts new file mode 100644 index 0000000000..7b05dc975a --- /dev/null +++ b/mocks/txs/stats.ts @@ -0,0 +1,8 @@ +import type { TransactionsStats } from 'types/api/transaction'; + +export const base: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/mocks/txs/tx.ts b/mocks/txs/tx.ts new file mode 100644 index 0000000000..595af11b47 --- /dev/null +++ b/mocks/txs/tx.ts @@ -0,0 +1,377 @@ +/* eslint-disable max-len */ +import type { Transaction } from 'types/api/transaction'; + +import { publicTag, privateTag, watchlistName } from 'mocks/address/tag'; +import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; +import * as decodedInputDataMock from 'mocks/txs/decodedInputData'; + +export const base: Transaction = { + base_fee_per_gas: '10000000000', + block: 29611750, + confirmation_duration: [ + 0, + 6364, + ], + confirmations: 508299, + created_contract: null, + decoded_input: decodedInputDataMock.withoutIndexedFields, + exchange_rate: '0.00254428', + fee: { + type: 'actual', + value: '7143168000000000', + }, + from: { + hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9', + implementations: null, + is_contract: false, + name: null, + is_verified: null, + private_tags: [ ], + public_tags: [ publicTag ], + watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', + }, + gas_limit: '800000', + gas_price: '48000000000', + gas_used: '148816', + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + max_fee_per_gas: '40190625000', + max_priority_fee_per_gas: '28190625000', + method: 'updateSmartAsset', + nonce: 27831, + position: 7, + priority_fee: '1299672384375000', + raw_input: '0xfa4b78b90000000000000000000000000000000000000000000000000000000005001bcfe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791000000000000000000000000a79b29ad7e0196c95b87f4663ded82fbf2e3add8', + result: 'success', + revert_reason: null, + status: 'ok', + timestamp: '2022-10-10T14:34:30.000000Z', + to: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: false, + is_verified: true, + name: null, + private_tags: [ privateTag ], + public_tags: [], + watchlist_names: [ watchlistName ], + ens_domain_name: null, + }, + token_transfers: [], + token_transfers_overflow: false, + tx_burnt_fee: '461030000000000', + tx_tag: null, + tx_types: [ + 'contract_call', + ], + type: 2, + value: '42000000000000000000', + actions: [], + has_error_in_internal_txs: false, +}; + +export const withWatchListNames: Transaction = { + ...base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194', + from: { + ...base.from, + watchlist_names: [ + { label: 'from #1', display_name: 'from utka' }, + { label: 'kitty', display_name: 'kitty kitty kitty cat where are you' }, + ], + }, + to: { + ...base.to, + watchlist_names: [ { label: 'to #1', display_name: 'to utka' } ], + } as Transaction['to'], +}; + +export const withContractCreation: Transaction = { + ...base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3195', + to: null, + created_contract: { + hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a', + implementations: null, + is_contract: true, + is_verified: null, + name: 'Shavuha token', + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + tx_types: [ + 'contract_creation', + ], +}; + +export const withTokenTransfer: Transaction = { + ...base, + hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3196', + to: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_contract: true, + is_verified: true, + name: 'ArianeeStore', + private_tags: [ privateTag ], + public_tags: [], + watchlist_names: [ watchlistName ], + ens_domain_name: null, + }, + token_transfers: [ + tokenTransferMock.erc20, + tokenTransferMock.erc721, + tokenTransferMock.erc1155A, + tokenTransferMock.erc1155B, + tokenTransferMock.erc1155C, + tokenTransferMock.erc1155D, + tokenTransferMock.erc404A, + tokenTransferMock.erc404B, + ], + token_transfers_overflow: true, + tx_types: [ + 'token_transfer', + ], +}; + +export const withDecodedRevertReason: Transaction = { + ...base, + status: 'error', + result: 'Reverted', + revert_reason: { + method_call: 'SomeCustomError(address addr, uint256 balance)', + method_id: '50289a9f', + parameters: [ + { + name: 'addr', + type: 'address', + value: '0xf26594f585de4eb0ae9de865d9053fee02ac6ef1', + }, + { + name: 'balance', + type: 'uint256', + value: '123', + }, + ], + }, +}; + +export const withRawRevertReason: Transaction = { + ...base, + status: 'error', + result: 'Reverted', + revert_reason: { + raw: '4f6e6c79206368616972706572736f6e2063616e206769766520726967687420746f20766f74652e', + }, + to: { + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + implementations: null, + is_verified: true, + is_contract: true, + name: 'Bad guy', + private_tags: [ ], + public_tags: [], + watchlist_names: [ ], + ens_domain_name: null, + }, +}; + +export const pending: Transaction = { + ...base, + base_fee_per_gas: null, + block: null, + confirmation_duration: [], + confirmations: 0, + decoded_input: null, + gas_used: null, + max_fee_per_gas: null, + max_priority_fee_per_gas: null, + method: null, + position: null, + priority_fee: null, + result: 'pending', + revert_reason: null, + status: null, + timestamp: null, + tx_burnt_fee: null, + tx_tag: null, + type: null, + value: '0', +}; + +export const withActionsUniswap: Transaction = { + ...base, + actions: [ + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '7143.488560357232097378', + amount1: '10', + symbol0: 'Ring ding ding daa baa Baa aramba baa bom baa barooumba Wh-wha-what's going on-on? Ding, ding This is the Crazy Frog Ding, ding Bem', + symbol1: 'Ether', + }, + protocol: 'uniswap_v3', + type: 'mint', + }, + { + data: { + address: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88', + ids: [ + '53699', + '53700123456', + '42', + ], + name: 'Uniswap V3: Positions NFT', + symbol: 'UNI-V3-POS', + to: '0x6d872Fb5F5B2B1f71fA9AadE159bc3976c1946B7', + }, + protocol: 'uniswap_v3', + type: 'mint_nft', + }, + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '42876.488560357232', + amount1: '345.908098203434', + symbol0: 'SHAVUHA', + symbol1: 'BOB', + }, + protocol: 'uniswap_v3', + type: 'swap', + }, + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '42', + amount1: '0.523523223232', + symbol0: 'VIC', + symbol1: 'USDT', + }, + protocol: 'uniswap_v3', + type: 'burn', + }, + { + data: { + address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE', + address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + amount0: '42', + amount1: '0.523523223232', + symbol0: 'BOB', + symbol1: 'UNI', + }, + protocol: 'uniswap_v3', + type: 'collect', + }, + ], +}; + +export const l2tx: Transaction = { + ...base, + l1_gas_price: '82702201886', + l1_fee_scalar: '1.0', + l1_gas_used: '17060', + l1_fee: '1584574188135760', +}; + +export const stabilityTx: Transaction = { + ...base, + stability_fee: { + dapp_address: { + hash: '0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + dapp_fee: '34381250000000', + token: { + address: '0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43', + circulating_market_cap: null, + decimals: '18', + exchange_rate: '123.567', + holders: '92', + icon_url: 'https://example.com/icon.png', + name: 'Stability Gas', + symbol: 'GAS', + total_supply: '10000000000000000000000000', + type: 'ERC-20', + }, + total_fee: '68762500000000', + validator_address: { + hash: '0x1432997a4058acbBe562F3c1E79738c142039044', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + validator_fee: '34381250000000', + }, +}; + +export const celoTxn: Transaction = { + ...base, + celo: { + gas_token: { + address: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', + circulating_market_cap: null, + decimals: '18', + exchange_rate: '0.42', + holders: '205738', + icon_url: 'https://example.com/icon.png', + name: 'Celo Dollar', + symbol: 'cUSD', + total_supply: '7145754483836626799435133', + type: 'ERC-20', + }, + }, +}; + +export const base2 = { + ...base, + hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + from: { + ...base.from, + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + }, +}; + +export const base3 = { + ...base, + hash: '0x12d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', + from: { + ...base.from, + hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', + }, +}; + +export const base4 = { + ...base, + hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', +}; + +export const withBlob = { + ...base, + blob_gas_price: '21518435987', + blob_gas_used: '131072', + blob_versioned_hashes: [ + '0x01a8c328b0370068aaaef49c107f70901cd79adcda81e3599a88855532122e09', + '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd1', + ], + burnt_blob_fee: '2820464441688064', + max_fee_per_blob_gas: '60000000000', + tx_types: [ 'blob_transaction' as const ], + type: 3, +}; diff --git a/mocks/txs/txInterpretation.ts b/mocks/txs/txInterpretation.ts new file mode 100644 index 0000000000..061164088d --- /dev/null +++ b/mocks/txs/txInterpretation.ts @@ -0,0 +1,46 @@ +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; + +export const txInterpretation: TxInterpretationResponse = { + data: { + summaries: [ { + summary_template: `{action_type} {amount} {token} to {to_address} on {timestamp}`, + summary_template_variables: { + action_type: { type: 'string', value: 'Transfer' }, + amount: { type: 'currency', value: '100' }, + token: { + type: 'token', + value: { + name: 'Duck', + type: 'ERC-20', + symbol: 'DUCK', + address: '0x486a3c5f34cDc4EF133f248f1C81168D78da52e8', + holders: '1152', + decimals: '18', + icon_url: null, + total_supply: '210000000000000000000000000', + exchange_rate: null, + circulating_market_cap: null, + }, + }, + to_address: { + type: 'address', + value: { + hash: '0x48c04ed5691981C42154C6167398f95e8f38a7fF', + implementations: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, + }, + }, + timestamp: { + type: 'timestamp', + value: '1687005431', + }, + }, + } ], + }, +}; diff --git a/mocks/user/profile.ts b/mocks/user/profile.ts new file mode 100644 index 0000000000..955f872e01 --- /dev/null +++ b/mocks/user/profile.ts @@ -0,0 +1,13 @@ +export const base = { + avatar: 'https://avatars.githubusercontent.com/u/22130104', + email: 'tom@ohhhh.me', + name: 'tom goriunov', + nickname: 'tom2drum', +}; + +export const withoutEmail = { + avatar: 'https://avatars.githubusercontent.com/u/22130104', + email: null, + name: 'tom goriunov', + nickname: 'tom2drum', +}; diff --git a/mocks/userOps/userOp.ts b/mocks/userOps/userOp.ts new file mode 100644 index 0000000000..e7368c20d5 --- /dev/null +++ b/mocks/userOps/userOp.ts @@ -0,0 +1,103 @@ +/* eslint-disable max-len */ +import type { UserOp } from 'types/api/userOps'; + +export const userOpData: UserOp = { + timestamp: '2024-01-19T12:42:12.000000Z', + transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a', + user_logs_start_index: 40, + fee: '187125856691380', + call_gas_limit: '26624', + gas: '258875', + status: true, + aggregator_signature: null, + block_hash: '0xff5f41ec89e5fb3dfcf103bbbd67469fed491a7dd7cffdf00bd9e3bf45d8aeab', + pre_verification_gas: '48396', + factory: null, + signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b', + verification_gas_limit: '61285', + max_fee_per_gas: '1575000898', + aggregator: null, + hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83', + gas_price: '1575000898', + user_logs_count: 1, + block_number: '10399597', + gas_used: '118810', + sender: { + ens_domain_name: null, + hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + nonce: '0x000000000000000000000000000000000000000000000000000000000000004f', + entry_point: { + ens_domain_name: null, + hash: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + sponsor_type: 'paymaster_sponsor', + raw: { + // eslint-disable-next-line max-len + call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', + call_gas_limit: '26624', + init_code: '0x', + max_fee_per_gas: '1575000898', + max_priority_fee_per_gas: '1575000898', + nonce: '79', + paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b', + pre_verification_gas: '48396', + sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b', + verification_gas_limit: '61285', + }, + max_priority_fee_per_gas: '1575000898', + revert_reason: null, + bundler: { + ens_domain_name: null, + hash: '0xd53Eb5203e367BbDD4f72338938224881Fc501Ab', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + }, + call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', + execute_call_data: '0x3cf80e6c', + decoded_call_data: { + method_call: 'execute(address dest, uint256 value, bytes func)', + method_id: 'b61d27f6', + parameters: [ + { + name: 'dest', + type: 'address', + value: '0xb0ccffd05f5a87c4c3ceffaa217900422a249915', + }, + { + name: 'value', + type: 'uint256', + value: '0', + }, + { + name: 'func', + type: 'bytes', + value: '0x3cf80e6c', + }, + ], + }, + decoded_execute_call_data: { + method_call: 'advanceEpoch()', + method_id: '3cf80e6c', + parameters: [], + }, + paymaster: { + ens_domain_name: null, + hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, +}; diff --git a/mocks/userOps/userOps.ts b/mocks/userOps/userOps.ts new file mode 100644 index 0000000000..9a44e62594 --- /dev/null +++ b/mocks/userOps/userOps.ts @@ -0,0 +1,58 @@ +import type { UserOpsResponse } from 'types/api/userOps'; + +export const userOpsData: UserOpsResponse = { + items: [ + { + address: { + ens_domain_name: null, + hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399597', + fee: '187125856691380', + hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83', + status: true, + timestamp: '2022-01-19T12:42:12.000000Z', + transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a', + }, + { + address: + { ens_domain_name: null, + hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399596', + fee: '381895502291373', + hash: '0xcb945ae86608bdc88c3318245403c81a880fcb1e49fef18ac59477761c056cea', + status: false, + timestamp: '2022-01-19T12:42:00.000000Z', + transaction_hash: '0x558d699e7cbc235461d48ed04b8c3892d598a4000f20851760d00dc3513c2e48', + }, + { + address: { + ens_domain_name: null, + hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2', + implementations: null, + is_contract: true, + is_verified: null, + name: null, + }, + block_number: '10399560', + fee: '165019501210143', + hash: '0x84c1270b12af3f0ffa204071f1bf503ebf9b1ccf6310680383be5a2b6fd1d8e5', + status: true, + timestamp: '2022-01-19T12:32:00.000000Z', + transaction_hash: '0xc4c1c38680ec63139411aa2193275e8de44be15217c4256db9473bf0ea2b6264', + }, + ], + next_page_params: { + page_size: 50, + page_token: '10396582,0x9bf4d2a28813c5c244884cb20cdfe01dabb3f927234ae961eab6e38502de7a28', + }, +}; diff --git a/mocks/validators/index.ts b/mocks/validators/index.ts new file mode 100644 index 0000000000..22081cae8c --- /dev/null +++ b/mocks/validators/index.ts @@ -0,0 +1,33 @@ +import type { Validator, ValidatorsCountersResponse, ValidatorsResponse } from 'types/api/validators'; + +import * as addressMock from '../address/address'; + +export const validator1: Validator = { + address: addressMock.withName, + blocks_validated_count: 7334224, + state: 'active', +}; + +export const validator2: Validator = { + address: addressMock.withEns, + blocks_validated_count: 8937453, + state: 'probation', +}; + +export const validator3: Validator = { + address: addressMock.withoutName, + blocks_validated_count: 1234, + state: 'inactive', +}; + +export const validatorsResponse: ValidatorsResponse = { + items: [ validator1, validator2, validator3 ], + next_page_params: null, +}; + +export const validatorsCountersResponse: ValidatorsCountersResponse = { + active_validators_counter: '42', + active_validators_percentage: 7.14, + new_validators_counter_24h: '11', + validators_counter: '140', +}; diff --git a/mocks/withdrawals/withdrawals.ts b/mocks/withdrawals/withdrawals.ts new file mode 100644 index 0000000000..97742fe3d6 --- /dev/null +++ b/mocks/withdrawals/withdrawals.ts @@ -0,0 +1,53 @@ +import type { AddressParam } from 'types/api/addressParams'; +import type { WithdrawalsResponse } from 'types/api/withdrawals'; + +export const data: WithdrawalsResponse = { + items: [ + { + amount: '192175000000000', + block_number: 43242, + index: 11688, + receiver: { + hash: '0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + } as AddressParam, + timestamp: '2022-06-07T18:12:24.000000Z', + validator_index: 49622, + }, + { + amount: '192175000000000', + block_number: 43242, + index: 11687, + receiver: { + hash: '0xf97e987c050e5Ab072211Ad2C213Eb5AEE4DF134', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + } as AddressParam, + timestamp: '2022-05-07T18:12:24.000000Z', + validator_index: 49621, + }, + { + amount: '182773000000000', + block_number: 43242, + index: 11686, + receiver: { + hash: '0xf97e123c050e5Ab072211Ad2C213Eb5AEE4DF134', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + } as AddressParam, + timestamp: '2022-04-07T18:12:24.000000Z', + validator_index: 49620, + }, + ], + next_page_params: { + index: 11639, + items_count: 50, + }, +}; diff --git a/mocks/zkEvm/deposits.ts b/mocks/zkEvm/deposits.ts new file mode 100644 index 0000000000..91ecc077c3 --- /dev/null +++ b/mocks/zkEvm/deposits.ts @@ -0,0 +1,28 @@ +import type { ZkEvmL2DepositsResponse } from 'types/api/zkEvmL2'; + +export const baseResponse: ZkEvmL2DepositsResponse = { + items: [ + { + block_number: 19681943, + index: 182177, + l1_transaction_hash: '0x29074452f976064aca1ca5c6e7c82d890c10454280693e6eca0257ae000c8e85', + l2_transaction_hash: null, + symbol: 'DAI', + timestamp: '2022-04-18T11:08:11.000000Z', + value: '0.003', + }, + { + block_number: 19681894, + index: 182176, + l1_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e', + l2_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436', + symbol: 'ETH', + timestamp: '2022-04-18T10:58:23.000000Z', + value: '0.0046651390188845', + }, + ], + next_page_params: { + items_count: 50, + index: 1, + }, +}; diff --git a/mocks/zkEvm/txnBatches.ts b/mocks/zkEvm/txnBatches.ts new file mode 100644 index 0000000000..bcaf55d941 --- /dev/null +++ b/mocks/zkEvm/txnBatches.ts @@ -0,0 +1,40 @@ +import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2'; + +export const txnBatchData: ZkEvmL2TxnBatch = { + acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3', + global_exit_root: '0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5', + number: 5, + sequence_tx_hash: '0x7ae010e9758441b302db10282807358af460f38c49c618d26a897592f64977f7', + state_root: '0x183b4a38a4a6027947ceb93b323cc94c548c8c05cf605d73ca88351d77cae1a3', + status: 'Finalized', + timestamp: '2023-10-20T10:08:18.000000Z', + transactions: [ + '0xb5d432c270057c223b973f3b5f00dbad32823d9ef26f3e8d97c819c7c573453a', + ], + verify_tx_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f', +}; + +export const txnBatchesData: ZkEvmL2TxnBatchesResponse = { + items: [ + { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Finalized', + verify_tx_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8', + sequence_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b', + number: 5218590, + tx_count: 9, + }, + { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Unfinalized', + verify_tx_hash: null, + sequence_tx_hash: null, + number: 5218591, + tx_count: 9, + }, + ], + next_page_params: { + number: 5902834, + items_count: 50, + }, +}; diff --git a/mocks/zkEvm/withdrawals.ts b/mocks/zkEvm/withdrawals.ts new file mode 100644 index 0000000000..c89635f4e5 --- /dev/null +++ b/mocks/zkEvm/withdrawals.ts @@ -0,0 +1,28 @@ +import type { ZkEvmL2WithdrawalsResponse } from 'types/api/zkEvmL2'; + +export const baseResponse: ZkEvmL2WithdrawalsResponse = { + items: [ + { + block_number: 11722417, + index: 47040, + l1_transaction_hash: null, + l2_transaction_hash: '0x68c378e412e51553524545ef1d3f00f69496fb37827c0b3b7e0870d245970408', + symbol: 'ETH', + timestamp: '2022-04-18T09:20:37.000000Z', + value: '0.025', + }, + { + block_number: 11722480, + index: 47041, + l1_transaction_hash: '0xbf76feb85b8b8f24dacb17f962dd359f82efc512928d7b11ffca92fb812ad6a5', + l2_transaction_hash: '0xfe3c168ac1751b8399f1e819f1d83ee4cf764128bc604d454abee29114dabf49', + symbol: 'ETH', + timestamp: '2022-04-18T09:23:45.000000Z', + value: '4', + }, + ], + next_page_params: { + items_count: 50, + index: 1, + }, +}; diff --git a/mocks/zkSync/zkSyncTxnBatch.ts b/mocks/zkSync/zkSyncTxnBatch.ts new file mode 100644 index 0000000000..ab865dc513 --- /dev/null +++ b/mocks/zkSync/zkSyncTxnBatch.ts @@ -0,0 +1,20 @@ +import type { ZkSyncBatch } from 'types/api/zkSyncL2'; + +export const base: ZkSyncBatch = { + commit_transaction_hash: '0x7cd80c88977c2b310f79196b0b2136da18012be015ce80d0d9e9fe6cfad52b16', + commit_transaction_timestamp: '2022-03-19T09:37:38.726996Z', + end_block: 1245490, + execute_transaction_hash: '0x110b9a19afbabd5818a996ab2b493a9b23c888d73d95f1ab5272dbae503e103a', + execute_transaction_timestamp: '2022-03-19T10:29:05.358066Z', + l1_gas_price: '4173068062', + l1_tx_count: 0, + l2_fair_gas_price: '100000000', + l2_tx_count: 287, + number: 8051, + prove_transaction_hash: '0xb424162ba5afe17c710dceb5fc8d15d7d46a66223454dae8c74aa39f6802625b', + prove_transaction_timestamp: '2022-03-19T10:29:05.279179Z', + root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792', + start_block: 1245209, + status: 'Executed on L1', + timestamp: '2022-03-19T09:05:49.000000Z', +}; diff --git a/mocks/zkSync/zkSyncTxnBatches.ts b/mocks/zkSync/zkSyncTxnBatches.ts new file mode 100644 index 0000000000..a717308641 --- /dev/null +++ b/mocks/zkSync/zkSyncTxnBatches.ts @@ -0,0 +1,49 @@ +import type { ZkSyncBatchesItem, ZkSyncBatchesResponse } from 'types/api/zkSyncL2'; + +export const sealed: ZkSyncBatchesItem = { + commit_transaction_hash: null, + commit_transaction_timestamp: null, + execute_transaction_hash: null, + execute_transaction_timestamp: null, + number: 8055, + prove_transaction_hash: null, + prove_transaction_timestamp: null, + status: 'Sealed on L2', + timestamp: '2022-03-19T12:53:36.000000Z', + tx_count: 738, +}; + +export const sent: ZkSyncBatchesItem = { + commit_transaction_hash: '0x262e7215739d6a7e33b2c20b45a838801a0f5f080f20bec8e54eb078420c4661', + commit_transaction_timestamp: '2022-03-19T13:09:07.357570Z', + execute_transaction_hash: null, + execute_transaction_timestamp: null, + number: 8054, + prove_transaction_hash: null, + prove_transaction_timestamp: null, + status: 'Sent to L1', + timestamp: '2022-03-19T11:36:45.000000Z', + tx_count: 766, +}; + +export const executed: ZkSyncBatchesItem = { + commit_transaction_hash: '0xa2628f477e1027ac1c60fa75c186b914647769ac1cb9c7e1cab50b13506a0035', + commit_transaction_timestamp: '2022-03-19T11:52:18.963659Z', + execute_transaction_hash: '0xb7bd6b2b17498c66d3f6e31ac3685133a81b7f728d4f6a6f42741daa257d0d68', + execute_transaction_timestamp: '2022-03-19T13:28:16.712656Z', + number: 8053, + prove_transaction_hash: '0x9d44f2b775bd771f8a53205755b3897929aa672d2cd419b3b988c16d41d4f21e', + prove_transaction_timestamp: '2022-03-19T13:28:16.603104Z', + status: 'Executed on L1', + timestamp: '2022-03-19T10:01:52.000000Z', + tx_count: 1071, +}; + +export const baseResponse: ZkSyncBatchesResponse = { + items: [ + sealed, + sent, + executed, + ], + next_page_params: null, +}; diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000000..4f11a03dc6 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000000..f1c2e19694 --- /dev/null +++ b/next.config.js @@ -0,0 +1,62 @@ +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.BUNDLE_ANALYZER === 'true', +}); + +const withRoutes = require('nextjs-routes/config')({ + outDir: 'nextjs', +}); + +const headers = require('./nextjs/headers'); +const redirects = require('./nextjs/redirects'); +const rewrites = require('./nextjs/rewrites'); + +/** @type {import('next').NextConfig} */ +const moduleExports = { + transpilePackages: [ + 'react-syntax-highlighter', + 'swagger-client', + 'swagger-ui-react', + ], + reactStrictMode: true, + webpack(config, { webpack }) { + config.plugins.push( + new webpack.DefinePlugin({ + __SENTRY_DEBUG__: false, + __SENTRY_TRACING__: false, + }), + ); + config.module.rules.push( + { + test: /\.svg$/, + use: [ '@svgr/webpack' ], + }, + ); + config.resolve.fallback = { fs: false, net: false, tls: false }; + config.externals.push('pino-pretty', 'lokijs', 'encoding'); + + return config; + }, + // NOTE: all config functions should be static and not depend on any environment variables + // since all variables will be passed to the app only at runtime and there is now way to change Next.js config at this time + // if you are stuck and strongly believe what you need some sort of flexibility here please fill free to join the discussion + // https://github.com/blockscout/frontend/discussions/167 + rewrites, + redirects, + headers, + output: 'standalone', + productionBrowserSourceMaps: true, + experimental: { + instrumentationHook: process.env.NEXT_OPEN_TELEMETRY_ENABLED === 'true', + // disabled as it is not stable yet + // turbo: { + // rules: { + // '*.svg': { + // loaders: [ '@svgr/webpack' ], + // as: '*.js', + // }, + // }, + // }, + }, +}; + +module.exports = withBundleAnalyzer(withRoutes(moduleExports)); diff --git a/nextjs/PageNextJs.tsx b/nextjs/PageNextJs.tsx new file mode 100644 index 0000000000..89e066ca6d --- /dev/null +++ b/nextjs/PageNextJs.tsx @@ -0,0 +1,57 @@ +import Head from 'next/head'; +import React from 'react'; + +import type { Route } from 'nextjs-routes'; +import type { Props as PageProps } from 'nextjs/getServerSideProps'; + +import config from 'configs/app'; +import useAdblockDetect from 'lib/hooks/useAdblockDetect'; +import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; +import * as metadata from 'lib/metadata'; +import * as mixpanel from 'lib/mixpanel'; +import { init as initSentry } from 'lib/sentry/config'; + +interface Props { + pathname: Pathname; + children: React.ReactNode; + query?: PageProps['query']; + apiData?: PageProps['apiData']; +} + +initSentry(); + +const PageNextJs = (props: Props) => { + const { title, description, opengraph, canonical } = metadata.generate(props, props.apiData); + + useGetCsrfToken(); + useAdblockDetect(); + + const isMixpanelInited = mixpanel.useInit(); + mixpanel.useLogPageView(isMixpanelInited); + + return ( + <> + + { title } + + { canonical && } + + { /* OG TAGS */ } + + { opengraph.description && } + + + + { /* Twitter Meta Tags */ } + + + + { opengraph.description && } + + + { props.children } + + ); +}; + +export default React.memo(PageNextJs); diff --git a/nextjs/csp/generateCspPolicy.ts b/nextjs/csp/generateCspPolicy.ts new file mode 100644 index 0000000000..758612f3f6 --- /dev/null +++ b/nextjs/csp/generateCspPolicy.ts @@ -0,0 +1,24 @@ +import * as descriptors from './policies'; +import { makePolicyString, mergeDescriptors } from './utils'; + +function generateCspPolicy() { + const policyDescriptor = mergeDescriptors( + descriptors.app(), + descriptors.ad(), + descriptors.cloudFlare(), + descriptors.googleAnalytics(), + descriptors.googleFonts(), + descriptors.googleReCaptcha(), + descriptors.growthBook(), + descriptors.marketplace(), + descriptors.mixpanel(), + descriptors.monaco(), + descriptors.safe(), + descriptors.sentry(), + descriptors.walletConnect(), + ); + + return makePolicyString(policyDescriptor); +} + +export default generateCspPolicy; diff --git a/nextjs/csp/policies/ad.ts b/nextjs/csp/policies/ad.ts new file mode 100644 index 0000000000..9b018aa019 --- /dev/null +++ b/nextjs/csp/policies/ad.ts @@ -0,0 +1,65 @@ +import Base64 from 'crypto-js/enc-base64'; +import sha256 from 'crypto-js/sha256'; +import type CspDev from 'csp-dev'; + +import { connectAdbutler, placeAd } from 'ui/shared/ad/adbutlerScript'; +import { hypeInit } from 'ui/shared/ad/hypeBannerScript'; + +export function ad(): CspDev.DirectiveDescriptor { + return { + 'connect-src': [ + // coinzilla + 'coinzilla.com', + '*.coinzilla.com', + 'https://request-global.czilladx.com', + + // adbutler + 'servedbyadbutler.com', + + // slise + '*.slise.xyz', + + // hype + 'api.hypelab.com', + '*.ixncdn.com', + '*.cloudfront.net', + + //getit + 'v1.getittech.io', + 'ipapi.co', + ], + 'frame-src': [ + // coinzilla + 'https://request-global.czilladx.com', + ], + 'script-src': [ + // coinzilla + 'coinzillatag.com', + + // adbutler + 'servedbyadbutler.com', + `'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`, + `'sha256-${ Base64.stringify(sha256(placeAd(undefined) ?? '')) }'`, + `'sha256-${ Base64.stringify(sha256(placeAd('mobile') ?? '')) }'`, + + // slise + '*.slise.xyz', + + //hype + `'sha256-${ Base64.stringify(sha256(hypeInit ?? '')) }'`, + 'https://api.hypelab.com', + 'd1q98dzwj6s2rb.cloudfront.net', + ], + 'img-src': [ + // coinzilla + 'cdn.coinzilla.io', + + // adbutler + 'servedbyadbutler.com', + ], + 'font-src': [ + // coinzilla + 'https://request-global.czilladx.com', + ], + }; +} diff --git a/nextjs/csp/policies/app.ts b/nextjs/csp/policies/app.ts new file mode 100644 index 0000000000..cadb26363e --- /dev/null +++ b/nextjs/csp/policies/app.ts @@ -0,0 +1,149 @@ +import type CspDev from 'csp-dev'; + +import { getFeaturePayload } from 'configs/app/features/types'; + +import config from 'configs/app'; + +import { KEY_WORDS } from '../utils'; + +const MAIN_DOMAINS = [ + `*.${ config.app.host }`, + config.app.host, +].filter(Boolean); + +const getCspReportUrl = () => { + try { + const sentryFeature = config.features.sentry; + if (!sentryFeature.isEnabled || !process.env.SENTRY_CSP_REPORT_URI) { + return; + } + + const url = new URL(process.env.SENTRY_CSP_REPORT_URI); + + // https://docs.sentry.io/product/security-policy-reporting/#additional-configuration + url.searchParams.set('sentry_environment', sentryFeature.environment); + sentryFeature.release && url.searchParams.set('sentry_release', sentryFeature.release); + + return url.toString(); + } catch (error) { + return; + } +}; + +export function app(): CspDev.DirectiveDescriptor { + return { + 'default-src': [ + // KEY_WORDS.NONE, + // https://bugzilla.mozilla.org/show_bug.cgi?id=1242902 + // need 'self' here to avoid an error with prefetch nextjs chunks in firefox + KEY_WORDS.SELF, + ], + + 'connect-src': [ + KEY_WORDS.SELF, + ...MAIN_DOMAINS, + + // webpack hmr in safari doesn't recognize localhost as 'self' for some reason + config.app.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '', + + // APIs + config.api.endpoint, + config.api.socket, + getFeaturePayload(config.features.stats)?.api.endpoint, + getFeaturePayload(config.features.sol2uml)?.api.endpoint, + getFeaturePayload(config.features.verifiedTokens)?.api.endpoint, + getFeaturePayload(config.features.addressVerification)?.api.endpoint, + getFeaturePayload(config.features.nameService)?.api.endpoint, + getFeaturePayload(config.features.addressMetadata)?.api.endpoint, + + // chain RPC server + config.chain.rpcUrl, + 'https://infragrid.v.network', // RPC providers + + // github (spec for api-docs page) + 'raw.githubusercontent.com', + ].filter(Boolean), + + 'script-src': [ + KEY_WORDS.SELF, + ...MAIN_DOMAINS, + + // next.js generates and rebuilds source maps in dev using eval() + // https://github.com/vercel/next.js/issues/14221#issuecomment-657258278 + config.app.isDev ? KEY_WORDS.UNSAFE_EVAL : '', + + // hash of ColorModeScript + '\'sha256-e7MRMmTzLsLQvIy1iizO1lXf7VWYoQ6ysj5fuUzvRwE=\'', + ], + + 'style-src': [ + KEY_WORDS.SELF, + ...MAIN_DOMAINS, + + // yes, it is unsafe as it stands, but + // - we cannot use hashes because all styles are generated dynamically + // - we cannot use nonces since we are not following along SSR path + // - and still there is very small damage that can be cause by CSS-based XSS-attacks + // so we hope we are fine here till the first major incident :) + KEY_WORDS.UNSAFE_INLINE, + ], + + 'img-src': [ + KEY_WORDS.SELF, + KEY_WORDS.DATA, + ...MAIN_DOMAINS, + + // we agreed that using wildcard for images is mostly safe + // why do we have to use it? the main reason is that for NFT and inventory pages we get resources urls from API only on the client + // so they cannot be added to the policy on the server + // there could be 3 possible workarounds + // a/ use server side rendering approach, that we don't want to do + // b/ wrap every image/video in iframe with a source to static page for which we enforce certain img-src rule; + // the downsides is page performance slowdown and code complexity (have to manage click on elements, color mode for + // embedded page, etc) + // c/ use wildcard for img-src directive; this can lead to some security vulnerabilities but we were unable to find evidence + // that loose img-src directive alone could cause serious flaws on the site as long as we keep script-src and connect-src strict + // + // feel free to propose alternative solution and fix this + '*', + ], + + 'media-src': [ + '*', // see comment for img-src directive + ], + + 'font-src': [ + KEY_WORDS.DATA, + ...MAIN_DOMAINS, + ], + + 'object-src': [ + KEY_WORDS.NONE, + ], + + 'base-uri': [ + KEY_WORDS.NONE, + ], + + 'frame-src': [ + // could be a marketplace app or NFT media (html-page) + '*', + ], + + 'frame-ancestors': [ + KEY_WORDS.SELF, + ], + + ...((() => { + if (!config.features.sentry.isEnabled) { + return {}; + } + + return { + 'report-uri': [ + getCspReportUrl(), + ].filter(Boolean), + }; + })()), + }; +} diff --git a/nextjs/csp/policies/cloudFlare.ts b/nextjs/csp/policies/cloudFlare.ts new file mode 100644 index 0000000000..e86085042f --- /dev/null +++ b/nextjs/csp/policies/cloudFlare.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import { KEY_WORDS } from '../utils'; + +// CloudFlare analytics +export function cloudFlare(): CspDev.DirectiveDescriptor { + return { + 'script-src': [ + 'static.cloudflareinsights.com', + ], + 'style-src': [ + KEY_WORDS.DATA, + ], + }; +} diff --git a/nextjs/csp/policies/googleAnalytics.ts b/nextjs/csp/policies/googleAnalytics.ts new file mode 100644 index 0000000000..a6d3f7b84a --- /dev/null +++ b/nextjs/csp/policies/googleAnalytics.ts @@ -0,0 +1,29 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function googleAnalytics(): CspDev.DirectiveDescriptor { + if (!config.features.googleAnalytics.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.google-analytics.com', + '*.analytics.google.com', + 'https://www.googletagmanager.com', + 'https://stats.g.doubleclick.net', + ], + 'script-src': [ + // inline script hash, see ui/shared/GoogleAnalytics.tsx + '\'sha256-WXRwCtfSfMoCPzPUIOUAosSaADdGgct0/Lhmnbm7MCA=\'', + 'https://www.googletagmanager.com', + '*.google-analytics.com', + '*.analytics.google.com', + ], + 'img-src': [ + '*.google-analytics.com', + '*.analytics.google.com', + ], + }; +} diff --git a/nextjs/csp/policies/googleFonts.ts b/nextjs/csp/policies/googleFonts.ts new file mode 100644 index 0000000000..f79b0d523d --- /dev/null +++ b/nextjs/csp/policies/googleFonts.ts @@ -0,0 +1,18 @@ +import type CspDev from 'csp-dev'; + +export function googleFonts(): CspDev.DirectiveDescriptor { + // we use Inter and Poppins in the app + + return { + 'connect-src': [ + 'fonts.gstatic.com', + ], + 'style-src': [ + 'fonts.googleapis.com', + ], + 'font-src': [ + 'fonts.gstatic.com', + 'fonts.googleapis.com', + ], + }; +} diff --git a/nextjs/csp/policies/googleReCaptcha.ts b/nextjs/csp/policies/googleReCaptcha.ts new file mode 100644 index 0000000000..d759e70d59 --- /dev/null +++ b/nextjs/csp/policies/googleReCaptcha.ts @@ -0,0 +1,29 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function googleReCaptcha(): CspDev.DirectiveDescriptor { + if (!config.services.reCaptcha.siteKey) { + return {}; + } + + return { + 'script-src': [ + 'https://www.google.com/recaptcha/api.js', + 'https://www.gstatic.com', + 'https://translate.google.com', + '\'sha256-FDyPg8CqqIpPAfGVKx1YeKduyLs0ghNYWII21wL+7HM=\'', + ], + 'style-src': [ + 'https://www.gstatic.com', + ], + 'img-src': [ + 'https://translate.google.com', + 'https://www.gstatic.com', + ], + 'frame-src': [ + 'https://www.google.com/recaptcha/api2/anchor', + 'https://www.google.com/recaptcha/api2/bframe', + ], + }; +} diff --git a/nextjs/csp/policies/growthBook.ts b/nextjs/csp/policies/growthBook.ts new file mode 100644 index 0000000000..ae0f055fb8 --- /dev/null +++ b/nextjs/csp/policies/growthBook.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function growthBook(): CspDev.DirectiveDescriptor { + if (!config.features.growthBook.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + 'cdn.growthbook.io', + ], + }; +} diff --git a/nextjs/csp/policies/index.ts b/nextjs/csp/policies/index.ts new file mode 100644 index 0000000000..1cbe44f1bc --- /dev/null +++ b/nextjs/csp/policies/index.ts @@ -0,0 +1,13 @@ +export { ad } from './ad'; +export { app } from './app'; +export { cloudFlare } from './cloudFlare'; +export { googleAnalytics } from './googleAnalytics'; +export { googleFonts } from './googleFonts'; +export { googleReCaptcha } from './googleReCaptcha'; +export { growthBook } from './growthBook'; +export { marketplace } from './marketplace'; +export { mixpanel } from './mixpanel'; +export { monaco } from './monaco'; +export { safe } from './safe'; +export { sentry } from './sentry'; +export { walletConnect } from './walletConnect'; diff --git a/nextjs/csp/policies/marketplace.ts b/nextjs/csp/policies/marketplace.ts new file mode 100644 index 0000000000..08474a4bc1 --- /dev/null +++ b/nextjs/csp/policies/marketplace.ts @@ -0,0 +1,22 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +const feature = config.features.marketplace; + +export function marketplace(): CspDev.DirectiveDescriptor { + if (!feature.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + 'api' in feature ? feature.api.endpoint : '', + feature.rating ? 'https://api.airtable.com' : '', + ], + + 'frame-src': [ + '*', + ], + }; +} diff --git a/nextjs/csp/policies/mixpanel.ts b/nextjs/csp/policies/mixpanel.ts new file mode 100644 index 0000000000..0e2d8fee31 --- /dev/null +++ b/nextjs/csp/policies/mixpanel.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function mixpanel(): CspDev.DirectiveDescriptor { + if (!config.features.mixpanel.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.mixpanel.com', + ], + }; +} diff --git a/nextjs/csp/policies/monaco.ts b/nextjs/csp/policies/monaco.ts new file mode 100644 index 0000000000..edf8ac5f1c --- /dev/null +++ b/nextjs/csp/policies/monaco.ts @@ -0,0 +1,23 @@ +import type CspDev from 'csp-dev'; + +import { KEY_WORDS } from '../utils'; + +export function monaco(): CspDev.DirectiveDescriptor { + return { + 'script-src': [ + KEY_WORDS.BLOB, + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/loader.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.nls.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/solidity/solidity.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/elixir/elixir.js', + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/worker/workerMain.js', + ], + 'style-src': [ + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.css', + ], + 'font-src': [ + 'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/browser/ui/codicons/codicon/codicon.ttf', + ], + }; +} diff --git a/nextjs/csp/policies/safe.ts b/nextjs/csp/policies/safe.ts new file mode 100644 index 0000000000..c7c9c05e27 --- /dev/null +++ b/nextjs/csp/policies/safe.ts @@ -0,0 +1,15 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +export function safe(): CspDev.DirectiveDescriptor { + if (!config.features.safe.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.safe.global', + ], + }; +} diff --git a/nextjs/csp/policies/sentry.ts b/nextjs/csp/policies/sentry.ts new file mode 100644 index 0000000000..298c17a63e --- /dev/null +++ b/nextjs/csp/policies/sentry.ts @@ -0,0 +1,10 @@ +import type CspDev from 'csp-dev'; + +export function sentry(): CspDev.DirectiveDescriptor { + return { + 'connect-src': [ + 'sentry.io', + '*.sentry.io', + ], + }; +} diff --git a/nextjs/csp/policies/walletConnect.ts b/nextjs/csp/policies/walletConnect.ts new file mode 100644 index 0000000000..e4c0c0e3fa --- /dev/null +++ b/nextjs/csp/policies/walletConnect.ts @@ -0,0 +1,28 @@ +import type CspDev from 'csp-dev'; + +import config from 'configs/app'; + +import { KEY_WORDS } from '../utils'; + +export function walletConnect(): CspDev.DirectiveDescriptor { + if (!config.features.blockchainInteraction.isEnabled) { + return {}; + } + + return { + 'connect-src': [ + '*.web3modal.com', + '*.walletconnect.com', + 'wss://relay.walletconnect.com', + 'wss://www.walletlink.org', + ], + 'frame-ancestors': [ + '*.walletconnect.org', + '*.walletconnect.com', + ], + 'img-src': [ + KEY_WORDS.BLOB, + '*.walletconnect.com', + ], + }; +} diff --git a/nextjs/csp/utils.ts b/nextjs/csp/utils.ts new file mode 100644 index 0000000000..cdc9a0fdd7 --- /dev/null +++ b/nextjs/csp/utils.ts @@ -0,0 +1,58 @@ +import type CspDev from 'csp-dev'; + +export const KEY_WORDS = { + BLOB: 'blob:', + DATA: 'data:', + NONE: '\'none\'', + REPORT_SAMPLE: `'report-sample'`, + SELF: '\'self\'', + STRICT_DYNAMIC: `'strict-dynamic'`, + UNSAFE_INLINE: '\'unsafe-inline\'', + UNSAFE_EVAL: '\'unsafe-eval\'', +}; + +// we cannot use lodash/uniq and lodash/mergeWith in middleware code since it calls new Set() and it'is causing an error in Next.js +// "Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime" +export function unique(array: Array) { + const set: Record = {}; + for (const item of array) { + item && (set[item] = true); + } + + return Object.keys(set); +} + +export function mergeDescriptors(...descriptors: Array) { + return descriptors.reduce((result, item) => { + for (const _key in item) { + const key = _key as CspDev.Directive; + const value = item[key]; + + if (!value) { + continue; + } + + if (result[key]) { + result[key]?.push(...value); + } else { + result[key] = [ ...value ]; + } + } + + return result; + }, {} as CspDev.DirectiveDescriptor); +} + +export function makePolicyString(policyDescriptor: CspDev.DirectiveDescriptor) { + return Object.entries(policyDescriptor) + .map(([ key, value ]) => { + if (!value || value.length === 0) { + return; + } + + const uniqueValues = unique(value); + return [ key, uniqueValues.join(' ') ].join(' '); + }) + .filter(Boolean) + .join(';'); +} diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts new file mode 100644 index 0000000000..05733b177b --- /dev/null +++ b/nextjs/getServerSideProps.ts @@ -0,0 +1,297 @@ +import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; + +import type { AdBannerProviders } from 'types/client/adProviders'; +import type { RollupType } from 'types/client/rollup'; + +import type { Route } from 'nextjs-routes'; + +import config from 'configs/app'; +import isNeedProxy from 'lib/api/isNeedProxy'; +const rollupFeature = config.features.rollup; +const adBannerFeature = config.features.adsBanner; +import type * as metadata from 'lib/metadata'; + +export interface Props { + query: Route['query']; + cookies: string; + referrer: string; + adBannerProvider: AdBannerProviders | null; + // if apiData is undefined, Next.js will complain that it is not serializable + // so we force it to be always present in the props but it can be null + apiData: metadata.ApiData | null; +} + +export const base = async ({ req, query }: GetServerSidePropsContext): +Promise>> => { + const adBannerProvider = (() => { + if (adBannerFeature.isEnabled) { + if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) { + // we need to get a random ad provider on the server side to keep it consistent with the client side + const randomIndex = Math.round(Math.random()); + return [ adBannerFeature.provider, adBannerFeature.additionalProvider ][randomIndex]; + } else { + return adBannerFeature.provider; + } + } + return null; + })(); + + return { + props: { + query, + cookies: req.headers.cookie || '', + referrer: req.headers.referer || '', + adBannerProvider: adBannerProvider, + apiData: null, + }, + }; +}; + +export const account: GetServerSideProps = async(context) => { + if (!config.features.account.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const verifiedAddresses: GetServerSideProps = async(context) => { + if (!config.features.addressVerification.isEnabled) { + return { + notFound: true, + }; + } + + return account(context); +}; + +const DEPOSITS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ]; +export const deposits: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && DEPOSITS_ROLLUP_TYPES.includes(rollupFeature.type))) { + return { + notFound: true, + }; + } + + return base(context); +}; + +const WITHDRAWALS_ROLLUP_TYPES: Array = [ 'optimistic', 'shibarium', 'zkEvm', 'arbitrum' ]; +export const withdrawals: GetServerSideProps = async(context) => { + if ( + !config.features.beaconChain.isEnabled && + !(rollupFeature.isEnabled && WITHDRAWALS_ROLLUP_TYPES.includes(rollupFeature.type)) + ) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const rollup: GetServerSideProps = async(context) => { + if (!config.features.rollup.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const optimisticRollup: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && rollupFeature.type === 'optimistic')) { + return { + notFound: true, + }; + } + + return base(context); +}; + +const BATCH_ROLLUP_TYPES: Array = [ 'zkEvm', 'zkSync', 'arbitrum' ]; +export const batch: GetServerSideProps = async(context) => { + if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const marketplace = async (context: GetServerSidePropsContext): +Promise>> => { + if (!config.features.marketplace.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const apiDocs: GetServerSideProps = async(context) => { + if (!config.features.restApiDocs.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const graphIQl: GetServerSideProps = async(context) => { + if (!config.features.graphqlApiDocs.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const csvExport: GetServerSideProps = async(context) => { + if (!config.features.csvExport.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const stats: GetServerSideProps = async(context) => { + if (!config.features.stats.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const suave: GetServerSideProps = async(context) => { + if (!config.features.suave.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const nameService: GetServerSideProps = async(context) => { + if (!config.features.nameService.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const accounts: GetServerSideProps = async(context) => { + if (config.UI.views.address.hiddenViews?.top_accounts) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const userOps: GetServerSideProps = async(context) => { + if (!config.features.userOps.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const validators: GetServerSideProps = async(context) => { + if (!config.features.validators.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const gasTracker: GetServerSideProps = async(context) => { + if (!config.features.gasTracker.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const dataAvailability: GetServerSideProps = async(context) => { + if (!config.features.dataAvailability.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const login: GetServerSideProps = async(context) => { + + if (!isNeedProxy()) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const dev: GetServerSideProps = async(context) => { + if (!config.app.isDev) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const publicTagsSubmit: GetServerSideProps = async(context) => { + + if (!config.features.publicTagsSubmission.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const disputeGames: GetServerSideProps = async(context) => { + if (!config.features.faultProofSystem.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + +export const mud: GetServerSideProps = async(context) => { + if (!config.features.mudFramework.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; diff --git a/nextjs/headers.js b/nextjs/headers.js new file mode 100644 index 0000000000..c38f294879 --- /dev/null +++ b/nextjs/headers.js @@ -0,0 +1,36 @@ +async function headers() { + return [ + { + source: '/:path*', + headers: [ + // security headers from here - https://nextjs.org/docs/advanced-features/security-headers + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + { + key: 'Referrer-Policy', + value: 'origin-when-cross-origin', + }, + ], + }, + ]; +} + +module.exports = headers; diff --git a/nextjs/middlewares/account.ts b/nextjs/middlewares/account.ts new file mode 100644 index 0000000000..e46c5a07fe --- /dev/null +++ b/nextjs/middlewares/account.ts @@ -0,0 +1,61 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import { DAY } from 'lib/consts'; +import * as cookies from 'lib/cookies'; + +export function account(req: NextRequest) { + const feature = config.features.account; + if (!feature.isEnabled) { + return; + } + + const apiTokenCookie = req.cookies.get(cookies.NAMES.API_TOKEN); + + // if user doesn't have api token cookie and he is trying to access account page + // do redirect to auth page + if (!apiTokenCookie) { + // we don't have any info from router here, so just do straight forward sub-string search (sorry) + const isAccountRoute = + req.nextUrl.pathname.includes('/account/') || + (req.nextUrl.pathname === '/txs' && req.nextUrl.searchParams.get('tab') === 'watchlist'); + const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile'); + + if ((isAccountRoute || isProfileRoute)) { + const authUrl = feature.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } }); + return NextResponse.redirect(authUrl); + } + } + + // if user hasn't confirmed email yet + if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) { + // if user has both cookies, make redirect to logout + if (apiTokenCookie) { + // yes, we could have checked that the current URL is not the logout URL, but we hadn't + // logout URL is always external URL in auth0.com sub-domain + // at least we hope so + + const res = NextResponse.redirect(feature.logoutUrl); + res.cookies.delete(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED); // reset cookie to show email verification page again + + return res; + } + + // if user hasn't seen email verification page, make redirect to it + if (!req.cookies.get(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED)) { + if (!req.nextUrl.pathname.includes('/auth/unverified-email')) { + const url = config.app.baseUrl + route({ pathname: '/auth/unverified-email' }); + const res = NextResponse.redirect(url); + res.cookies.set({ + name: cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED, + value: 'true', + expires: Date.now() + 7 * DAY, + }); + return res; + } + } + } +} diff --git a/nextjs/middlewares/colorTheme.ts b/nextjs/middlewares/colorTheme.ts new file mode 100644 index 0000000000..dcb8314cae --- /dev/null +++ b/nextjs/middlewares/colorTheme.ts @@ -0,0 +1,15 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import appConfig from 'configs/app'; +import * as cookiesLib from 'lib/cookies'; + +export default function colorThemeMiddleware(req: NextRequest, res: NextResponse) { + const colorModeCookie = req.cookies.get(cookiesLib.NAMES.COLOR_MODE); + + if (!colorModeCookie) { + if (appConfig.UI.colorTheme.default) { + res.cookies.set(cookiesLib.NAMES.COLOR_MODE, appConfig.UI.colorTheme.default.colorMode, { path: '/' }); + res.cookies.set(cookiesLib.NAMES.COLOR_MODE_HEX, appConfig.UI.colorTheme.default.hex, { path: '/' }); + } + } +} diff --git a/nextjs/middlewares/index.ts b/nextjs/middlewares/index.ts new file mode 100644 index 0000000000..b9466373a3 --- /dev/null +++ b/nextjs/middlewares/index.ts @@ -0,0 +1,2 @@ +export { account } from './account'; +export { default as colorTheme } from './colorTheme'; diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts new file mode 100644 index 0000000000..6a3fb43e7b --- /dev/null +++ b/nextjs/nextjs-routes.d.ts @@ -0,0 +1,180 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +// This file will be automatically regenerated when your Next.js server is running. +// nextjs-routes version: 1.0.8 +/* eslint-disable */ + +// prettier-ignore +declare module "nextjs-routes" { + export type Route = + | StaticRoute<"/404"> + | StaticRoute<"/account/api-key"> + | StaticRoute<"/account/custom-abi"> + | StaticRoute<"/account/tag-address"> + | StaticRoute<"/account/verified-addresses"> + | StaticRoute<"/account/watchlist"> + | StaticRoute<"/accounts"> + | DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }> + | DynamicRoute<"/address/[hash]", { "hash": string }> + | StaticRoute<"/api/config"> + | StaticRoute<"/api/csrf"> + | StaticRoute<"/api/healthz"> + | StaticRoute<"/api/log"> + | StaticRoute<"/api/media-type"> + | StaticRoute<"/api/metrics"> + | StaticRoute<"/api/proxy"> + | StaticRoute<"/api/sprite"> + | StaticRoute<"/api-docs"> + | DynamicRoute<"/apps/[id]", { "id": string }> + | StaticRoute<"/apps"> + | StaticRoute<"/auth/auth0"> + | StaticRoute<"/auth/profile"> + | StaticRoute<"/auth/unverified-email"> + | DynamicRoute<"/batches/[number]", { "number": string }> + | StaticRoute<"/batches"> + | DynamicRoute<"/blobs/[hash]", { "hash": string }> + | DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }> + | DynamicRoute<"/block/countdown/[height]", { "height": string }> + | StaticRoute<"/block/countdown"> + | StaticRoute<"/blocks"> + | StaticRoute<"/contract-verification"> + | StaticRoute<"/csv-export"> + | StaticRoute<"/deposits"> + | StaticRoute<"/dispute-games"> + | StaticRoute<"/gas-tracker"> + | StaticRoute<"/graphiql"> + | StaticRoute<"/"> + | StaticRoute<"/login"> + | StaticRoute<"/mud-worlds"> + | DynamicRoute<"/name-domains/[name]", { "name": string }> + | StaticRoute<"/name-domains"> + | DynamicRoute<"/op/[hash]", { "hash": string }> + | StaticRoute<"/ops"> + | StaticRoute<"/output-roots"> + | StaticRoute<"/public-tags/submit"> + | StaticRoute<"/search-results"> + | StaticRoute<"/sprite"> + | StaticRoute<"/stats"> + | DynamicRoute<"/token/[hash]", { "hash": string }> + | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> + | StaticRoute<"/tokens"> + | DynamicRoute<"/tx/[hash]", { "hash": string }> + | StaticRoute<"/txs"> + | DynamicRoute<"/txs/kettle/[hash]", { "hash": string }> + | StaticRoute<"/validators"> + | StaticRoute<"/verified-contracts"> + | StaticRoute<"/visualize/sol2uml"> + | StaticRoute<"/withdrawals">; + + interface StaticRoute { + pathname: Pathname; + query?: Query | undefined; + hash?: string | null | undefined; + } + + interface DynamicRoute { + pathname: Pathname; + query: Parameters & Query; + hash?: string | null | undefined; + } + + interface Query { + [key: string]: string | string[] | undefined; + }; + + export type RoutedQuery

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

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

(): NextRouter

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

= NextPage & { + getLayout?: (page: React.ReactElement) => React.ReactNode; +} + +export interface RouteParams { + pathname: Pathname; + query?: Route['query']; +} diff --git a/nextjs/utils/buildUrl.ts b/nextjs/utils/buildUrl.ts new file mode 100644 index 0000000000..35aa914d8d --- /dev/null +++ b/nextjs/utils/buildUrl.ts @@ -0,0 +1,23 @@ +import { compile } from 'path-to-regexp'; + +import config from 'configs/app'; +import { RESOURCES } from 'lib/api/resources'; +import type { ApiResource, ResourceName } from 'lib/api/resources'; + +export default function buildUrl( + _resource: ApiResource | ResourceName, + pathParams?: Record, + queryParams?: Record, +) { + const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource; + const baseUrl = resource.endpoint || config.api.endpoint; + const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath; + const path = basePath + resource.path; + const url = new URL(compile(path)(pathParams), baseUrl); + + queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { + value && url.searchParams.append(key, String(value)); + }); + + return url.toString(); +} diff --git a/nextjs/utils/detectBotRequest.ts b/nextjs/utils/detectBotRequest.ts new file mode 100644 index 0000000000..1eecf6333a --- /dev/null +++ b/nextjs/utils/detectBotRequest.ts @@ -0,0 +1,52 @@ +import type { IncomingMessage } from 'http'; + +type SocialPreviewBot = 'twitter' | 'facebook' | 'telegram' | 'slack'; +type SearchEngineBot = 'google' | 'bing' | 'yahoo' | 'duckduckgo'; + +type ReturnType = { + type: 'social_preview'; + bot: SocialPreviewBot; +} | { + type: 'search_engine'; + bot: SearchEngineBot; +} | undefined + +export default function detectBotRequest(req: IncomingMessage): ReturnType { + const userAgent = req.headers['user-agent']; + + if (!userAgent) { + return; + } + + if (userAgent.toLowerCase().includes('twitter')) { + return { type: 'social_preview', bot: 'twitter' }; + } + + if (userAgent.toLowerCase().includes('facebook')) { + return { type: 'social_preview', bot: 'facebook' }; + } + + if (userAgent.toLowerCase().includes('telegram')) { + return { type: 'social_preview', bot: 'telegram' }; + } + + if (userAgent.toLowerCase().includes('slack')) { + return { type: 'social_preview', bot: 'slack' }; + } + + if (userAgent.toLowerCase().includes('googlebot')) { + return { type: 'search_engine', bot: 'google' }; + } + + if (userAgent.toLowerCase().includes('bingbot')) { + return { type: 'search_engine', bot: 'bing' }; + } + + if (userAgent.toLowerCase().includes('yahoo')) { + return { type: 'search_engine', bot: 'yahoo' }; + } + + if (userAgent.toLowerCase().includes('duckduck')) { + return { type: 'search_engine', bot: 'duckduckgo' }; + } +} diff --git a/nextjs/utils/fetchApi.ts b/nextjs/utils/fetchApi.ts new file mode 100644 index 0000000000..63eff42384 --- /dev/null +++ b/nextjs/utils/fetchApi.ts @@ -0,0 +1,53 @@ +import fetch, { AbortError } from 'node-fetch'; + +import buildUrl from 'nextjs/utils/buildUrl'; +import { httpLogger } from 'nextjs/utils/logger'; + +import { RESOURCES } from 'lib/api/resources'; +import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources'; +import { SECOND } from 'lib/consts'; +import metrics from 'lib/monitoring/metrics'; + +type Params = ( + { + resource: R; + pathParams?: ResourcePathParams; + } | { + url: string; + route: string; + } +) & { + timeout?: number; +} + +export default async function fetchApi>(params: Params): Promise { + const controller = new AbortController(); + + const timeout = setTimeout(() => { + controller.abort(); + }, params.timeout || SECOND); + + const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams); + const route = 'route' in params ? params.route : RESOURCES[params.resource]['path']; + + const end = metrics?.apiRequestDuration.startTimer(); + + try { + const response = await fetch(url, { signal: controller.signal }); + + const duration = end?.({ route, code: response.status }); + if (response.status === 200) { + httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration }); + } else { + httpLogger.logger.error({ message: 'API fetch', url, code: response.status, duration }); + } + + return await response.json() as Promise; + } catch (error) { + const code = error instanceof AbortError ? 504 : 500; + const duration = end?.({ route, code }); + httpLogger.logger.error({ message: 'API fetch', url, code, duration }); + } finally { + clearTimeout(timeout); + } +} diff --git a/nextjs/utils/fetchProxy.ts b/nextjs/utils/fetchProxy.ts new file mode 100644 index 0000000000..0081781215 --- /dev/null +++ b/nextjs/utils/fetchProxy.ts @@ -0,0 +1,57 @@ +import type { IncomingMessage } from 'http'; +import _pick from 'lodash/pick'; +import type { NextApiRequest } from 'next'; +import type { NextApiRequestCookies } from 'next/dist/server/api-utils'; +import type { RequestInit, Response } from 'node-fetch'; +import nodeFetch from 'node-fetch'; + +import { httpLogger } from 'nextjs/utils/logger'; + +import * as cookies from 'lib/cookies'; + +export default function fetchFactory( + _req: NextApiRequest | (IncomingMessage & { cookies: NextApiRequestCookies }), +) { + // first arg can be only a string + // FIXME migrate to RequestInfo later if needed + return function fetch(url: string, init?: RequestInit): Promise { + const apiToken = _req.cookies[cookies.NAMES.API_TOKEN]; + + const headers = { + accept: _req.headers['accept'] || 'application/json', + 'content-type': _req.headers['content-type'] || 'application/json', + cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '', + ..._pick(_req.headers, [ + 'x-csrf-token', + 'Authorization', + // feature flags + 'updated-gas-oracle', + ]) as Record, + }; + + httpLogger.logger.info({ + message: 'API fetch via Next.js proxy', + url, + // headers, + }); + + const body = (() => { + const _body = init?.body; + if (!_body) { + return; + } + + if (typeof _body === 'string') { + return _body; + } + + return JSON.stringify(_body); + })(); + + return nodeFetch(url, { + ...init, + headers, + body, + }); + }; +} diff --git a/nextjs/utils/logRequestFromBot.ts b/nextjs/utils/logRequestFromBot.ts new file mode 100644 index 0000000000..6c22b63258 --- /dev/null +++ b/nextjs/utils/logRequestFromBot.ts @@ -0,0 +1,28 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + +import metrics from 'lib/monitoring/metrics'; + +import detectBotRequest from './detectBotRequest'; + +export default async function logRequestFromBot(req: IncomingMessage | undefined, res: ServerResponse | undefined, pathname: string) { + if (!req || !res || !metrics) { + return; + } + + const botInfo = detectBotRequest(req); + + if (!botInfo) { + return; + } + + switch (botInfo.type) { + case 'search_engine': { + metrics.searchEngineBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + case 'social_preview': { + metrics.socialPreviewBotRequests.inc({ route: pathname, bot: botInfo.bot }); + return; + } + } +} diff --git a/nextjs/utils/logger.ts b/nextjs/utils/logger.ts new file mode 100644 index 0000000000..3882ce95bc --- /dev/null +++ b/nextjs/utils/logger.ts @@ -0,0 +1,3 @@ +import pino from 'pino-http'; + +export const httpLogger = pino(); diff --git a/nextjs/utils/serverTiming.ts b/nextjs/utils/serverTiming.ts new file mode 100644 index 0000000000..5a1a455890 --- /dev/null +++ b/nextjs/utils/serverTiming.ts @@ -0,0 +1,10 @@ +import type { ServerResponse } from 'http'; + +export function appendValue(res: ServerResponse | undefined, name: string, value: number) { + const currentValue = res?.getHeader('Server-Timing') || ''; + const nextValue = [ + currentValue, + `${ name };dur=${ value }`, + ].filter(Boolean).join(','); + res?.setHeader('Server-Timing', nextValue); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..b6638752f5 --- /dev/null +++ b/package.json @@ -0,0 +1,168 @@ +{ + "name": "blockscout-frontend", + "version": "1.0.0", + "private": false, + "homepage": "https://github.com/blockscout/frontend#readme", + "engines": { + "node": "20.11.0", + "npm": "10.2.4" + }, + "scripts": { + "dev": "./tools/scripts/dev.sh", + "dev:preset": "./tools/scripts/dev.preset.sh", + "dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js", + "build": "next build", + "build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./", + "start": "next start", + "start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout-frontend:local", + "start:docker:preset": "./tools/scripts/docker.preset.sh", + "lint:eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:eslint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", + "lint:tsc": "tsc -p ./tsconfig.json", + "lint:envs-validator:test": "cd ./deploy/tools/envs-validator && ./test.sh", + "prepare": "husky install", + "svg:format": "svgo -r ./icons", + "svg:build-sprite": "icons build -i ./icons -o ./public/icons --optimize", + "test:pw": "./tools/scripts/pw.sh", + "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", + "test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.sh", + "test:pw:docker:deps": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.deps.sh", + "test:pw:ci": "yarn test:pw --project=$PW_PROJECT", + "test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js", + "test:jest": "jest", + "test:jest:watch": "jest --watch", + "favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh", + "monitoring:prometheus:local": "docker run --name blockscout_prometheus -d -p 127.0.0.1:9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus", + "monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise" + }, + "dependencies": { + "@blockscout/bens-types": "1.4.1", + "@blockscout/stats-types": "1.6.0", + "@blockscout/visualizer-types": "0.2.0", + "@chakra-ui/react": "2.7.1", + "@chakra-ui/theme-tools": "^2.0.18", + "@emotion/react": "^11.10.4", + "@emotion/styled": "^11.10.4", + "@growthbook/growthbook-react": "0.21.0", + "@hypelab/sdk-react": "^1.0.0", + "@metamask/post-message-stream": "^7.0.0", + "@metamask/providers": "^10.2.1", + "@monaco-editor/react": "^4.4.6", + "@next/bundle-analyzer": "14.2.3", + "@opentelemetry/auto-instrumentations-node": "^0.39.4", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.45.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.45.0", + "@opentelemetry/resources": "^1.18.0", + "@opentelemetry/sdk-node": "^0.45.0", + "@opentelemetry/sdk-trace-node": "^1.18.0", + "@opentelemetry/semantic-conventions": "^1.18.0", + "@sentry/cli": "^2.21.2", + "@sentry/react": "7.24.0", + "@sentry/tracing": "7.24.0", + "@slise/embed-react": "^2.2.0", + "@tanstack/react-query": "^5.4.3", + "@tanstack/react-query-devtools": "^5.4.3", + "@types/papaparse": "^5.3.5", + "@types/react-scroll": "^1.8.4", + "@web3modal/wagmi": "4.2.1", + "airtable": "^0.12.2", + "bignumber.js": "^9.1.0", + "blo": "^1.1.1", + "chakra-react-select": "^4.4.3", + "crypto-js": "^4.2.0", + "d3": "^7.6.1", + "dappscout-iframe": "0.2.2", + "dayjs": "^1.11.5", + "dom-to-image": "^2.6.0", + "focus-visible": "^5.2.0", + "framer-motion": "^6.5.1", + "getit-sdk": "^1.0.4", + "gradient-avatar": "git+https://github.com/blockscout/gradient-avatar.git", + "graphiql": "^2.2.0", + "graphql": "^16.8.1", + "graphql-ws": "^5.11.3", + "js-cookie": "^3.0.1", + "lodash": "^4.0.0", + "magic-bytes.js": "1.8.0", + "mixpanel-browser": "^2.47.0", + "monaco-editor": "^0.34.1", + "next": "14.2.3", + "nextjs-routes": "^1.0.8", + "node-fetch": "^3.2.9", + "papaparse": "^5.3.2", + "path-to-regexp": "^6.2.1", + "phoenix": "^1.6.15", + "pino-http": "^8.2.1", + "pino-pretty": "^9.1.1", + "prom-client": "15.1.1", + "qrcode": "^1.5.1", + "react": "18.2.0", + "react-device-detect": "^2.2.3", + "react-dom": "18.2.0", + "react-google-recaptcha": "^3.1.0", + "react-hook-form": "^7.33.1", + "react-identicons": "^1.2.5", + "react-intersection-observer": "^9.5.2", + "react-jazzicon": "^1.0.4", + "react-number-format": "^5.3.1", + "react-scroll": "^1.8.7", + "swagger-ui-react": "^5.9.0", + "use-font-face-observer": "^1.2.1", + "viem": "2.10.9", + "wagmi": "2.9.2", + "xss": "^1.0.14" + }, + "devDependencies": { + "@playwright/experimental-ct-react": "1.41.1", + "@playwright/test": "1.41.1", + "@svgr/webpack": "^6.5.1", + "@tanstack/eslint-plugin-query": "^5.0.5", + "@testing-library/react": "^14.0.0", + "@total-typescript/ts-reset": "^0.4.0", + "@types/crypto-js": "^4.1.1", + "@types/csp-dev": "^1.0.0", + "@types/d3": "^7.4.0", + "@types/dom-to-image": "^2.6.4", + "@types/jest": "^29.2.0", + "@types/js-cookie": "^3.0.2", + "@types/mixpanel-browser": "^2.38.1", + "@types/node": "20.11.0", + "@types/phoenix": "^1.5.4", + "@types/qrcode": "^1.5.0", + "@types/react": "18.0.9", + "@types/react-dom": "18.0.5", + "@types/react-google-recaptcha": "^2.1.5", + "@types/swagger-ui-react": "^4.11.0", + "@types/ws": "^8.5.3", + "@typescript-eslint/eslint-plugin": "^5.60.0", + "@vitejs/plugin-react": "^4.0.0", + "css-loader": "^6.7.3", + "dotenv-cli": "^6.0.0", + "eslint": "^8.32.0", + "eslint-config-next": "13.3.0", + "eslint-plugin-es5": "^1.5.0", + "eslint-plugin-import-helpers": "^1.2.1", + "eslint-plugin-jest": "^27.1.6", + "eslint-plugin-no-cyrillic-string": "^1.0.5", + "eslint-plugin-playwright": "^0.11.2", + "eslint-plugin-regexp": "^1.7.0", + "husky": "^8.0.0", + "jest": "^29.2.1", + "jest-environment-jsdom": "^29.2.1", + "jest-fetch-mock": "^3.0.3", + "lint-staged": ">=10", + "mockdate": "^3.0.5", + "style-loader": "^3.3.1", + "svg-icons-cli": "^0.0.5", + "svgo": "^2.8.0", + "ts-jest": "^29.0.3", + "ts-node": "^10.9.1", + "typescript": "5.4.2", + "vite-plugin-svgr": "^2.2.2", + "vite-tsconfig-paths": "^3.5.2", + "ws": "^8.17.1" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": "eslint --cache --fix" + } +} diff --git a/pages/404.tsx b/pages/404.tsx new file mode 100644 index 0000000000..0e747ae875 --- /dev/null +++ b/pages/404.tsx @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; + +import type { NextPageWithLayout } from 'nextjs/types'; + +import PageNextJs from 'nextjs/PageNextJs'; + +import AppError from 'ui/shared/AppError/AppError'; +import LayoutError from 'ui/shared/layout/LayoutError'; + +const error = new Error('Not found', { cause: { status: 404 } }); + +const Page: NextPageWithLayout = () => { + React.useEffect(() => { + Sentry.captureException(new Error('Page not found'), { tags: { source: '404' } }); + }, []); + + return ( + + + + ); +}; + +Page.getLayout = function getLayout(page: React.ReactElement) { + return ( + + { page } + + ); +}; + +export default Page; diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000000..28520d76a3 --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,86 @@ +import type { ChakraProps } from '@chakra-ui/react'; +import { GrowthBookProvider } from '@growthbook/growthbook-react'; +import * as Sentry from '@sentry/react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import type { AppProps } from 'next/app'; +import React from 'react'; + +import type { NextPageWithLayout } from 'nextjs/types'; + +import config from 'configs/app'; +import useQueryClientConfig from 'lib/api/useQueryClientConfig'; +import { AppContextProvider } from 'lib/contexts/app'; +import { ChakraProvider } from 'lib/contexts/chakra'; +import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; +import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; +import { growthBook } from 'lib/growthbook/init'; +import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; +import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; +import { SocketProvider } from 'lib/socket/context'; +import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; +import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; +import Layout from 'ui/shared/layout/Layout'; +import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; + +import 'lib/setLocale'; +// import 'focus-visible/dist/focus-visible'; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +} + +const ERROR_SCREEN_STYLES: ChakraProps = { + h: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'center', + width: 'fit-content', + maxW: '800px', + margin: '0 auto', + p: { base: 4, lg: 0 }, +}; + +function MyApp({ Component, pageProps }: AppPropsWithLayout) { + + useLoadFeatures(); + useNotifyOnNavigation(); + + const queryClient = useQueryClientConfig(); + + const handleError = React.useCallback((error: Error) => { + Sentry.captureException(error); + }, []); + + const getLayout = Component.getLayout ?? ((page) => { page }); + + return ( + + + + + + + + + + { getLayout() } + + + + + + + + + + + + ); +} + +export default MyApp; diff --git a/pages/_document.tsx b/pages/_document.tsx new file mode 100644 index 0000000000..bfb6c3b107 --- /dev/null +++ b/pages/_document.tsx @@ -0,0 +1,68 @@ +import { ColorModeScript } from '@chakra-ui/react'; +import type { DocumentContext } from 'next/document'; +import Document, { Html, Head, Main, NextScript } from 'next/document'; +import React from 'react'; + +import logRequestFromBot from 'nextjs/utils/logRequestFromBot'; +import * as serverTiming from 'nextjs/utils/serverTiming'; + +import theme from 'theme'; +import * as svgSprite from 'ui/shared/IconSvg'; + +class MyDocument extends Document { + static async getInitialProps(ctx: DocumentContext) { + const originalRenderPage = ctx.renderPage; + ctx.renderPage = async() => { + const start = Date.now(); + const result = await originalRenderPage(); + const end = Date.now(); + + serverTiming.appendValue(ctx.res, 'renderPage', end - start); + + return result; + }; + + await logRequestFromBot(ctx.req, ctx.res, ctx.pathname); + + const initialProps = await Document.getInitialProps(ctx); + + return initialProps; + } + + render() { + return ( + + + { /* FONTS */ } + + + + { /* eslint-disable-next-line @next/next/no-sync-scripts */ } + + + + + diff --git a/playwright/index.ts b/playwright/index.ts new file mode 100644 index 0000000000..e0e5b4be99 --- /dev/null +++ b/playwright/index.ts @@ -0,0 +1,25 @@ +import './fonts.css'; +import './index.css'; +import { beforeMount } from '@playwright/experimental-ct-react/hooks'; +import _defaultsDeep from 'lodash/defaultsDeep'; +import MockDate from 'mockdate'; +import * as router from 'next/router'; + +const NEXT_ROUTER_MOCK = { + query: {}, + pathname: '', + push: () => Promise.resolve(), + replace: () => Promise.resolve(), +}; + +beforeMount(async({ hooksConfig }) => { + // Before mount, redefine useRouter to return mock value from test. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: I really want to redefine this property :) + router.useRouter = () => _defaultsDeep(hooksConfig?.router, NEXT_ROUTER_MOCK); + + // set current date + MockDate.set('2022-11-11T12:00:00Z'); +}); + +export {}; diff --git a/playwright/lib.tsx b/playwright/lib.tsx new file mode 100644 index 0000000000..586f980bf0 --- /dev/null +++ b/playwright/lib.tsx @@ -0,0 +1,73 @@ +/* eslint-disable no-console */ +import { test as base } from '@playwright/experimental-ct-react'; + +import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider'; +import * as mockApiResponse from './fixtures/mockApiResponse'; +import * as mockAssetResponse from './fixtures/mockAssetResponse'; +import * as mockConfigResponse from './fixtures/mockConfigResponse'; +import * as mockContractReadResponse from './fixtures/mockContractReadResponse'; +import * as mockEnvs from './fixtures/mockEnvs'; +import * as mockFeatures from './fixtures/mockFeatures'; +import * as mockRpcResponse from './fixtures/mockRpcResponse'; +import * as mockTextAd from './fixtures/mockTextAd'; +import * as render from './fixtures/render'; +import * as socketServer from './fixtures/socketServer'; + +interface Fixtures { + render: render.RenderFixture; + mockApiResponse: mockApiResponse.MockApiResponseFixture; + mockAssetResponse: mockAssetResponse.MockAssetResponseFixture; + mockConfigResponse: mockConfigResponse.MockConfigResponseFixture; + mockContractReadResponse: mockContractReadResponse.MockContractReadResponseFixture; + mockEnvs: mockEnvs.MockEnvsFixture; + mockFeatures: mockFeatures.MockFeaturesFixture; + mockRpcResponse: mockRpcResponse.MockRpcResponseFixture; + createSocket: socketServer.CreateSocketFixture; + injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider; + mockTextAd: mockTextAd.MockTextAdFixture; +} + +const test = base.extend({ + render: render.default, + mockApiResponse: mockApiResponse.default, + mockAssetResponse: mockAssetResponse.default, + mockConfigResponse: mockConfigResponse.default, + mockContractReadResponse: mockContractReadResponse.default, + mockEnvs: mockEnvs.default, + mockFeatures: mockFeatures.default, + mockRpcResponse: mockRpcResponse.default, + // FIXME: for some reason Playwright does not intercept requests to text ad provider when running multiple tests in parallel + // even if we have a global request interceptor (maybe it is related to service worker issue, maybe not) + // so we have to inject mockTextAd fixture in each test and mock the response where it is needed + mockTextAd: mockTextAd.default, + createSocket: socketServer.createSocket, + injectMetaMaskProvider: injectMetaMaskProvider.default, +}); + +test.beforeEach(async({ page, mockTextAd }) => { + // debug + const isDebug = process.env.PWDEBUG === '1'; + + if (isDebug) { + page.on('console', msg => console.log(msg.text())); + page.on('request', request => console.info('\x1b[34m%s\x1b[0m', '>>', request.method(), request.url())); + page.on('response', response => console.info('\x1b[35m%s\x1b[0m', '<<', String(response.status()), response.url())); + } + + // Abort all other requests to external resources + await page.route('**', (route) => { + if (!route.request().url().startsWith('http://localhost')) { + isDebug && console.info('Aborting request to', route.request().url()); + route.abort(); + } else { + route.continue(); + } + }); + + // with few exceptions: + // 1. mock text AD requests + await mockTextAd(); +}); + +export * from '@playwright/experimental-ct-react'; +export { test }; diff --git a/playwright/mocks/file_mock_1.json b/playwright/mocks/file_mock_1.json new file mode 100644 index 0000000000..13d905de84 --- /dev/null +++ b/playwright/mocks/file_mock_1.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} \ No newline at end of file diff --git a/playwright/mocks/file_mock_2.json b/playwright/mocks/file_mock_2.json new file mode 100644 index 0000000000..9532cafdf5 --- /dev/null +++ b/playwright/mocks/file_mock_2.json @@ -0,0 +1,4 @@ +{ + "foo": "bar", + "baz": ["baz","baz","baz"] +} \ No newline at end of file diff --git a/playwright/mocks/file_mock_with_very_long_name.json b/playwright/mocks/file_mock_with_very_long_name.json new file mode 100644 index 0000000000..c6ca79c343 --- /dev/null +++ b/playwright/mocks/file_mock_with_very_long_name.json @@ -0,0 +1,19 @@ +{ + "id": 1, + "title": "iPhone 9", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce nec leo odio. Vivamus iaculis faucibus tempus. Duis dapibus, ligula eu consequat ornare, sapien nisl laoreet arcu, eu pharetra dolor urna eget ligula. Sed facilisis pretium risus in finibus. Maecenas egestas orci euismod venenatis aliquam. Vivamus sed pretium dui, vitae porta ipsum. Donec elit tortor, imperdiet eu nulla at, condimentum consequat nisi. Phasellus mollis sem aliquam dolor lacinia bibendum. Mauris pulvinar vestibulum sodales. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam vitae facilisis odio. Mauris in ultricies nulla. Sed ornare consequat nulla. Cras pellentesque, erat at gravida rutrum, erat neque facilisis magna, et iaculis enim tortor ornare felis. Quisque mollis dignissim magna. In vitae ultricies felis. Quisque in ipsum non lorem ornare elementum vel vitae nisl. Integer luctus pulvinar dui a eleifend. Aliquam iaculis odio vitae metus ullamcorper, sed semper turpis consectetur. In convallis, justo a aliquet rhoncus, erat metus fringilla turpis, eget imperdiet purus est quis magna. Curabitur ornare mattis malesuada. In vitae lacinia est. Sed sit amet magna eget dolor tempus euismod.", + "price": 549, + "discountPercentage": 12.96, + "rating": 4.69, + "stock": 94, + "brand": "Apple", + "category": "smartphones", + "thumbnail": "https://i.dummyjson.com/data/products/1/thumbnail.jpg", + "images": [ + "https://i.dummyjson.com/data/products/1/1.jpg", + "https://i.dummyjson.com/data/products/1/2.jpg", + "https://i.dummyjson.com/data/products/1/3.jpg", + "https://i.dummyjson.com/data/products/1/4.jpg", + "https://i.dummyjson.com/data/products/1/thumbnail.jpg" + ] +} \ No newline at end of file diff --git a/playwright/mocks/image_long.jpg b/playwright/mocks/image_long.jpg new file mode 100644 index 0000000000..f830ed57c5 Binary files /dev/null and b/playwright/mocks/image_long.jpg differ diff --git a/playwright/mocks/image_md.jpg b/playwright/mocks/image_md.jpg new file mode 100644 index 0000000000..88e2472a3e Binary files /dev/null and b/playwright/mocks/image_md.jpg differ diff --git a/playwright/mocks/image_s.jpg b/playwright/mocks/image_s.jpg new file mode 100644 index 0000000000..c03a062033 Binary files /dev/null and b/playwright/mocks/image_s.jpg differ diff --git a/playwright/mocks/image_svg.svg b/playwright/mocks/image_svg.svg new file mode 100644 index 0000000000..59b2b6ef6c --- /dev/null +++ b/playwright/mocks/image_svg.svg @@ -0,0 +1,3 @@ + + + diff --git a/playwright/mocks/lib/growthbook/useFeatureValue.js b/playwright/mocks/lib/growthbook/useFeatureValue.js new file mode 100644 index 0000000000..e731be89f3 --- /dev/null +++ b/playwright/mocks/lib/growthbook/useFeatureValue.js @@ -0,0 +1,13 @@ +const useFeatureValue = (name, fallback) => { + try { + const value = JSON.parse(localStorage.getItem(`pw_feature:${ name }`)); + if (value === null) { + throw new Error(); + } + return { isLoading: false, value }; + } catch (error) { + return { isLoading: false, value: fallback }; + } +}; + +export default useFeatureValue; diff --git a/playwright/mocks/modules/@metamask/post-message-stream.js b/playwright/mocks/modules/@metamask/post-message-stream.js new file mode 100644 index 0000000000..0212528dc3 --- /dev/null +++ b/playwright/mocks/modules/@metamask/post-message-stream.js @@ -0,0 +1,9 @@ +class WindowPostMessageStream { + constructor() { + return null; + } +} + +export { + WindowPostMessageStream, +}; diff --git a/playwright/mocks/modules/@metamask/providers.js b/playwright/mocks/modules/@metamask/providers.js new file mode 100644 index 0000000000..4c2f48b6fc --- /dev/null +++ b/playwright/mocks/modules/@metamask/providers.js @@ -0,0 +1,5 @@ +function initializeProvider() {} + +export { + initializeProvider, +}; diff --git a/playwright/mocks/modules/@web3modal/wagmi/react.js b/playwright/mocks/modules/@web3modal/wagmi/react.js new file mode 100644 index 0000000000..7765272c71 --- /dev/null +++ b/playwright/mocks/modules/@web3modal/wagmi/react.js @@ -0,0 +1,26 @@ +function useWeb3Modal() { + return { + open: () => {}, + }; +} + +function useWeb3ModalState() { + return { + isOpen: false, + }; +} + +function useWeb3ModalTheme() { + return { + setThemeMode: () => {}, + }; +} + +function createWeb3Modal() {} + +export { + createWeb3Modal, + useWeb3Modal, + useWeb3ModalState, + useWeb3ModalTheme, +}; diff --git a/playwright/mocks/network-logo.svg b/playwright/mocks/network-logo.svg new file mode 100644 index 0000000000..b3781338b1 --- /dev/null +++ b/playwright/mocks/network-logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/playwright/mocks/page.html b/playwright/mocks/page.html new file mode 100644 index 0000000000..55da628a86 --- /dev/null +++ b/playwright/mocks/page.html @@ -0,0 +1,18 @@ + + + + + + this is HTML page + + \ No newline at end of file diff --git a/playwright/utils/config.ts b/playwright/utils/config.ts new file mode 100644 index 0000000000..32a234a605 --- /dev/null +++ b/playwright/utils/config.ts @@ -0,0 +1,11 @@ +import { devices } from '@playwright/test'; + +export const viewport = { + mobile: devices['iPhone 13 Pro'].viewport, + md: { width: 1001, height: 800 }, + xl: { width: 1600, height: 1000 }, +}; + +export const maskColor = '#4299E1'; // blue.400 + +export const adsBannerSelector = '.adsbyslise'; diff --git a/playwright/utils/socket.ts b/playwright/utils/socket.ts new file mode 100644 index 0000000000..26a8a2202b --- /dev/null +++ b/playwright/utils/socket.ts @@ -0,0 +1 @@ +export const port = 3200; diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000000..89b95e0ecb --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + # - "first.rules" + # - "second.rules" + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ['localhost:9090'] + - job_name: frontend + metrics_path: /node-api/metrics + static_configs: + - targets: ['host.docker.internal:3000'] \ No newline at end of file diff --git a/public/README.md b/public/README.md new file mode 100644 index 0000000000..3fd1a97a4c --- /dev/null +++ b/public/README.md @@ -0,0 +1,4 @@ +**Directories** +- `/icons` - Folder for SVG-sprite assets, generated at build time. +- `/static` - Folder for static assets that are consistent between app re-runs but may differ from one build version to another. +- `/assets` - Folder for dynamically generated assets during the app start, such as the favicon bundle, ENV variables file, and external app configurations. \ No newline at end of file diff --git a/public/assets/favicon/apple-touch-icon-120x120.png b/public/assets/favicon/apple-touch-icon-120x120.png new file mode 100644 index 0000000000..7f502ce922 Binary files /dev/null and b/public/assets/favicon/apple-touch-icon-120x120.png differ diff --git a/public/assets/favicon/apple-touch-icon-152x152.png b/public/assets/favicon/apple-touch-icon-152x152.png new file mode 100644 index 0000000000..fbadd1501c Binary files /dev/null and b/public/assets/favicon/apple-touch-icon-152x152.png differ diff --git a/public/assets/favicon/apple-touch-icon-180x180.png b/public/assets/favicon/apple-touch-icon-180x180.png new file mode 100644 index 0000000000..06dd66a668 Binary files /dev/null and b/public/assets/favicon/apple-touch-icon-180x180.png differ diff --git a/public/assets/favicon/apple-touch-icon-60x60.png b/public/assets/favicon/apple-touch-icon-60x60.png new file mode 100644 index 0000000000..e2de1ac496 Binary files /dev/null and b/public/assets/favicon/apple-touch-icon-60x60.png differ diff --git a/public/assets/favicon/apple-touch-icon-76x76.png b/public/assets/favicon/apple-touch-icon-76x76.png new file mode 100644 index 0000000000..68ad9f2584 Binary files /dev/null and b/public/assets/favicon/apple-touch-icon-76x76.png differ diff --git a/public/assets/favicon/apple-touch-icon.png b/public/assets/favicon/apple-touch-icon.png new file mode 100644 index 0000000000..06dd66a668 Binary files /dev/null and b/public/assets/favicon/apple-touch-icon.png differ diff --git a/public/assets/favicon/favicon-16x16.png b/public/assets/favicon/favicon-16x16.png new file mode 100644 index 0000000000..ee37c4c3a5 Binary files /dev/null and b/public/assets/favicon/favicon-16x16.png differ diff --git a/public/assets/favicon/favicon-32x32.png b/public/assets/favicon/favicon-32x32.png new file mode 100644 index 0000000000..f004f2956c Binary files /dev/null and b/public/assets/favicon/favicon-32x32.png differ diff --git a/public/assets/favicon/favicon.ico b/public/assets/favicon/favicon.ico new file mode 100644 index 0000000000..145e61f5ae Binary files /dev/null and b/public/assets/favicon/favicon.ico differ diff --git a/public/assets/favicon/safari-pinned-tab.svg b/public/assets/favicon/safari-pinned-tab.svg new file mode 100644 index 0000000000..f6a897c125 --- /dev/null +++ b/public/assets/favicon/safari-pinned-tab.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts new file mode 100644 index 0000000000..eb787ac206 --- /dev/null +++ b/public/icons/name.d.ts @@ -0,0 +1,169 @@ +// This file is generated by npm run build:icons + + export type IconName = + | "ABI_slim" + | "ABI" + | "API" + | "apps_list" + | "apps_slim" + | "apps" + | "arrows/down-right" + | "arrows/east-mini" + | "arrows/east" + | "arrows/north-east" + | "arrows/south-east" + | "arrows/up-down" + | "arrows/up-head" + | "beta_xs" + | "beta" + | "blob" + | "blobs/image" + | "blobs/raw" + | "blobs/text" + | "block_countdown" + | "block_slim" + | "block" + | "brands/blockscout" + | "brands/safe" + | "brands/solidity_scan" + | "burger" + | "certified" + | "check" + | "clock-light" + | "clock" + | "coins/bitcoin" + | "collection" + | "contracts/proxy" + | "contracts/regular_many" + | "contracts/regular" + | "contracts/verified_many" + | "contracts/verified" + | "copy" + | "cross" + | "delete" + | "docs" + | "donate" + | "dots" + | "edit" + | "email-sent" + | "email" + | "empty_search_result" + | "ENS_slim" + | "ENS" + | "error-pages/404" + | "error-pages/422" + | "error-pages/429" + | "error-pages/500" + | "explorer" + | "files/csv" + | "files/image" + | "files/json" + | "files/placeholder" + | "files/sol" + | "files/yul" + | "filter" + | "flame" + | "games" + | "gas_xl" + | "gas" + | "gear_slim" + | "gear" + | "globe-b" + | "globe" + | "graphQL" + | "heart_filled" + | "heart_outline" + | "hourglass" + | "info" + | "integration/full" + | "integration/partial" + | "key" + | "lightning_navbar" + | "lightning" + | "link_external" + | "link" + | "lock" + | "minus" + | "monaco/file" + | "monaco/folder-open" + | "monaco/folder" + | "monaco/solidity" + | "monaco/vyper" + | "moon-with-star" + | "moon" + | "MUD_menu" + | "MUD" + | "networks" + | "networks/icon-placeholder" + | "networks/logo-placeholder" + | "nft_shield" + | "open-link" + | "output_roots" + | "payment_link" + | "plus" + | "privattags" + | "profile" + | "publictags_slim" + | "publictags" + | "qr_code" + | "refresh" + | "repeat" + | "restAPI" + | "rocket_xl" + | "rocket" + | "RPC" + | "scope" + | "score/score-not-ok" + | "score/score-ok" + | "search" + | "social/canny" + | "social/coingecko" + | "social/coinmarketcap" + | "social/defi_llama" + | "social/discord_filled" + | "social/discord" + | "social/facebook_filled" + | "social/git" + | "social/github_filled" + | "social/linkedin_filled" + | "social/medium_filled" + | "social/opensea_filled" + | "social/reddit_filled" + | "social/slack_filled" + | "social/stats" + | "social/telega" + | "social/telegram_filled" + | "social/twitter_filled" + | "social/twitter" + | "star_filled" + | "star_outline" + | "stats" + | "status/error" + | "status/pending" + | "status/success" + | "status/warning" + | "sun" + | "swap" + | "testnet" + | "token-placeholder" + | "token" + | "tokens" + | "tokens/xdai" + | "top-accounts" + | "transactions_slim" + | "transactions" + | "txn_batches_slim" + | "txn_batches" + | "uniswap" + | "user_op_slim" + | "user_op" + | "validator" + | "verification-steps/finalized" + | "verification-steps/unfinalized" + | "verified" + | "wallet" + | "wallets/coinbase" + | "wallets/metamask" + | "wallets/token-pocket" + | "watchlist"; + \ No newline at end of file diff --git a/public/static/apple_calendar.svg b/public/static/apple_calendar.svg new file mode 100644 index 0000000000..a55a0d5f36 --- /dev/null +++ b/public/static/apple_calendar.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/static/contract_star.png b/public/static/contract_star.png new file mode 100644 index 0000000000..32d635ef60 Binary files /dev/null and b/public/static/contract_star.png differ diff --git a/public/static/google_calendar.svg b/public/static/google_calendar.svg new file mode 100644 index 0000000000..7bd127e92c --- /dev/null +++ b/public/static/google_calendar.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/static/identicon_logos/blockies.png b/public/static/identicon_logos/blockies.png new file mode 100644 index 0000000000..d9ccead353 Binary files /dev/null and b/public/static/identicon_logos/blockies.png differ diff --git a/public/static/identicon_logos/github.png b/public/static/identicon_logos/github.png new file mode 100644 index 0000000000..346f9d212e Binary files /dev/null and b/public/static/identicon_logos/github.png differ diff --git a/public/static/identicon_logos/gradient_avatar.png b/public/static/identicon_logos/gradient_avatar.png new file mode 100644 index 0000000000..396ee27848 Binary files /dev/null and b/public/static/identicon_logos/gradient_avatar.png differ diff --git a/public/static/identicon_logos/jazzicon.png b/public/static/identicon_logos/jazzicon.png new file mode 100644 index 0000000000..c06bc880ca Binary files /dev/null and b/public/static/identicon_logos/jazzicon.png differ diff --git a/public/static/og_placeholder.png b/public/static/og_placeholder.png new file mode 100644 index 0000000000..1babd9e522 Binary files /dev/null and b/public/static/og_placeholder.png differ diff --git a/public/static/og_twitter.png b/public/static/og_twitter.png new file mode 100644 index 0000000000..1d83cbc77b Binary files /dev/null and b/public/static/og_twitter.png differ diff --git a/public/static/resizer.png b/public/static/resizer.png new file mode 100644 index 0000000000..4a894f2e2e Binary files /dev/null and b/public/static/resizer.png differ diff --git a/public/static/resizer_dark.png b/public/static/resizer_dark.png new file mode 100644 index 0000000000..b8ab11d653 Binary files /dev/null and b/public/static/resizer_dark.png differ diff --git a/public/static/resizer_light.png b/public/static/resizer_light.png new file mode 100644 index 0000000000..d6bce79540 Binary files /dev/null and b/public/static/resizer_light.png differ diff --git a/reset.d.ts b/reset.d.ts new file mode 100644 index 0000000000..12bd3edc94 --- /dev/null +++ b/reset.d.ts @@ -0,0 +1 @@ +import '@total-typescript/ts-reset'; diff --git a/stubs/ENS.ts b/stubs/ENS.ts new file mode 100644 index 0000000000..a35773f077 --- /dev/null +++ b/stubs/ENS.ts @@ -0,0 +1,34 @@ +import * as bens from '@blockscout/bens-types'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ENS_DOMAIN: bens.DetailedDomain = { + id: '0x126d74db13895f8d3a1d362410212731d1e1d9be8add83e388385f93d84c8c84', + name: 'kitty.cat.eth', + tokens: [ + { + id: '973523146267017920308', + contract_hash: ADDRESS_HASH, + type: bens.TokenType.NATIVE_DOMAIN_TOKEN, + }, + ], + owner: ADDRESS_PARAMS, + resolved_address: ADDRESS_PARAMS, + registrant: ADDRESS_PARAMS, + registration_date: '2023-12-20T01:29:12.000Z', + expiry_date: '2099-01-02T01:29:12.000Z', + other_addresses: { + ETH: ADDRESS_HASH, + }, + protocol: undefined, + resolved_with_wildcard: false, + stored_offchain: false, +}; + +export const ENS_DOMAIN_EVENT: bens.DomainEvent = { + transaction_hash: TX_HASH, + timestamp: '2022-06-06T08:43:15.000000Z', + from_address: ADDRESS_PARAMS, + action: '0xf7a16963', +}; diff --git a/stubs/L2.ts b/stubs/L2.ts new file mode 100644 index 0000000000..20bb128d90 --- /dev/null +++ b/stubs/L2.ts @@ -0,0 +1,58 @@ +import type { + OptimisticL2DepositsItem, + OptimisticL2DisputeGamesItem, + OptimisticL2OutputRootsItem, + OptimisticL2TxnBatchesItem, + OptimisticL2WithdrawalsItem, +} from 'types/api/optimisticL2'; + +import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const L2_DEPOSIT_ITEM: OptimisticL2DepositsItem = { + l1_block_number: 9045233, + l1_block_timestamp: '2023-05-22T18:00:36.000000Z', + l1_tx_hash: TX_HASH, + l1_tx_origin: ADDRESS_HASH, + l2_tx_gas_limit: '100000', + l2_tx_hash: TX_HASH, +}; + +export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = { + challenge_period_end: null, + from: ADDRESS_PARAMS, + l1_tx_hash: TX_HASH, + l2_timestamp: '2023-06-01T13:44:56.000000Z', + l2_tx_hash: TX_HASH, + msg_nonce: 2393, + msg_nonce_version: 1, + status: 'Ready to prove', +}; + +export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = { + l1_timestamp: '2023-06-01T14:46:48.000000Z', + l1_tx_hashes: [ + TX_HASH, + ], + l2_block_number: 5218590, + tx_count: 9, +}; + +export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = { + l1_block_number: 9103684, + l1_timestamp: '2023-06-01T15:26:12.000000Z', + l1_tx_hash: TX_HASH, + l2_block_number: 10102468, + l2_output_index: 50655, + output_root: TX_HASH, +}; + +export const L2_DISPUTE_GAMES_ITEM: OptimisticL2DisputeGamesItem = { + contract_address: ADDRESS_HASH, + created_at: '2023-06-01T15:26:12.000000Z', + game_type: 0, + index: 6594, + l2_block_number: 50655, + resolved_at: null, + status: 'In progress', +}; diff --git a/stubs/RPC.ts b/stubs/RPC.ts new file mode 100644 index 0000000000..0da0947c7c --- /dev/null +++ b/stubs/RPC.ts @@ -0,0 +1,94 @@ +import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt, Withdrawal } from 'viem'; + +import { ADDRESS_HASH } from './addressParams'; +import { BLOCK_HASH } from './block'; +import { TX_HASH } from './tx'; + +export const WITHDRAWAL: Withdrawal = { + index: '0x1af95d9', + validatorIndex: '0x7d748', + address: '0x9b52b9033ecbb6635f1c31a646d5691b282878aa', + amount: '0x29e16b', +}; + +export const GET_TRANSACTION: GetTransactionReturnType = { + blockHash: BLOCK_HASH, + blockNumber: BigInt(10361367), + from: ADDRESS_HASH, + gas: BigInt(800000), + maxPriorityFeePerGas: BigInt(2), + maxFeePerGas: BigInt(14), + hash: TX_HASH, + input: '0x7898e0', + nonce: 117694, + to: ADDRESS_HASH, + transactionIndex: 60, + value: BigInt(42), + type: 'eip1559', + accessList: [], + chainId: 5, + v: BigInt(0), + r: '0x2c5022ff7f78a22f1a99afbd568f75cb52812189ed8c264c8310e0b8dba2c8a8', + s: '0x50938f87c92b9eeb9777507ca8f7397840232d00d1dbac3edac6c115b4656763', + yParity: 1, + typeHex: '0x2', +}; + +export const GET_TRANSACTION_RECEIPT: TransactionReceipt = { + blockHash: BLOCK_HASH, + blockNumber: BigInt(10361367), + contractAddress: null, + cumulativeGasUsed: BigInt(39109), + effectiveGasPrice: BigInt(13), + from: ADDRESS_HASH, + gasUsed: BigInt(39109), + logs: [], + logsBloom: '0x0', + status: 'success', + to: ADDRESS_HASH, + transactionHash: TX_HASH, + transactionIndex: 60, + type: '0x2', +}; + +export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420); + +export const GET_BALANCE = BigInt(42_000_000_000_000); + +export const GET_TRANSACTIONS_COUNT = 42; + +export const GET_BLOCK: GetBlockReturnType = { + baseFeePerGas: BigInt(11), + difficulty: BigInt(111), + extraData: '0xd8830', + gasLimit: BigInt(800000), + gasUsed: BigInt(42000), + hash: BLOCK_HASH, + logsBloom: '0x008000', + miner: ADDRESS_HASH, + mixHash: BLOCK_HASH, + nonce: '0x0000000000000000', + number: BigInt(10361367), + parentHash: BLOCK_HASH, + receiptsRoot: BLOCK_HASH, + sha3Uncles: BLOCK_HASH, + size: BigInt(88), + stateRoot: BLOCK_HASH, + timestamp: BigInt(1628580000), + totalDifficulty: BigInt(10361367), + transactions: [ + TX_HASH, + ], + transactionsRoot: TX_HASH, + uncles: [], + withdrawals: Array(10).fill(WITHDRAWAL), + withdrawalsRoot: TX_HASH, + sealFields: [ '0x00' ], + blobGasUsed: BigInt(0), + excessBlobGas: BigInt(0), +}; + +export const GET_BLOCK_WITH_TRANSACTIONS: GetBlockReturnType = { + ...GET_BLOCK, + transactions: Array(50).fill(GET_TRANSACTION), +}; diff --git a/stubs/account.ts b/stubs/account.ts new file mode 100644 index 0000000000..363496ffca --- /dev/null +++ b/stubs/account.ts @@ -0,0 +1,98 @@ +import type { AddressTag, TransactionTag, ApiKey, CustomAbi, VerifiedAddress, TokenInfoApplication, WatchlistAddress } from 'types/api/account'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const PRIVATE_TAG_ADDRESS: AddressTag = { + address: ADDRESS_PARAMS, + address_hash: ADDRESS_HASH, + id: '4', + name: 'placeholder', +}; + +export const PRIVATE_TAG_TX: TransactionTag = { + id: '1', + name: 'placeholder', + transaction_hash: TX_HASH, +}; + +export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = { + address: ADDRESS_PARAMS, + address_balance: '7072643779453701031672', + address_hash: ADDRESS_HASH, + exchange_rate: '0.00099052', + id: '18', + name: 'placeholder', + notification_methods: { + email: false, + }, + notification_settings: { + 'ERC-20': { + incoming: true, + outcoming: true, + }, + 'ERC-721': { + incoming: true, + outcoming: true, + }, + 'ERC-404': { + incoming: true, + outcoming: true, + }, + 'native': { + incoming: true, + outcoming: true, + }, + }, + tokens_count: 42, + tokens_fiat_value: '12345', + tokens_overflow: false, +}; + +export const API_KEY: ApiKey = { + api_key: '9c3ecf44-a1ca-4ff1-b28e-329e8b65f652', + name: 'placeholder', +}; + +export const CUSTOM_ABI: CustomAbi = { + abi: [ + { + constant: false, + payable: false, + inputs: [ { name: 'target', type: 'address' } ], + name: 'unknownWriteMethod', + outputs: [ { name: 'result', type: 'address' } ], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + contract_address: ADDRESS_PARAMS, + contract_address_hash: ADDRESS_HASH, + id: '1', + name: 'placeholder', +}; + +export const VERIFIED_ADDRESS: VerifiedAddress = { + userId: 'john.doe@gmail.com', + chainId: '5', + contractAddress: ADDRESS_HASH, + verifiedDate: '2022-11-11', + metadata: { + tokenName: 'Placeholder Token', + tokenSymbol: 'PLC', + }, +}; + +export const TOKEN_INFO_APPLICATION: TokenInfoApplication = { + id: '1', + tokenAddress: ADDRESS_HASH, + status: 'IN_PROCESS', + updatedAt: '2022-11-11 13:49:48.031453Z', + requesterName: 'John Doe', + requesterEmail: 'john.doe@gmail.com', + projectWebsite: 'http://example.com', + projectEmail: 'info@example.com', + iconUrl: 'https://example.com/100/100', + projectDescription: 'Hello!', + projectSector: 'DeFi', +}; diff --git a/stubs/address.ts b/stubs/address.ts new file mode 100644 index 0000000000..dbdc51d132 --- /dev/null +++ b/stubs/address.ts @@ -0,0 +1,118 @@ +import type { + Address, + AddressCoinBalanceHistoryItem, + AddressCollection, + AddressCounters, + AddressMudTableItem, + AddressNFT, + AddressTabsCounters, + AddressTokenBalance, +} from 'types/api/address'; +import type { AddressesItem } from 'types/api/addresses'; + +import { ADDRESS_HASH } from './addressParams'; +import { MUD_SCHEMA, MUD_TABLE } from './mud'; +import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token'; +import { TX_HASH } from './tx'; + +export const ADDRESS_INFO: Address = { + block_number_balance_updated_at: 8774377, + coin_balance: '810941268802273085757', + creation_tx_hash: null, + creator_address_hash: ADDRESS_HASH, + exchange_rate: null, + has_decompiled_code: false, + has_logs: true, + has_token_transfers: false, + has_tokens: false, + has_validated_blocks: false, + hash: ADDRESS_HASH, + implementations: [ { address: ADDRESS_HASH, name: 'Proxy' } ], + is_contract: true, + is_verified: true, + name: 'ChainLink Token (goerli)', + token: TOKEN_INFO_ERC_20, + private_tags: [], + public_tags: [], + watchlist_names: [], + watchlist_address_id: null, + ens_domain_name: null, +}; + +export const ADDRESS_COUNTERS: AddressCounters = { + gas_usage_count: '8028907522', + token_transfers_count: '420', + transactions_count: '119020', + validations_count: '0', +}; + +export const ADDRESS_TABS_COUNTERS: AddressTabsCounters = { + internal_txs_count: 10, + logs_count: 10, + token_balances_count: 10, + token_transfers_count: 10, + transactions_count: 10, + validations_count: 10, + withdrawals_count: 10, +}; + +export const TOP_ADDRESS: AddressesItem = { + coin_balance: '11886682377162664596540805', + tx_count: '1835', + hash: '0x4f7A67464B5976d7547c860109e4432d50AfB38e', + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [ ], + watchlist_names: [], + ens_domain_name: null, +}; + +export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = { + block_number: 9004413, + block_timestamp: '2023-05-15T13:16:24Z', + delta: '1000000000000000000', + transaction_hash: TX_HASH, + value: '953427250000000000000000', +}; + +export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = { + token: TOKEN_INFO_ERC_20, + token_id: null, + token_instance: null, + value: '1000000000000000000000000', +}; + +export const ADDRESS_NFT_721: AddressNFT = { + token_type: 'ERC-721', + token: TOKEN_INFO_ERC_721, + value: '1', + ...TOKEN_INSTANCE, +}; + +export const ADDRESS_NFT_1155: AddressNFT = { + token_type: 'ERC-1155', + token: TOKEN_INFO_ERC_1155, + value: '10', + ...TOKEN_INSTANCE, +}; + +export const ADDRESS_NFT_404: AddressNFT = { + token_type: 'ERC-404', + token: TOKEN_INFO_ERC_404, + value: '10', + ...TOKEN_INSTANCE, +}; + +export const ADDRESS_COLLECTION: AddressCollection = { + token: TOKEN_INFO_ERC_1155, + amount: '4', + token_instances: Array(4).fill(TOKEN_INSTANCE), +}; + +export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = { + schema: MUD_SCHEMA, + table: MUD_TABLE, +}; diff --git a/stubs/addressParams.ts b/stubs/addressParams.ts new file mode 100644 index 0000000000..8548abca9c --- /dev/null +++ b/stubs/addressParams.ts @@ -0,0 +1,15 @@ +import type { AddressParam } from 'types/api/addressParams'; + +export const ADDRESS_HASH = '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a'; + +export const ADDRESS_PARAMS: AddressParam = { + hash: ADDRESS_HASH, + implementations: null, + is_contract: false, + is_verified: null, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + ens_domain_name: null, +}; diff --git a/stubs/arbitrumL2.ts b/stubs/arbitrumL2.ts new file mode 100644 index 0000000000..44ff30251b --- /dev/null +++ b/stubs/arbitrumL2.ts @@ -0,0 +1,34 @@ +import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatch, ArbitrumL2MessagesItem } from 'types/api/arbitrumL2'; + +import { ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ARBITRUM_MESSAGES_ITEM: ArbitrumL2MessagesItem = { + completion_transaction_hash: TX_HASH, + id: 181920, + origination_address: ADDRESS_HASH, + origination_transaction_block_number: 123456, + origination_transaction_hash: TX_HASH, + origination_timestamp: '2023-06-01T14:46:48.000000Z', + status: 'relayed', +}; + +export const ARBITRUM_L2_TXN_BATCHES_ITEM: ArbitrumL2TxnBatchesItem = { + number: 12345, + blocks_count: 12345, + transactions_count: 10000, + commitment_transaction: { + block_number: 12345, + timestamp: '2024-04-17T08:51:58.000000Z', + hash: TX_HASH, + status: 'finalized', + }, +}; + +export const ARBITRUM_L2_TXN_BATCH: ArbitrumL2TxnBatch = { + ...ARBITRUM_L2_TXN_BATCHES_ITEM, + after_acc: '0xcd064f3409015e8e6407e492e5275a185e492c6b43ccf127f22092d8057a9ffb', + before_acc: '0x2ed7c4985eb778d76ec400a43805e7feecc8c2afcdb492dbe5caf227de6d37bc', + start_block: 1245209, + end_block: 1245490, +}; diff --git a/stubs/blobs.ts b/stubs/blobs.ts new file mode 100644 index 0000000000..89ca5c4cb4 --- /dev/null +++ b/stubs/blobs.ts @@ -0,0 +1,20 @@ +import type { Blob, TxBlob } from 'types/api/blobs'; + +import { TX_HASH } from './tx'; + +const BLOB_HASH = '0x0137cd898a9aaa92bbe94999d2a98241f5eabc829d9354160061789963f85995'; +const BLOB_PROOF = '0x82683d5d6e58a76f2a607b8712cad113621d46cb86a6bcfcffb1e274a70c7308b3243c6075ee22d904fecf8d4c147c6f'; + +export const TX_BLOB: TxBlob = { + blob_data: '0x010203040506070809101112', + hash: BLOB_HASH, + kzg_commitment: BLOB_PROOF, + kzg_proof: BLOB_PROOF, +}; + +export const BLOB: Blob = { + ...TX_BLOB, + transaction_hashes: [ + { block_consensus: true, transaction_hash: TX_HASH }, + ], +}; diff --git a/stubs/block.ts b/stubs/block.ts new file mode 100644 index 0000000000..aea58c499b --- /dev/null +++ b/stubs/block.ts @@ -0,0 +1,37 @@ +import type { Block } from 'types/api/block'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70'; + +export const BLOCK: Block = { + base_fee_per_gas: '14', + burnt_fees: '92834504000000000', + burnt_fees_percentage: 42.2, + difficulty: '340282366920938463463374607431768211451', + extra_data: 'TODO', + gas_limit: '30000000', + gas_target_percentage: 55.79, + gas_used: '6631036', + gas_used_percentage: 22.10, + hash: BLOCK_HASH, + height: 8988736, + miner: ADDRESS_PARAMS, + nonce: '0x0000000000000000', + parent_hash: BLOCK_HASH, + priority_fee: '19241635454943109', + rewards: [ + { + reward: '19241635454943109', + type: 'Validator Reward', + }, + ], + size: 46406, + state_root: 'TODO', + timestamp: '2023-05-12T19:29:12.000000Z', + total_difficulty: '10837812015930321201107455268036056402048391639', + tx_count: 142, + tx_fees: '19241635547777613', + type: 'block', + uncles_hashes: [], +}; diff --git a/stubs/contract.ts b/stubs/contract.ts new file mode 100644 index 0000000000..8dd3aa1993 --- /dev/null +++ b/stubs/contract.ts @@ -0,0 +1,102 @@ +import type { SmartContract, SolidityscanReport } from 'types/api/contract'; +import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const CONTRACT_CODE_UNVERIFIED = { + creation_bytecode: '0x60806040526e', + deployed_bytecode: '0x608060405233', + is_self_destructed: false, +} as SmartContract; + +export const CONTRACT_CODE_VERIFIED = { + abi: [ + { + inputs: [], + name: 'symbol', + outputs: [ { internalType: 'string', name: '', type: 'string' } ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ { internalType: 'address', name: 'newOwner', type: 'address' } ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + additional_sources: [], + can_be_visualized_via_sol2uml: true, + compiler_settings: { + compilationTarget: { + 'contracts/StubContract.sol': 'StubContract', + }, + evmVersion: 'london', + libraries: {}, + metadata: { + bytecodeHash: 'ipfs', + }, + optimizer: { + enabled: false, + runs: 200, + }, + remappings: [], + }, + compiler_version: 'v0.8.7+commit.e28d00a7', + creation_bytecode: '0x6080604052348', + deployed_bytecode: '0x60806040', + evm_version: 'london', + external_libraries: [], + file_path: 'contracts/StubContract.sol', + is_verified: true, + name: 'StubContract', + optimization_enabled: false, + optimization_runs: 200, + source_code: 'source_code', + verified_at: '2023-02-21T14:39:16.906760Z', + license_type: 'mit', +} as unknown as SmartContract; + +export const VERIFIED_CONTRACT_INFO: VerifiedContract = { + address: { ...ADDRESS_PARAMS, name: 'StubContract' }, + coin_balance: '30319033612988277', + compiler_version: 'v0.8.17+commit.8df45f5f', + has_constructor_args: true, + language: 'solidity', + market_cap: null, + optimization_enabled: false, + tx_count: 565058, + verified_at: '2023-04-10T13:16:33.884921Z', + license_type: 'mit', +}; + +export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { + smart_contracts: '123456789', + new_smart_contracts_24h: '12345', + verified_smart_contracts: '654321', + new_verified_smart_contracts_24h: '1234', +}; + +export const SOLIDITYSCAN_REPORT: SolidityscanReport = { + scan_report: { + contractname: 'BullRunners', + scan_status: 'scan_done', + scan_summary: { + issue_severity_distribution: { + critical: 0, + gas: 1, + high: 0, + informational: 0, + low: 2, + medium: 0, + }, + lines_analyzed_count: 18, + scan_time_taken: 1, + score: '3.61', + score_v2: '72.22', + threat_score: '94.74', + }, + scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout', + }, +}; diff --git a/stubs/internalTx.ts b/stubs/internalTx.ts new file mode 100644 index 0000000000..15f0785da9 --- /dev/null +++ b/stubs/internalTx.ts @@ -0,0 +1,19 @@ +import type { InternalTransaction } from 'types/api/internalTransaction'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const INTERNAL_TX: InternalTransaction = { + block: 9006105, + created_contract: null, + error: null, + from: ADDRESS_PARAMS, + gas_limit: '754278', + index: 1, + success: true, + timestamp: '2023-05-15T20:14:00.000000Z', + to: ADDRESS_PARAMS, + transaction_hash: TX_HASH, + type: 'staticcall', + value: '22324344900000000', +}; diff --git a/stubs/log.ts b/stubs/log.ts new file mode 100644 index 0000000000..0200ba4d04 --- /dev/null +++ b/stubs/log.ts @@ -0,0 +1,35 @@ +import type { Log } from 'types/api/log'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const LOG: Log = { + address: ADDRESS_PARAMS, + data: '0x000000000000000000000000000000000000000000000000000000d75e4be200', + decoded: { + method_call: 'CreditSpended(uint256 indexed _type, uint256 _quantity)', + method_id: '58cdf94a', + parameters: [ + { + indexed: true, + name: '_type', + type: 'uint256', + value: 'placeholder', + }, + { + indexed: false, + name: '_quantity', + type: 'uint256', + value: 'placeholder', + }, + ], + }, + index: 42, + topics: [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x000000000000000000000000c52ea157a7fb3e25a069d47df0428ac70cd656b1', + '0x000000000000000000000000302fd86163cb9ad5533b3952dafa3b633a82bc51', + null, + ], + tx_hash: TX_HASH, +}; diff --git a/stubs/marketplace.ts b/stubs/marketplace.ts new file mode 100644 index 0000000000..0671368937 --- /dev/null +++ b/stubs/marketplace.ts @@ -0,0 +1,19 @@ +/* eslint-disable max-len */ +import type { MarketplaceAppOverview } from 'types/client/marketplace'; + +export const MARKETPLACE_APP: MarketplaceAppOverview = { + author: 'StubApp Inc.', + id: 'stub-app', + title: 'My cool app name', + logo: '', + categories: [ + 'Bridge', + ], + shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + site: 'https://example.com', + description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', + external: true, + url: 'https://example.com', +}; + +export const CATEGORIES: Array = Array(9).fill('Bridge').map((c, i) => c + i); diff --git a/stubs/mud.ts b/stubs/mud.ts new file mode 100644 index 0000000000..d49db88913 --- /dev/null +++ b/stubs/mud.ts @@ -0,0 +1,24 @@ +import type { MudWorldItem, MudWorldSchema, MudWorldTable } from 'types/api/mudWorlds'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const MUD_TABLE: MudWorldTable = { + table_full_name: 'ot.Match', + table_id: '0x6f7400000000000000000000000000004d617463680000000000000000000000', + table_name: 'Match', + table_namespace: '', + table_type: 'offchain', +}; + +export const MUD_SCHEMA: MudWorldSchema = { + key_names: [ 'matchEntityKey', 'entity' ], + key_types: [ 'bytes32', 'bytes32' ], + value_names: [ 'matchEntity' ], + value_types: [ 'bytes32' ], +}; + +export const MUD_WORLD: MudWorldItem = { + address: ADDRESS_PARAMS, + coin_balance: '7072643779453701031672', + tx_count: 442, +}; diff --git a/stubs/noves/NovesTranslate.ts b/stubs/noves/NovesTranslate.ts new file mode 100644 index 0000000000..848ed6dab9 --- /dev/null +++ b/stubs/noves/NovesTranslate.ts @@ -0,0 +1,43 @@ +import type { NovesResponseData, NovesClassificationData, NovesRawTransactionData } from 'types/api/noves'; + +const NOVES_TRANSLATE_CLASSIFIED: NovesClassificationData = { + description: 'Sent 0.04 ETH', + received: [ { + action: 'Sent Token', + actionFormatted: 'Sent Token', + amount: '45', + from: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + to: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + token: { + address: '', + name: 'ETH', + symbol: 'ETH', + decimals: 18, + }, + } ], + sent: [], + source: { + type: '', + }, + type: '0x2', + typeFormatted: 'Send NFT', +}; + +const NOVES_TRANSLATE_RAW: NovesRawTransactionData = { + blockNumber: 1, + fromAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + gas: 2, + gasPrice: 3, + timestamp: 20000, + toAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + transactionFee: 2, + transactionHash: '0x128b79937a0eDE33258992c9668455f997f1aF24', +}; + +export const NOVES_TRANSLATE: NovesResponseData = { + accountAddress: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + chain: 'base', + txTypeVersion: 2, + rawTransactionData: NOVES_TRANSLATE_RAW, + classificationData: NOVES_TRANSLATE_CLASSIFIED, +}; diff --git a/stubs/search.ts b/stubs/search.ts new file mode 100644 index 0000000000..77d1662a89 --- /dev/null +++ b/stubs/search.ts @@ -0,0 +1,30 @@ +import type { SearchResult, SearchResultItem } from 'types/api/search'; + +import { ADDRESS_HASH } from './addressParams'; + +export const SEARCH_RESULT_ITEM: SearchResultItem = { + address: ADDRESS_HASH, + address_url: '/address/0x3714A8C7824B22271550894f7555f0a672f97809', + name: 'USDC', + symbol: 'USDC', + token_url: '/token/0x3714A8C7824B22271550894f7555f0a672f97809', + type: 'token', + icon_url: null, + is_verified_via_admin_panel: false, + is_smart_contract_verified: false, + exchange_rate: '1.11', + total_supply: null, + token_type: 'ERC-20', +}; + +export const SEARCH_RESULT_NEXT_PAGE_PARAMS: SearchResult['next_page_params'] = { + address_hash: ADDRESS_HASH, + block_hash: null, + holder_count: 11, + inserted_at: '2023-05-19T17:21:19.203681Z', + item_type: 'token', + items_count: 50, + name: 'USDCTest', + q: 'usd', + tx_hash: null, +}; diff --git a/stubs/shibarium.ts b/stubs/shibarium.ts new file mode 100644 index 0000000000..3003be7110 --- /dev/null +++ b/stubs/shibarium.ts @@ -0,0 +1,20 @@ +import type { ShibariumDepositsItem, ShibariumWithdrawalsItem } from 'types/api/shibarium'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TX_HASH } from './tx'; + +export const SHIBARIUM_DEPOSIT_ITEM: ShibariumDepositsItem = { + l1_block_number: 9045233, + l1_transaction_hash: TX_HASH, + l2_transaction_hash: TX_HASH, + timestamp: '2023-05-22T18:00:36.000000Z', + user: ADDRESS_PARAMS, +}; + +export const SHIBARIUM_WITHDRAWAL_ITEM: ShibariumWithdrawalsItem = { + l2_block_number: 9045233, + l1_transaction_hash: TX_HASH, + l2_transaction_hash: TX_HASH, + timestamp: '2023-05-22T18:00:36.000000Z', + user: ADDRESS_PARAMS, +}; diff --git a/stubs/stats.ts b/stubs/stats.ts new file mode 100644 index 0000000000..e1e70724fb --- /dev/null +++ b/stubs/stats.ts @@ -0,0 +1,86 @@ +import type * as stats from '@blockscout/stats-types'; +import type { HomeStats } from 'types/api/stats'; + +export const HOMEPAGE_STATS: HomeStats = { + average_block_time: 14346, + coin_price: '1807.68', + coin_price_change_percentage: 42, + gas_prices: { + average: { + fiat_price: '1.01', + price: 20.41, + time: 12283, + base_fee: 2.22222, + priority_fee: 12.424242, + }, + fast: { + fiat_price: '1.26', + price: 25.47, + time: 9321, + base_fee: 4.44444, + priority_fee: 22.242424, + }, + slow: { + fiat_price: '0.97', + price: 19.55, + time: 24543, + base_fee: 1.11111, + priority_fee: 7.8909, + }, + }, + gas_price_updated_at: '2022-11-11T11:09:49.051171Z', + gas_prices_update_in: 300000, + gas_used_today: '0', + market_cap: '0', + network_utilization_percentage: 22.56, + static_gas_price: null, + total_addresses: '28634064', + total_blocks: '8940150', + total_gas_used: '0', + total_transactions: '193823272', + transactions_today: '0', + tvl: '1767425.102766552', +}; + +export const STATS_CHARTS_SECTION: stats.LineChartSection = { + id: 'placeholder', + title: 'Placeholder', + charts: [ + { + id: 'chart_0', + title: 'Average transaction fee', + description: 'The average amount in ETH spent per transaction', + units: 'ETH', + }, + { + id: 'chart_1', + title: 'Transactions fees', + description: 'Amount of tokens paid as fees', + units: 'ETH', + }, + { + id: 'chart_2', + title: 'New transactions', + description: 'New transactions number', + units: undefined, + }, + { + id: 'chart_3', + title: 'Transactions growth', + description: 'Cumulative transactions number', + units: undefined, + }, + ], +}; + +export const STATS_CHARTS = { + sections: [ STATS_CHARTS_SECTION ], +}; + +export const STATS_COUNTER: stats.Counter = { + id: 'stub', + value: '9074405', + title: 'Placeholder Counter', + description: 'Placeholder description', + units: '', +}; diff --git a/stubs/token.ts b/stubs/token.ts new file mode 100644 index 0000000000..756a311ca0 --- /dev/null +++ b/stubs/token.ts @@ -0,0 +1,176 @@ +import type { + TokenCounters, + TokenHolder, + TokenHolders, + TokenHoldersPagination, + TokenInfo, + TokenInstance, + TokenType, +} from 'types/api/token'; +import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } from 'types/api/tokens'; +import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { BLOCK_HASH } from './block'; +import { TX_HASH } from './tx'; +import { generateListStub } from './utils'; + +export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = { + address: ADDRESS_HASH, + circulating_market_cap: '117629601.61913824', + decimals: '18', + exchange_rate: '0.999997', + holders: '16026', + name: 'Stub Token (goerli)', + symbol: 'STUB', + total_supply: '60000000000000000000000', + type: 'ERC-20', + icon_url: null, +}; + +export const TOKEN_INFO_ERC_721: TokenInfo<'ERC-721'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-721', +}; + +export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-1155', +}; + +export const TOKEN_INFO_ERC_404: TokenInfo<'ERC-404'> = { + ...TOKEN_INFO_ERC_20, + circulating_market_cap: null, + type: 'ERC-404', +}; + +export const TOKEN_COUNTERS: TokenCounters = { + token_holders_count: '123456', + transfers_count: '123456', +}; + +export const TOKEN_HOLDER_ERC_20: TokenHolder = { + address: ADDRESS_PARAMS, + value: '1021378038331138520', +}; + +export const TOKEN_HOLDER_ERC_1155: TokenHolder = { + address: ADDRESS_PARAMS, + token_id: '12345', + value: '1021378038331138520', +}; + +export const getTokenHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination }); + default: + return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination }); + } +}; + +export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination }); + default: + return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination }); + } +}; + +export const TOKEN_TRANSFER_ERC_20: TokenTransfer = { + block_hash: BLOCK_HASH, + from: ADDRESS_PARAMS, + log_index: '4', + method: 'addLiquidity', + timestamp: '2022-06-24T10:22:11.000000Z', + to: ADDRESS_PARAMS, + token: TOKEN_INFO_ERC_20, + total: { + decimals: '18', + value: '9851351626684503', + }, + tx_hash: TX_HASH, + type: 'token_minting', +}; + +export const TOKEN_TRANSFER_ERC_721: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + }, + token: TOKEN_INFO_ERC_721, +}; + +export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + value: '123', + decimals: '18', + }, + token: TOKEN_INFO_ERC_1155, +}; + +export const TOKEN_TRANSFER_ERC_404: TokenTransfer = { + ...TOKEN_TRANSFER_ERC_20, + total: { + token_id: '35870', + value: '123', + decimals: '18', + }, + token: TOKEN_INFO_ERC_404, +}; + +export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_404, 50, { next_page_params: pagination }); + default: + return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination }); + } +}; + +export const getTokenInstanceTransfersStub = (type?: TokenType, pagination: TokenInstanceTransferPagination | null = null): TokenInstanceTransferResponse => { + switch (type) { + case 'ERC-721': + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_721, 10, { next_page_params: pagination }); + case 'ERC-1155': + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_1155, 10, { next_page_params: pagination }); + case 'ERC-404': + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_404, 10, { next_page_params: pagination }); + default: + return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_20, 10, { next_page_params: pagination }); + } +}; + +export const TOKEN_INSTANCE: TokenInstance = { + animation_url: null, + external_app_url: 'https://vipsland.com/nft/collections/genesis/188882', + id: '188882', + image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif', + is_unique: true, + metadata: { + attributes: Array(3).fill({ trait_type: 'skin tone', value: 'very light skin tone' }), + description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*', + external_url: 'https://vipsland.com/nft/collections/genesis/188882', + image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif', + name: 'GENESIS #188882, 8a77ca1bcaa4036f', + }, + owner: ADDRESS_PARAMS, + holder_address_hash: ADDRESS_HASH, +}; diff --git a/stubs/tx.ts b/stubs/tx.ts new file mode 100644 index 0000000000..2e80408689 --- /dev/null +++ b/stubs/tx.ts @@ -0,0 +1,68 @@ +import type { RawTracesResponse } from 'types/api/rawTrace'; +import type { Transaction, TransactionsStats } from 'types/api/transaction'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967'; + +export const TX: Transaction = { + timestamp: '2022-11-11T11:11:11.000000Z', + fee: { + type: 'actual', + value: '2100000000000000', + }, + gas_limit: '21000', + block: 9004925, + status: 'ok', + method: 'placeholder', + confirmations: 71, + type: 0, + exchange_rate: '1828.71', + to: ADDRESS_PARAMS, + tx_burnt_fee: null, + max_fee_per_gas: null, + result: 'success', + hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + gas_price: '100000000000', + priority_fee: null, + base_fee_per_gas: '24', + from: ADDRESS_PARAMS, + token_transfers: null, + tx_types: [ + 'coin_transfer', + ], + gas_used: '21000', + created_contract: null, + position: 0, + nonce: 295929, + has_error_in_internal_txs: false, + actions: [], + decoded_input: null, + token_transfers_overflow: false, + raw_input: '0x', + value: '42000420000000000000', + max_priority_fee_per_gas: null, + revert_reason: null, + confirmation_duration: [ + 0, + 14545, + ], + tx_tag: null, +}; + +export const TX_ZKEVM_L2: Transaction = { + ...TX, + zkevm_batch_number: 12345, + zkevm_sequence_hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + zkevm_status: 'Confirmed by Sequencer', + zkevm_verify_hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', +}; + +export const TX_RAW_TRACE: RawTracesResponse = []; + +export const TXS_STATS: TransactionsStats = { + pending_transactions_count: '4200', + transaction_fees_avg_24h: '22342870314428', + transaction_fees_sum_24h: '22184012506492688277', + transactions_count_24h: '992890', +}; diff --git a/stubs/txInterpretation.ts b/stubs/txInterpretation.ts new file mode 100644 index 0000000000..e54c2cddae --- /dev/null +++ b/stubs/txInterpretation.ts @@ -0,0 +1,34 @@ +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; + +import { TOKEN_INFO_ERC_20 } from './token'; + +export const TX_INTERPRETATION: TxInterpretationResponse = { + data: { + summaries: [ + { + summary_template: '{action_type} {source_amount} Ether into {destination_amount} {destination_token}', + summary_template_variables: { + action_type: { type: 'string', value: 'Wrap' }, + source_amount: { type: 'currency', value: '0.7' }, + destination_amount: { type: 'currency', value: '0.7' }, + destination_token: { + type: 'token', + value: TOKEN_INFO_ERC_20, + }, + }, + }, + { + summary_template: '{action_type} {source_amount} Ether into {destination_amount} {destination_token}', + summary_template_variables: { + action_type: { type: 'string', value: 'Wrap' }, + source_amount: { type: 'currency', value: '0.7' }, + destination_amount: { type: 'currency', value: '0.7' }, + destination_token: { + type: 'token', + value: TOKEN_INFO_ERC_20, + }, + }, + }, + ], + }, +}; diff --git a/stubs/txStateChanges.ts b/stubs/txStateChanges.ts new file mode 100644 index 0000000000..7a26ee8a77 --- /dev/null +++ b/stubs/txStateChanges.ts @@ -0,0 +1,47 @@ +import type { TxStateChange } from 'types/api/txStateChanges'; + +import { ADDRESS_PARAMS } from './addressParams'; +import { TOKEN_INFO_ERC_721 } from './token'; + +export const STATE_CHANGE_MINER: TxStateChange = { + address: ADDRESS_PARAMS, + balance_after: '124280364215547113', + balance_before: '123405277440098758', + change: '875086775448355', + is_miner: true, + token: null, + type: 'coin', +}; + +export const STATE_CHANGE_COIN: TxStateChange = { + address: ADDRESS_PARAMS, + balance_after: '61659392141463351540', + balance_before: '61660292436225994690', + change: '-900294762600000', + is_miner: false, + token: null, + type: 'coin', +}; + +export const STATE_CHANGE_TOKEN: TxStateChange = { + address: ADDRESS_PARAMS, + balance_after: '43', + balance_before: '42', + change: [ + { + direction: 'to', + total: { + token_id: '1621395', + }, + }, + ], + is_miner: false, + token: TOKEN_INFO_ERC_721, + type: 'token', +}; + +export const TX_STATE_CHANGES: Array = [ + STATE_CHANGE_MINER, + STATE_CHANGE_COIN, + STATE_CHANGE_TOKEN, +]; diff --git a/stubs/userOps.ts b/stubs/userOps.ts new file mode 100644 index 0000000000..283e43c4fd --- /dev/null +++ b/stubs/userOps.ts @@ -0,0 +1,69 @@ +import type { UserOpsItem, UserOp, UserOpsAccount } from 'types/api/userOps'; + +import { ADDRESS_HASH } from './addressParams'; +import { BLOCK_HASH } from './block'; +import { TX_HASH } from './tx'; + +const USER_OP_HASH = '0xb94fab8f31f83001a23e20b2ce3cdcfb284c57a64b9a073e0e09c018bc701978'; + +export const USER_OPS_ITEM: UserOpsItem = { + hash: USER_OP_HASH, + block_number: '10356381', + transaction_hash: TX_HASH, + address: ADDRESS_HASH, + timestamp: '2023-12-18T10:48:49.000000Z', + status: true, + fee: '48285720012071430', +}; + +export const USER_OP: UserOp = { + hash: USER_OP_HASH, + sender: ADDRESS_HASH, + nonce: '0x00b', + call_data: '0x123', + execute_call_data: null, + decoded_call_data: null, + decoded_execute_call_data: null, + call_gas_limit: '71316', + verification_gas_limit: '91551', + pre_verification_gas: '53627', + max_fee_per_gas: '100000020', + max_priority_fee_per_gas: '100000000', + signature: '0x000', + aggregator: null, + aggregator_signature: null, + entry_point: ADDRESS_HASH, + transaction_hash: TX_HASH, + block_number: '10358181', + block_hash: BLOCK_HASH, + bundler: ADDRESS_HASH, + factory: null, + paymaster: ADDRESS_HASH, + status: true, + revert_reason: null, + gas: '399596', + gas_price: '1575000898', + gas_used: '118810', + sponsor_type: 'paymaster_sponsor', + fee: '17927001792700', + timestamp: '2023-12-18T10:48:49.000000Z', + user_logs_count: 1, + user_logs_start_index: 2, + raw: { + sender: ADDRESS_HASH, + nonce: '1', + init_code: '0x', + call_data: '0x345', + call_gas_limit: '29491', + verification_gas_limit: '80734', + pre_verification_gas: '3276112', + max_fee_per_gas: '309847206', + max_priority_fee_per_gas: '100000000', + paymaster_and_data: '0x', + signature: '0x000', + }, +}; + +export const USER_OPS_ACCOUNT: UserOpsAccount = { + total_ops: 1, +}; diff --git a/stubs/utils.ts b/stubs/utils.ts new file mode 100644 index 0000000000..ef825f9252 --- /dev/null +++ b/stubs/utils.ts @@ -0,0 +1,14 @@ +import type { ArrayElement } from 'types/utils'; + +import type { PaginatedResources, PaginatedResponse, PaginatedResponseItems } from 'lib/api/resources'; + +export function generateListStub( + stub: ArrayElement>, + num = 50, + rest: Omit, 'items'>, +) { + return { + items: Array(num).fill(stub), + ...rest, + }; +} diff --git a/stubs/validators.ts b/stubs/validators.ts new file mode 100644 index 0000000000..1717a59ec0 --- /dev/null +++ b/stubs/validators.ts @@ -0,0 +1,16 @@ +import type { Validator, ValidatorsCountersResponse } from 'types/api/validators'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const VALIDATOR: Validator = { + address: ADDRESS_PARAMS, + blocks_validated_count: 25987, + state: 'active', +}; + +export const VALIDATORS_COUNTERS: ValidatorsCountersResponse = { + active_validators_counter: '42', + active_validators_percentage: 7.14, + new_validators_counter_24h: '11', + validators_counter: '140', +}; diff --git a/stubs/withdrawals.ts b/stubs/withdrawals.ts new file mode 100644 index 0000000000..25fbb4a826 --- /dev/null +++ b/stubs/withdrawals.ts @@ -0,0 +1,12 @@ +import type { WithdrawalsItem } from 'types/api/withdrawals'; + +import { ADDRESS_PARAMS } from './addressParams'; + +export const WITHDRAWAL: WithdrawalsItem = { + amount: '12565723', + index: 3810697, + receiver: ADDRESS_PARAMS, + validator_index: 25987, + block_number: 9005713, + timestamp: '2023-05-12T19:29:12.000000Z', +}; diff --git a/stubs/zkEvmL2.ts b/stubs/zkEvmL2.ts new file mode 100644 index 0000000000..46015e0ccf --- /dev/null +++ b/stubs/zkEvmL2.ts @@ -0,0 +1,44 @@ +import type { ZkEvmL2DepositsItem, ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2'; + +import { TX_HASH } from './tx'; + +export const ZKEVM_DEPOSITS_ITEM: ZkEvmL2DepositsItem = { + block_number: 19674901, + index: 181920, + l1_transaction_hash: '0xa74edfa5824a07a5f95ca1145140ed589df7f05bb17796bf18090b14c4566b5d', + l2_transaction_hash: '0x436d1c7ada270466ca0facdb96ecc22934d68d13b8a08f541b8df11b222967b5', + symbol: 'ETH', + timestamp: '2023-06-01T14:46:48.000000Z', + value: '0.13040262', +}; + +export const ZKEVM_WITHDRAWALS_ITEM: ZkEvmL2WithdrawalsItem = { + block_number: 11692968, + index: 47003, + l1_transaction_hash: '0x230cf46dabea287ac7d0ba83b8ea120bb83c1de58a81d34f44788f0459096c52', + l2_transaction_hash: '0x519d9f025ec47f08a48d708964d177189d2246ddf988686c481f5debcf097e34', + symbol: 'ETH', + timestamp: '2024-04-17T08:51:58.000000Z', + value: '110.35', +}; + +export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = { + timestamp: '2023-06-01T14:46:48.000000Z', + status: 'Finalized', + verify_tx_hash: TX_HASH, + sequence_tx_hash: TX_HASH, + number: 5218590, + tx_count: 9, +}; + +export const ZKEVM_L2_TXN_BATCH: ZkEvmL2TxnBatch = { + acc_input_hash: '0xb815fe2832977f1324ad0124a019b938f189f7b470292f40a21284f15774b3b3', + global_exit_root: '0x0000000000000000000000000000000000000000000000000000000000000000', + number: 1, + sequence_tx_hash: '0x57b9b95db5f94f125710bdc8fbb3fabaac10125b44b0cb61dbc69daddf06d0cd', + state_root: '0xb9a589d6b3ae44d3b250a9993caa5e3721568197f56e4743989ecb2285d80ec4', + status: 'Finalized', + timestamp: '2023-09-15T06:22:48.000000Z', + transactions: [ '0xff99dd67646b8f3d657cc6f19eb33abc346de2dbaccd03e45e7726cc28e3e186' ], + verify_tx_hash: '0x093276fa65c67d7b12dd96f4fefafba9d9ad2f1c23c6e53f96583971ce75352d', +}; diff --git a/stubs/zkSyncL2.ts b/stubs/zkSyncL2.ts new file mode 100644 index 0000000000..9d3782abcf --- /dev/null +++ b/stubs/zkSyncL2.ts @@ -0,0 +1,27 @@ +import type { ZkSyncBatch, ZkSyncBatchesItem } from 'types/api/zkSyncL2'; + +import { TX_HASH } from './tx'; + +export const ZKSYNC_L2_TXN_BATCHES_ITEM: ZkSyncBatchesItem = { + commit_transaction_hash: TX_HASH, + commit_transaction_timestamp: '2022-03-17T19:33:04.519145Z', + execute_transaction_hash: TX_HASH, + execute_transaction_timestamp: '2022-03-17T20:49:48.856345Z', + number: 8002, + prove_transaction_hash: TX_HASH, + prove_transaction_timestamp: '2022-03-17T20:49:48.772442Z', + status: 'Executed on L1', + timestamp: '2022-03-17T17:00:11.000000Z', + tx_count: 1215, +}; + +export const ZKSYNC_L2_TXN_BATCH: ZkSyncBatch = { + ...ZKSYNC_L2_TXN_BATCHES_ITEM, + start_block: 1245209, + end_block: 1245490, + l1_gas_price: '4173068062', + l1_tx_count: 0, + l2_fair_gas_price: '100000000', + l2_tx_count: 287, + root_hash: '0x108c635b94f941fcabcb85500daec2f6be4f0747dff649b1cdd9dd7a7a264792', +}; diff --git a/svgo.config.js b/svgo.config.js new file mode 100644 index 0000000000..1c3d7c355d --- /dev/null +++ b/svgo.config.js @@ -0,0 +1,18 @@ +module.exports = { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + removeHiddenElems: false, + }, + }, + }, + 'removeDimensions', + ], + js2svg: { + indent: 2, + pretty: true, + }, +}; diff --git a/theme/components/Alert/Alert.pw.tsx b/theme/components/Alert/Alert.pw.tsx new file mode 100644 index 0000000000..6314658e43 --- /dev/null +++ b/theme/components/Alert/Alert.pw.tsx @@ -0,0 +1,43 @@ +import type { AlertProps } from '@chakra-ui/react'; +import { Alert, AlertIcon, AlertTitle } from '@chakra-ui/react'; +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +test.use({ viewport: { width: 400, height: 720 } }); + +const TEST_CASES: Array = [ + { + status: 'info', + }, + { + status: 'warning', + }, + { + status: 'success', + }, + { + status: 'error', + }, + { + status: 'info', + colorScheme: 'gray', + }, +]; + +TEST_CASES.forEach((props) => { + const testName = Object.entries(props).map(([ key, value ]) => `${ key }=${ value }`).join(', '); + + test(`${ testName } +@dark-mode`, async({ render }) => { + const component = await render( + + + + This is alert text + + , + ); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/theme/components/Alert/Alert.ts b/theme/components/Alert/Alert.ts new file mode 100644 index 0000000000..457511c2d7 --- /dev/null +++ b/theme/components/Alert/Alert.ts @@ -0,0 +1,97 @@ +import { alertAnatomy as parts } from '@chakra-ui/anatomy'; +import type { StyleFunctionProps } from '@chakra-ui/styled-system'; +import { createMultiStyleConfigHelpers, cssVar } from '@chakra-ui/styled-system'; +import { transparentize } from '@chakra-ui/theme-tools'; + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys); + +const $fg = cssVar('alert-fg'); +const $bg = cssVar('alert-bg'); + +function getBg(props: StyleFunctionProps) { + const { theme, colorScheme: c } = props; + const darkBg = transparentize(`${ c }.200`, 0.16)(theme); + return { + light: `colors.${ c }.${ c === 'red' ? '50' : '100' }`, + dark: darkBg, + }; +} + +const baseStyle = definePartsStyle({ + container: { + bg: $bg.reference, + borderRadius: 'md', + px: 6, + py: 3, + }, + title: { + fontWeight: 'bold', + lineHeight: '6', + marginEnd: '2', + }, + description: { + lineHeight: '6', + }, + icon: { + color: $fg.reference, + flexShrink: 0, + marginEnd: '3', + w: '5', + h: '6', + }, + spinner: { + color: $fg.reference, + flexShrink: 0, + marginEnd: '3', + w: '5', + h: '5', + }, +}); + +const variantSubtle = definePartsStyle((props) => { + const { colorScheme } = props; + const bg = getBg(props); + + return { + container: { + [$fg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.800' : `colors.${ colorScheme }.500`, + [$bg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.100' : bg.light, + _dark: { + [$fg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.800' : `colors.${ colorScheme }.200`, + [$bg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.200' : bg.dark, + }, + }, + }; +}); + +const variantSolid = definePartsStyle((props) => { + const { colorScheme: c } = props; + return { + container: { + [$fg.variable]: `colors.white`, + [$bg.variable]: `colors.${ c }.500`, + _dark: { + [$fg.variable]: `colors.gray.900`, + [$bg.variable]: `colors.${ c }.200`, + }, + color: $fg.reference, + }, + }; +}); + +const variants = { + subtle: variantSubtle, + solid: variantSolid, +}; + +const Alert = defineMultiStyleConfig({ + baseStyle, + variants, + defaultProps: { + variant: 'subtle', + colorScheme: 'blue', + }, +}); + +export default Alert; diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-error-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-error-dark-mode-1.png new file mode 100644 index 0000000000..0d7cf86918 Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-error-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-info-colorScheme-gray-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-info-colorScheme-gray-dark-mode-1.png new file mode 100644 index 0000000000..44fa15ebd5 Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-info-colorScheme-gray-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-info-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-info-dark-mode-1.png new file mode 100644 index 0000000000..e2666ea5d4 Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-info-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-success-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-success-dark-mode-1.png new file mode 100644 index 0000000000..1e0f949386 Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-success-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-warning-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-warning-dark-mode-1.png new file mode 100644 index 0000000000..d3a7652e7d Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_dark-color-mode_status-warning-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-error-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-error-dark-mode-1.png new file mode 100644 index 0000000000..2c591eb46a Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-error-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-info-colorScheme-gray-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-info-colorScheme-gray-dark-mode-1.png new file mode 100644 index 0000000000..8353f5d3f8 Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-info-colorScheme-gray-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-info-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-info-dark-mode-1.png new file mode 100644 index 0000000000..25af83257a Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-info-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-success-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-success-dark-mode-1.png new file mode 100644 index 0000000000..59d97008ca Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-success-dark-mode-1.png differ diff --git a/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-warning-dark-mode-1.png b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-warning-dark-mode-1.png new file mode 100644 index 0000000000..e80b215d6b Binary files /dev/null and b/theme/components/Alert/__screenshots__/Alert.pw.tsx_default_status-warning-dark-mode-1.png differ diff --git a/theme/components/Badge.ts b/theme/components/Badge.ts new file mode 100644 index 0000000000..b673b528f6 --- /dev/null +++ b/theme/components/Badge.ts @@ -0,0 +1,46 @@ +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +const baseStyle = defineStyle({ + fontSize: 'xs', + borderRadius: 'sm', + fontWeight: 'bold', +}); + +const variantSubtle = defineStyle((props) => { + const { colorScheme: c } = props; + + if (c === 'gray') { + return { + bg: mode('blackAlpha.50', 'whiteAlpha.100')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + }; + } + + if (c === 'gray-blue') { + return { + bg: mode('gray.100', 'gray.800')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + }; + } + + return { + bg: mode(`${ c }.50`, `${ c }.800`)(props), + color: mode(`${ c }.500`, `${ c }.100`)(props), + }; +}); + +const variants = { + subtle: variantSubtle, +}; + +const Badge = defineStyleConfig({ + baseStyle, + variants, + defaultProps: { + variant: 'subtle', + colorScheme: 'gray', + }, +}); + +export default Badge; diff --git a/theme/components/Button/Button.pw.tsx b/theme/components/Button/Button.pw.tsx new file mode 100644 index 0000000000..5e50dbf2c8 --- /dev/null +++ b/theme/components/Button/Button.pw.tsx @@ -0,0 +1,75 @@ +import { Box, Button, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +test.use({ viewport: { width: 150, height: 350 } }); + +[ + { variant: 'solid', states: [ 'default', 'disabled', 'hovered', 'active' ] }, + { variant: 'outline', colorScheme: 'gray', withDarkMode: true, states: [ 'default', 'disabled', 'hovered', 'active', 'selected' ] }, + { variant: 'outline', colorScheme: 'blue', withDarkMode: true, states: [ 'default', 'disabled', 'hovered', 'active', 'selected' ] }, + { variant: 'simple', withDarkMode: true, states: [ 'default', 'hovered' ] }, + { variant: 'ghost', withDarkMode: true, states: [ 'default', 'hovered', 'active' ] }, + { variant: 'subtle', states: [ 'default', 'hovered' ] }, + { variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true }, +].forEach(({ variant, colorScheme, withDarkMode, states }) => { + test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => { + test('', async({ render }) => { + const component = await render( + + { states?.map((state) => { + switch (state) { + case 'default': { + return ( + + Default: + + + ); + } + case 'disabled': { + return ( + + Disabled: + + + ); + } + case 'active': { + return ( + + Active: + + + ); + } + case 'hovered': { + return ( + + Hovered: + + + ); + } + case 'selected': { + return ( + + Selected: + + + ); + } + + default: { + return null; + } + } + }) } + , + ); + await component.getByText('Hover me').hover(); + await expect(component).toHaveScreenshot(); + }); + }); +}); diff --git a/theme/components/Button/Button.ts b/theme/components/Button/Button.ts new file mode 100644 index 0000000000..0cac051ad2 --- /dev/null +++ b/theme/components/Button/Button.ts @@ -0,0 +1,208 @@ +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; +import { runIfFn } from '@chakra-ui/utils'; + +const variantSolid = defineStyle((props) => { + const { colorScheme: c } = props; + + const bg = `${ c }.600`; + const color = 'white'; + const hoverBg = `${ c }.400`; + const activeBg = hoverBg; + + return { + bg, + color, + _hover: { + bg: hoverBg, + _disabled: { + bg, + }, + }, + _disabled: { + opacity: 0.2, + }, + // According to design there is no "active" or "pressed" state + // It is simply should be the same as the "hover" state + _active: { bg: activeBg }, + fontWeight: 600, + }; +}); + +const variantOutline = defineStyle((props) => { + const { colorScheme: c } = props; + + const isGrayTheme = c === 'gray'; + + const bg = 'transparent'; + + const color = isGrayTheme ? mode('blackAlpha.800', 'whiteAlpha.800')(props) : mode(`${ c }.600`, `${ c }.300`)(props); + const borderColor = isGrayTheme ? mode('gray.200', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props); + + const selectedBg = isGrayTheme ? mode('blue.50', 'gray.600')(props) : mode(`${ c }.50`, 'gray.600')(props); + const selectedColor = mode('blue.600', 'gray.50')(props); + + return { + color, + fontWeight: props.fontWeight || 600, + borderWidth: props.borderWidth || '2px', + borderStyle: 'solid', + borderColor, + bg, + _hover: { + color: 'link_hovered', + borderColor: 'link_hovered', + bg, + span: { + color: 'link_hovered', + }, + _disabled: { + color, + borderColor, + }, + }, + _disabled: { + opacity: 0.2, + }, + // According to design there is no "active" or "pressed" state + // It is simply should be the same as the "hover" state + _active: { + color: 'link_hovered', + borderColor: 'link_hovered', + bg, + span: { + color: 'link_hovered', + }, + _disabled: { + color: 'link_hovered', + borderColor: 'link_hovered', + }, + }, + // We have a special state for this button variant that serves as a popover trigger. + // When any items (filters) are selected in the popover, the button should change its background and text color. + // The last CSS selector is for redefining styles for the TabList component. + [` + &[data-selected=true], + &[data-selected=true][aria-selected=true] + `]: { + bg: selectedBg, + color: selectedColor, + borderColor: selectedBg, + }, + }; +}); + +const variantSimple = defineStyle((props) => { + const outline = runIfFn(variantOutline, props); + + return { + color: outline.color, + _hover: { + color: outline._hover.color, + }, + }; +}); + +const variantGhost = defineStyle((props) => { + const { colorScheme: c } = props; + const activeBg = mode(`${ c }.50`, 'gray.800')(props); + + return { + bg: 'transparent', + color: mode(`${ c }.700`, 'gray.400')(props), + _active: { + color: mode(`${ c }.700`, 'gray.50')(props), + bg: mode(`${ c }.50`, 'gray.800')(props), + }, + _hover: { + color: `${ c }.400`, + _active: { + bg: props.isActive ? activeBg : 'transparent', + color: mode(`${ c }.700`, 'gray.50')(props), + }, + }, + }; +}); + +const variantSubtle = defineStyle((props) => { + const { colorScheme: c } = props; + + if (c === 'gray') { + return { + bg: mode('blackAlpha.200', 'whiteAlpha.200')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + _hover: { + color: 'link_hovered', + _disabled: { + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + bg: mode('blackAlpha.200', 'whiteAlpha.200')(props), + }, + }, + }; + } + + return { + bg: `${ c }.100`, + color: `${ c }.600`, + _hover: { + color: 'link_hovered', + }, + }; +}); + +const variants = { + solid: variantSolid, + outline: variantOutline, + simple: variantSimple, + ghost: variantGhost, + subtle: variantSubtle, +}; + +const baseStyle = defineStyle({ + fontWeight: 600, + borderRadius: 'base', + overflow: 'hidden', + _focusVisible: { + boxShadow: { base: 'none', lg: 'outline' }, + }, +}); + +const sizes = { + lg: defineStyle({ + h: 12, + minW: 'unset', + fontSize: 'lg', + px: 6, + }), + md: defineStyle({ + h: 10, + minW: 'unset', + fontSize: 'md', + px: 4, + }), + sm: defineStyle({ + h: 8, + minW: 'unset', + fontSize: 'sm', + px: 3, + }), + xs: defineStyle({ + h: 6, + minW: 'unset', + fontSize: 'xs', + px: 2, + }), +}; + +const Button = defineStyleConfig({ + baseStyle, + variants, + sizes, + defaultProps: { + variant: 'solid', + size: 'md', + colorScheme: 'blue', + }, +}); + +export default Button; diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-ghost-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-ghost-dark-mode-1.png new file mode 100644 index 0000000000..a8c51a2f69 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-ghost-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-outline-with-blue-color-scheme-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-outline-with-blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..ea247969a5 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-outline-with-blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-outline-with-gray-color-scheme-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-outline-with-gray-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..8450e14238 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-outline-with-gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-simple-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-simple-dark-mode-1.png new file mode 100644 index 0000000000..e6bda7dbc4 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-simple-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-subtle-with-gray-color-scheme-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-subtle-with-gray-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..a54cabfe22 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-subtle-with-gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-ghost-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-ghost-dark-mode-1.png new file mode 100644 index 0000000000..11a01abe7d Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-ghost-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-outline-with-blue-color-scheme-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-outline-with-blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..698e13cc7b Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-outline-with-blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-outline-with-gray-color-scheme-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-outline-with-gray-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..4e623efadf Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-outline-with-gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-simple-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-simple-dark-mode-1.png new file mode 100644 index 0000000000..94f93a73b5 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-simple-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-solid-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-solid-1.png new file mode 100644 index 0000000000..3bc86f3500 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-solid-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-subtle-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-subtle-1.png new file mode 100644 index 0000000000..69be1fa9a7 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-subtle-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-subtle-with-gray-color-scheme-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-subtle-with-gray-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..95cf92e089 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-subtle-with-gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Checkbox.ts b/theme/components/Checkbox.ts new file mode 100644 index 0000000000..7a24f98e0c --- /dev/null +++ b/theme/components/Checkbox.ts @@ -0,0 +1,66 @@ +import { checkboxAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + defineStyle, + cssVar, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; +import { runIfFn } from '@chakra-ui/utils'; + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys); + +const $size = cssVar('checkbox-size'); + +const baseStyleControl = defineStyle((props) => { + const { colorScheme: c } = props; + + return { + _checked: { + bg: mode(`${ c }.500`, `${ c }.300`)(props), + borderColor: mode(`${ c }.500`, `${ c }.300`)(props), + _hover: { + bg: mode(`${ c }.600`, `${ c }.400`)(props), + borderColor: mode(`${ c }.600`, `${ c }.400`)(props), + }, + }, + _indeterminate: { + bg: mode(`${ c }.500`, `${ c }.300`)(props), + borderColor: mode(`${ c }.500`, `${ c }.300`)(props), + }, + }; +}); + +const sizes = { + sm: definePartsStyle({ + control: { [$size.variable]: 'sizes.3' }, + label: { fontSize: 'sm' }, + icon: { fontSize: '3xs' }, + }), + md: definePartsStyle({ + control: { [$size.variable]: 'sizes.4' }, + label: { fontSize: 'md' }, + icon: { fontSize: '2xs' }, + }), + lg: definePartsStyle({ + control: { [$size.variable]: 'sizes.5' }, + label: { fontSize: 'md' }, + icon: { fontSize: '2xs' }, + }), +}; + +const baseStyleLabel = defineStyle({ + _disabled: { opacity: 0.2 }, +}); + +const baseStyle = definePartsStyle((props) => ({ + label: baseStyleLabel, + control: runIfFn(baseStyleControl, props), +})); + +const Checkbox = defineMultiStyleConfig({ + baseStyle, + sizes, +}); + +export default Checkbox; diff --git a/theme/components/Drawer.ts b/theme/components/Drawer.ts new file mode 100644 index 0000000000..47ceb12b63 --- /dev/null +++ b/theme/components/Drawer.ts @@ -0,0 +1,45 @@ +import { drawerAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; +import { runIfFn } from '@chakra-ui/utils'; + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys); + +import getDefaultTransitionProps from '../utils/getDefaultTransitionProps'; + +const transitionProps = getDefaultTransitionProps(); + +const baseStyleOverlay = defineStyle({ + ...transitionProps, + bg: 'blackAlpha.800', + zIndex: 'overlay', +}); + +const baseStyleDialog = defineStyle((props) => { + const { isFullHeight } = props; + + return { + ...(isFullHeight && { height: '100vh' }), + ...transitionProps, + zIndex: 'modal', + maxH: '100vh', + bg: mode('white', 'gray.900')(props), + color: 'inherit', + boxShadow: mode('lg', 'dark-lg')(props), + }; +}); + +const baseStyle = definePartsStyle((props) => ({ + overlay: baseStyleOverlay, + dialog: runIfFn(baseStyleDialog, props), +})); + +const Drawer = defineMultiStyleConfig({ + baseStyle, +}); + +export default Drawer; diff --git a/theme/components/FancySelect.ts b/theme/components/FancySelect.ts new file mode 100644 index 0000000000..72fd57425d --- /dev/null +++ b/theme/components/FancySelect.ts @@ -0,0 +1,26 @@ +const sizes = { + sm: { + field: { + px: '0', + height: '36px', + }, + }, + md: { + field: { + px: '0', + height: '56px', + }, + }, + lg: { + field: { + px: '0', + height: '76px', + }, + }, +}; + +const FancySelect = { + sizes, +}; + +export default FancySelect; diff --git a/theme/components/Form.ts b/theme/components/Form.ts new file mode 100644 index 0000000000..0c9a02e59b --- /dev/null +++ b/theme/components/Form.ts @@ -0,0 +1,167 @@ +import { formAnatomy as parts } from '@chakra-ui/anatomy'; +import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; + +import getFormStyles from '../utils/getFormStyles'; +import FancySelect from './FancySelect'; +import FormLabel from './FormLabel'; +import Input from './Input'; +import Textarea from './Textarea'; + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys); + +function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) { + const formStyles = getFormStyles(props); + + const activeLabelStyles = { + ...FormLabel.variants?.floating?.(props)._focusWithin, + ...FormLabel.sizes?.[size](props)._focusWithin, + } || {}; + + const activeInputStyles = (() => { + switch (size) { + case 'md': { + return { + paddingTop: '26px', + paddingBottom: '10px', + }; + } + + case 'lg': { + return { + paddingTop: '38px', + paddingBottom: '18px', + }; + } + } + })(); + + const inputPx = (() => { + switch (size) { + case 'md': { + return '16px'; + } + + case 'lg': { + return '24px'; + } + } + })(); + + return { + container: { + // active styles + _focusWithin: { + label: activeLabelStyles, + 'input, textarea': activeInputStyles, + }, + '&[data-active=true] label': activeLabelStyles, + + // label styles + label: FormLabel.sizes?.[size](props) || {}, + 'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles, + 'textarea:not(:placeholder-shown) + label': { + bgColor: formStyles.input.filled.bgColor, + }, + [` + input[readonly] + label, + textarea[readonly] + label, + &[aria-readonly=true] label + `]: { + bgColor: formStyles.input.readOnly.bgColor, + }, + [` + input[aria-invalid=true] + label, + textarea[aria-invalid=true] + label, + &[aria-invalid=true] label + `]: { + color: formStyles.placeholder.error.color, + }, + [` + input[disabled] + label, + textarea[disabled] + label, + &[aria-disabled=true] label + `]: { + color: formStyles.placeholder.disabled.color, + }, + + // input styles + input: Input.sizes?.[size].field, + 'input[aria-autocomplete=list]': FancySelect.sizes[size].field, + textarea: Textarea.sizes?.[size], + 'input, textarea': { + padding: inputPx, + }, + 'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles, + + // indicator styles + 'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': { + color: formStyles.placeholder.default.color, + }, + [` + input[aria-invalid=true] + label .chakra-form__required-indicator, + textarea[aria-invalid=true] + label .chakra-form__required-indicator, + &[aria-invalid=true] .chakra-form__required-indicator + `]: { + color: formStyles.placeholder.error.color, + }, + [` + input[disabled] + label .chakra-form__required-indicator, + textarea[disabled] + label .chakra-form__required-indicator, + &[aria-disabled=true] .chakra-form__required-indicator + `]: { + color: formStyles.placeholder.disabled.color, + }, + }, + }; +} + +const baseStyle = definePartsStyle(() => { + return { + requiredIndicator: { + marginStart: 0, + color: 'gray.500', + }, + }; +}); + +const variantFloating = definePartsStyle((props) => { + return { + container: { + label: FormLabel.variants?.floating(props) || {}, + }, + }; +}); + +const sizes = { + lg: definePartsStyle((props) => { + if (props.variant === 'floating') { + return getFloatingVariantStylesForSize('lg', props); + } + + return {}; + }), + md: definePartsStyle((props) => { + if (props.variant === 'floating') { + return getFloatingVariantStylesForSize('md', props); + } + + return {}; + }), +}; + +const variants = { + floating: variantFloating, +}; + +const Form = defineMultiStyleConfig({ + baseStyle, + variants, + sizes, + defaultProps: { + size: 'md', + }, +}); + +export default Form; diff --git a/theme/components/Form/FormControl.pw.tsx b/theme/components/Form/FormControl.pw.tsx new file mode 100644 index 0000000000..39ab811036 --- /dev/null +++ b/theme/components/Form/FormControl.pw.tsx @@ -0,0 +1,110 @@ +import { FormControl, Input, FormLabel } from '@chakra-ui/react'; +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +test.use({ viewport: { width: 500, height: 300 } }); + +test.describe('floating label size md +@dark-mode', () => { + test('empty', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + + await component.locator('input').focus(); + await expect(component).toHaveScreenshot(); + }); + + test('empty error', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + + await component.locator('input').focus(); + await expect(component).toHaveScreenshot(); + }); + + test('filled', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + }); + + test('filled disabled', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + }); + + test('filled read-only', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + }); + + test('filled error', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + }); +}); + +test.describe('floating label size lg +@dark-mode', () => { + test('empty', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + + await component.locator('input').focus(); + await expect(component).toHaveScreenshot(); + }); + + test('filled', async({ render }) => { + const component = await render( + + + Smart contract / Address (0x...) + , + ); + + await expect(component).toHaveScreenshot(); + + await component.locator('input').focus(); + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-empty-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-empty-1.png new file mode 100644 index 0000000000..8114db8880 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-empty-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-empty-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-empty-2.png new file mode 100644 index 0000000000..89db3e3de7 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-empty-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-filled-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-filled-1.png new file mode 100644 index 0000000000..d562e42ec3 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-filled-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-filled-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-filled-2.png new file mode 100644 index 0000000000..7dc7f74c87 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-lg-dark-mode-filled-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-1.png new file mode 100644 index 0000000000..ef143a201a Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-2.png new file mode 100644 index 0000000000..df1f13adc2 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-error-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-error-1.png new file mode 100644 index 0000000000..cb02d37576 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-error-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-error-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-error-2.png new file mode 100644 index 0000000000..8f252b3add Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-empty-error-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-1.png new file mode 100644 index 0000000000..39bc437bdb Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-disabled-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-disabled-1.png new file mode 100644 index 0000000000..c37d9af17f Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-disabled-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-error-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-error-1.png new file mode 100644 index 0000000000..c9c3020f33 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-error-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-read-only-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-read-only-1.png new file mode 100644 index 0000000000..305028d0ad Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_dark-color-mode_floating-label-size-md-dark-mode-filled-read-only-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-empty-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-empty-1.png new file mode 100644 index 0000000000..621109f3e9 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-empty-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-empty-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-empty-2.png new file mode 100644 index 0000000000..60812d7443 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-empty-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-filled-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-filled-1.png new file mode 100644 index 0000000000..8fceb62be4 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-filled-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-filled-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-filled-2.png new file mode 100644 index 0000000000..94f92a10b8 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-lg-dark-mode-filled-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-1.png new file mode 100644 index 0000000000..3aa991ab81 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-2.png new file mode 100644 index 0000000000..e118b2e86f Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-error-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-error-1.png new file mode 100644 index 0000000000..b6266c0093 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-error-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-error-2.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-error-2.png new file mode 100644 index 0000000000..b077743f2f Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-empty-error-2.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-1.png new file mode 100644 index 0000000000..2fc531aa82 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-disabled-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-disabled-1.png new file mode 100644 index 0000000000..9b30da40a5 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-disabled-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-error-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-error-1.png new file mode 100644 index 0000000000..5d0e7e2e55 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-error-1.png differ diff --git a/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-read-only-1.png b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-read-only-1.png new file mode 100644 index 0000000000..53870a1836 Binary files /dev/null and b/theme/components/Form/__screenshots__/FormControl.pw.tsx_default_floating-label-size-md-dark-mode-filled-read-only-1.png differ diff --git a/theme/components/FormLabel.ts b/theme/components/FormLabel.ts new file mode 100644 index 0000000000..c67ac969eb --- /dev/null +++ b/theme/components/FormLabel.ts @@ -0,0 +1,109 @@ +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; + +import getFormStyles from '../utils/getFormStyles'; + +const baseStyle = defineStyle({ + display: 'flex', + fontSize: 'md', + marginEnd: '3', + mb: '2', + fontWeight: 'medium', + transitionProperty: 'common', + transitionDuration: 'normal', + opacity: 1, + _disabled: { + opacity: 0.2, + }, +}); + +const variantFloating = defineStyle((props) => { + const formStyles = getFormStyles(props); + + return { + left: '2px', + top: '2px', + zIndex: 2, + position: 'absolute', + borderRadius: 'base', + boxSizing: 'border-box', + color: formStyles.placeholder.default.color, + backgroundColor: props.bgColor || props.backgroundColor || 'transparent', + pointerEvents: 'none', + margin: 0, + transformOrigin: 'top left', + transitionProperty: 'font-size, line-height, padding, top, background-color', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + _focusWithin: { + backgroundColor: props.bgColor || props.backgroundColor || 'transparent', + color: formStyles.placeholder.default.color, + fontSize: 'xs', + lineHeight: '16px', + borderTopRightRadius: 'none', + '& svg': { + width: '16px', + height: '16px', + }, + }, + '& svg': { + transitionProperty: 'width, height', + transitionDuration: 'normal', + transitionTimingFunction: 'ease', + width: '24px', + height: '24px', + mr: '2', + }, + }; +}); + +const variants = { + floating: variantFloating, +}; + +const sizes = { + lg: defineStyle((props) => { + if (props.variant === 'floating') { + return { + fontSize: 'md', + lineHeight: '24px', + padding: '26px 4px 26px 24px', + right: '22px', + _focusWithin: { + padding: '16px 0 2px 24px', + }, + '&[data-fancy=true]': { + right: '36px', + }, + }; + } + + return {}; + }), + md: defineStyle((props) => { + if (props.variant === 'floating') { + return { + fontSize: 'md', + lineHeight: '20px', + padding: '18px 4px 18px 16px', + right: '22px', + _focusWithin: { + padding: '10px 0 2px 16px', + }, + '&[data-fancy=true]': { + right: '36px', + }, + }; + } + + return {}; + }), +}; + +const FormLabel = defineStyleConfig({ + variants, + baseStyle, + sizes, +}); + +export default FormLabel; diff --git a/theme/components/Heading.ts b/theme/components/Heading.ts new file mode 100644 index 0000000000..e9cf8c51b2 --- /dev/null +++ b/theme/components/Heading.ts @@ -0,0 +1,45 @@ +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import type { SystemStyleInterpolation } from '@chakra-ui/theme-tools'; +import { mode } from '@chakra-ui/theme-tools'; + +const baseStyle: SystemStyleInterpolation = (props) => { + return { + fontWeight: '500', + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + }; +}; + +const sizes = { + '2xl': defineStyle({ + fontSize: '48px', + lineHeight: '60px', + }), + xl: defineStyle({ + fontSize: '40px', + lineHeight: '48px', + }), + lg: defineStyle({ + fontSize: '32px', + lineHeight: '40px', + letterSpacing: '-0.5px', + }), + md: defineStyle({ + fontSize: '24px', + lineHeight: '32px', + }), + sm: defineStyle({ + fontSize: '18px', + lineHeight: '24px', + }), + xs: defineStyle({ + fontSize: '14px', + lineHeight: '20px', + }), +}; + +const Heading = defineStyleConfig({ + sizes, + baseStyle, +}); + +export default Heading; diff --git a/theme/components/Input.ts b/theme/components/Input.ts new file mode 100644 index 0000000000..d127835109 --- /dev/null +++ b/theme/components/Input.ts @@ -0,0 +1,119 @@ +import { inputAnatomy as parts } from '@chakra-ui/anatomy'; +import { Input as InputComponent } from '@chakra-ui/react'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(parts.keys); + +import getDefaultTransitionProps from '../utils/getDefaultTransitionProps'; +import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles'; + +const size = { + xs: defineStyle({ + fontSize: 'md', + lineHeight: '24px', + px: '8px', + py: '4px', + h: '32px', + borderRadius: 'base', + }), + sm: defineStyle({ + fontSize: 'md', + lineHeight: '24px', + px: '8px', + py: '12px', + h: '40px', + borderRadius: 'base', + }), + // TEMPORARY INPUT SIZE!!! + // soon we will migrate to the new size and get rid off this one + // lg -> 60 + // md -> 48 + // sm -> 40 + // xs ->32 + sm_md: defineStyle({ + fontSize: 'md', + lineHeight: '24px', + px: '8px', + py: '12px', + h: '48px', + borderRadius: 'base', + }), + md: defineStyle({ + fontSize: 'md', + lineHeight: '20px', + px: '20px', + py: '20px', + h: '60px', + borderRadius: 'base', + }), + lg: defineStyle({ + fontSize: 'md', + lineHeight: '20px', + px: '24px', + py: '28px', + h: '80px', + borderRadius: 'base', + }), +}; + +const variantOutline = definePartsStyle((props) => { + const transitionProps = getDefaultTransitionProps(); + + return { + field: getOutlinedFieldStyles(props), + addon: { + border: '2px solid', + borderColor: 'transparent', + bg: mode('blackAlpha.100', 'whiteAlpha.200')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + ...transitionProps, + }, + }; +}); + +const sizes = { + xs: definePartsStyle({ + field: size.xs, + addon: size.xs, + }), + sm: definePartsStyle({ + field: size.sm, + addon: size.sm, + }), + sm_md: definePartsStyle({ + field: size.sm_md, + addon: size.sm_md, + }), + md: definePartsStyle({ + field: size.md, + addon: size.md, + }), + lg: definePartsStyle({ + field: size.lg, + addon: size.lg, + }), +}; + +const variants = { + outline: variantOutline, +}; + +const Input = defineMultiStyleConfig({ + sizes, + variants, + defaultProps: { + size: 'md', + }, +}); + +InputComponent.defaultProps = { + ...InputComponent.defaultProps, + placeholder: ' ', +}; + +export default Input; diff --git a/theme/components/Link.ts b/theme/components/Link.ts new file mode 100644 index 0000000000..6cbee7bf00 --- /dev/null +++ b/theme/components/Link.ts @@ -0,0 +1,43 @@ +import type { SystemStyleInterpolation } from '@chakra-ui/styled-system'; +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +import getDefaultTransitionProps from '../utils/getDefaultTransitionProps'; + +const baseStyle = defineStyle(getDefaultTransitionProps()); + +const variantPrimary = defineStyle((props) => { + return { + color: 'link', + _hover: { + color: 'link_hovered', + textDecorationStyle: props.textDecorationStyle || 'solid', + }, + }; +}); + +const variantSecondary = defineStyle((props) => { + return { + color: mode('gray.600', 'gray.500')(props), + _hover: { + color: mode('gray.600', 'gray.400')(props), + }, + }; +}); + +const variants: Record = { + primary: variantPrimary, + secondary: variantSecondary, +}; + +const defaultProps = { + variant: 'primary', +}; + +const Link = defineStyleConfig({ + variants, + defaultProps, + baseStyle, +}); + +export default Link; diff --git a/theme/components/Menu.ts b/theme/components/Menu.ts new file mode 100644 index 0000000000..5e425ff738 --- /dev/null +++ b/theme/components/Menu.ts @@ -0,0 +1,51 @@ +import { menuAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + cssVar, + defineStyle, +} from '@chakra-ui/styled-system'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const $bg = cssVar('menu-bg'); +const $shadow = cssVar('menu-shadow'); + +const baseStyleList = defineStyle({ + [$bg.variable]: '#fff', + [$shadow.variable]: 'shadows.2xl', + _dark: { + [$bg.variable]: 'colors.gray.900', + [$shadow.variable]: 'shadows.dark-lg', + }, + borderWidth: '0', + bg: $bg.reference, + boxShadow: $shadow.reference, +}); + +const baseStyleItem = defineStyle({ + _focus: { + [$bg.variable]: 'transparent', + _dark: { + [$bg.variable]: 'transparent', + }, + }, + _hover: { + [$bg.variable]: 'colors.blue.50', + _dark: { + [$bg.variable]: 'colors.whiteAlpha.100', + }, + }, + bg: $bg.reference, +}); + +const baseStyle = definePartsStyle({ + list: baseStyleList, + item: baseStyleItem, +}); + +const Menu = defineMultiStyleConfig({ + baseStyle, +}); + +export default Menu; diff --git a/theme/components/Modal.ts b/theme/components/Modal.ts new file mode 100644 index 0000000000..2992f4e7d8 --- /dev/null +++ b/theme/components/Modal.ts @@ -0,0 +1,125 @@ +import { modalAnatomy as parts } from '@chakra-ui/anatomy'; +import { Modal as ModalComponent } from '@chakra-ui/react'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; +import { runIfFn } from '@chakra-ui/utils'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const baseStyleDialog = defineStyle(() => { + return { + padding: 8, + borderRadius: 'lg', + bg: 'dialog_bg', + margin: 'auto', + }; +}); + +const baseStyleDialogContainer = defineStyle({ + '::-webkit-scrollbar': { display: 'none' }, + 'scrollbar-width': 'none', + // '@supports (height: -webkit-fill-available)': { height: '-webkit-fill-available' }, +}); + +const baseStyleHeader = defineStyle((props) => ({ + padding: 0, + marginBottom: 8, + fontSize: '2xl', + lineHeight: 10, + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), +})); + +const baseStyleBody = defineStyle({ + padding: 0, + marginBottom: 8, + flex: 'initial', +}); + +const baseStyleFooter = defineStyle({ + padding: 0, + justifyContent: 'flex-start', +}); + +const baseStyleCloseButton = defineStyle((props) => { + return { + top: 8, + right: 8, + height: 10, + width: 10, + color: mode('gray.700', 'gray.500')(props), + _hover: { color: 'link_hovered' }, + _active: { bg: 'none' }, + }; +}); + +const baseStyleOverlay = defineStyle({ + bg: 'blackAlpha.800', +}); + +const baseStyle = definePartsStyle((props) => ({ + dialog: runIfFn(baseStyleDialog), + dialogContainer: baseStyleDialogContainer, + + header: runIfFn(baseStyleHeader, props), + body: baseStyleBody, + footer: baseStyleFooter, + closeButton: runIfFn(baseStyleCloseButton, props), + overlay: baseStyleOverlay, +})); + +const sizes = { + sm: definePartsStyle({ + dialogContainer: { + height: '100%', + }, + dialog: { + maxW: '536px', + }, + }), + md: definePartsStyle({ + dialogContainer: { + height: '100%', + }, + dialog: { + maxW: '760px', + }, + }), + full: definePartsStyle({ + dialogContainer: { + height: '100%', + }, + dialog: { + maxW: '100vw', + my: '0', + borderRadius: '0', + padding: '80px 16px 32px 16px', + height: '100%', + overflowY: 'scroll', + }, + closeButton: { + top: 4, + right: 6, + width: 6, + height: 6, + }, + header: { + mb: 6, + }, + }), +}; + +const Modal = defineMultiStyleConfig({ + sizes, + baseStyle, +}); + +export default Modal; + +ModalComponent.defaultProps = { + ...ModalComponent.defaultProps, + isCentered: true, +}; diff --git a/theme/components/Popover.ts b/theme/components/Popover.ts new file mode 100644 index 0000000000..9f277d8605 --- /dev/null +++ b/theme/components/Popover.ts @@ -0,0 +1,86 @@ +import { popoverAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; +import { cssVar, mode } from '@chakra-ui/theme-tools'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const $popperBg = cssVar('popper-bg'); + +const $arrowBg = cssVar('popper-arrow-bg'); +const $arrowShadowColor = cssVar('popper-arrow-shadow-color'); + +const baseStylePopper = defineStyle({ + zIndex: 'popover', +}); + +const baseStyleContent = defineStyle((props) => { + const bg = mode('white', 'gray.900')(props); + const shadowColor = mode('blackAlpha.200', 'whiteAlpha.300')(props); + + return { + [$popperBg.variable]: `colors.${ bg }`, + bg: $popperBg.reference, + [$arrowBg.variable]: $popperBg.reference, + [$arrowShadowColor.variable]: `colors.${ shadowColor }`, + _dark: { + [$popperBg.variable]: `colors.gray.900`, + [$arrowShadowColor.variable]: `colors.whiteAlpha.300`, + boxShadow: 'dark-lg', + }, + width: 'xs', + border: 'none', + borderColor: 'inherit', + borderRadius: 'md', + boxShadow: '2xl', + zIndex: 'inherit', + _focusVisible: { + outline: 0, + boxShadow: '2xl', + }, + }; +}); + +const baseStyleHeader = defineStyle({ + px: 3, + py: 2, + borderBottomWidth: '1px', +}); + +const baseStyleBody = defineStyle({ + px: 4, + py: 4, +}); + +const baseStyleFooter = defineStyle({ + px: 3, + py: 2, + borderTopWidth: '1px', +}); + +const baseStyleCloseButton = defineStyle({ + position: 'absolute', + borderRadius: 'md', + top: 1, + insetEnd: 2, + padding: 2, +}); + +const baseStyle = definePartsStyle((props) => ({ + popper: baseStylePopper, + content: baseStyleContent(props), + header: baseStyleHeader, + body: baseStyleBody, + footer: baseStyleFooter, + arrow: {}, + closeButton: baseStyleCloseButton, +})); + +const Popover = defineMultiStyleConfig({ + baseStyle, +}); + +export default Popover; diff --git a/theme/components/Radio.ts b/theme/components/Radio.ts new file mode 100644 index 0000000000..18033bc5fd --- /dev/null +++ b/theme/components/Radio.ts @@ -0,0 +1,44 @@ +import { radioAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const baseStyleLabel = defineStyle({ + _disabled: { opacity: 0.2 }, + width: 'fit-content', +}); + +const baseStyleContainer = defineStyle({ + width: 'fit-content', +}); + +const baseStyle = definePartsStyle({ + label: baseStyleLabel, + container: baseStyleContainer, +}); + +const sizes = { + md: definePartsStyle({ + control: { w: '4', h: '4' }, + label: { fontSize: 'md' }, + }), + lg: definePartsStyle({ + control: { w: '5', h: '5' }, + label: { fontSize: 'md' }, + }), + sm: definePartsStyle({ + control: { width: '3', height: '3' }, + label: { fontSize: 'sm' }, + }), +}; + +const Radio = defineMultiStyleConfig({ + baseStyle, + sizes, +}); + +export default Radio; diff --git a/theme/components/Select.ts b/theme/components/Select.ts new file mode 100644 index 0000000000..3e53c6952a --- /dev/null +++ b/theme/components/Select.ts @@ -0,0 +1,78 @@ +import { selectAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +import Input from './Input'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const variantOutline = definePartsStyle((props) => { + return { + field: { + ...Input.variants?.outline(props).field, + borderColor: mode('gray.200', 'gray.600')(props), + _hover: { + borderColor: mode('gray.300', 'gray.500')(props), + }, + _focusVisible: { + borderColor: mode('gray.200', 'gray.600')(props), + boxShadow: 'none', + }, + cursor: 'pointer', + }, + }; +}); + +const iconSpacing = defineStyle({ + paddingInlineEnd: '8', +}); + +const sizes = { + lg: { + ...Input.sizes?.lg, + field: { + ...Input.sizes?.lg.field, + ...iconSpacing, + }, + }, + md: { + ...Input.sizes?.md, + field: { + ...Input.sizes?.md.field, + ...iconSpacing, + }, + }, + sm: { + ...Input.sizes?.sm, + field: { + ...Input.sizes?.sm.field, + ...iconSpacing, + }, + }, + xs: { + ...Input.sizes?.xs, + field: { + ...Input.sizes?.xs.field, + ...iconSpacing, + fontSize: 'sm', + lineHeight: '20px', + }, + }, +}; + +const Select = defineMultiStyleConfig({ + variants: { + ...Input.variants, + outline: variantOutline, + }, + sizes, + defaultProps: { + size: 'xs', + }, +}); + +export default Select; diff --git a/theme/components/Skeleton.ts b/theme/components/Skeleton.ts new file mode 100644 index 0000000000..65555c8c58 --- /dev/null +++ b/theme/components/Skeleton.ts @@ -0,0 +1,46 @@ +import { Skeleton as SkeletonComponent } from '@chakra-ui/react'; +import { + defineStyle, + defineStyleConfig, +} from '@chakra-ui/styled-system'; +import { keyframes } from '@chakra-ui/system'; +import { getColor, mode } from '@chakra-ui/theme-tools'; + +const shine = () => + keyframes({ + to: { backgroundPositionX: '-200%' }, + }); + +const baseStyle = defineStyle((props) => { + const defaultStartColor = mode('blackAlpha.50', 'whiteAlpha.50')(props); + const defaultEndColor = mode('blackAlpha.100', 'whiteAlpha.100')(props); + + const { + startColor = defaultStartColor, + endColor = defaultEndColor, + theme, + } = props; + + const start = getColor(theme, startColor); + const end = getColor(theme, endColor); + + return { + opacity: 1, + borderRadius: 'md', + borderColor: start, + background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`, + backgroundSize: '200% 100%', + }; +}); + +const Skeleton = defineStyleConfig({ + baseStyle, +}); + +export default Skeleton; + +SkeletonComponent.defaultProps = { + ...SkeletonComponent.defaultProps, + speed: 1, + animation: `1s linear infinite ${ shine() }`, +}; diff --git a/theme/components/Spinner.ts b/theme/components/Spinner.ts new file mode 100644 index 0000000000..3179c944b3 --- /dev/null +++ b/theme/components/Spinner.ts @@ -0,0 +1,21 @@ +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +const baseStyle = defineStyle((props) => { + const { emptyColor, color } = props; + + return { + borderColor: color || 'blue.500', + borderBottomColor: emptyColor || mode('blackAlpha.200', 'whiteAlpha.200')(props), + borderLeftColor: emptyColor || mode('blackAlpha.200', 'whiteAlpha.200')(props), + }; +}); + +const Spinner = defineStyleConfig({ + baseStyle, + defaultProps: { + size: 'md', + }, +}); + +export default Spinner; diff --git a/theme/components/Switch.ts b/theme/components/Switch.ts new file mode 100644 index 0000000000..6059618c0f --- /dev/null +++ b/theme/components/Switch.ts @@ -0,0 +1,32 @@ +import { switchAnatomy as parts } from '@chakra-ui/anatomy'; +import { defineStyle, createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const baseStyleTrack = defineStyle((props) => { + const { colorScheme: c } = props; + + return { + _checked: { + bg: mode(`${ c }.500`, `${ c }.300`)(props), + _hover: { + bg: mode(`${ c }.600`, `${ c }.400`)(props), + }, + }, + _focusVisible: { + boxShadow: 'none', + }, + }; +}); + +const baseStyle = definePartsStyle((props) => ({ + track: baseStyleTrack(props), +})); + +const Switch = defineMultiStyleConfig({ + baseStyle, +}); + +export default Switch; diff --git a/theme/components/Table.ts b/theme/components/Table.ts new file mode 100644 index 0000000000..0441fbb72c --- /dev/null +++ b/theme/components/Table.ts @@ -0,0 +1,109 @@ +import { tableAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +import getDefaultTransitionProps from '../utils/getDefaultTransitionProps'; + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const variantSimple = definePartsStyle((props) => { + const transitionProps = getDefaultTransitionProps(); + + return { + th: { + border: 0, + color: mode('blackAlpha.700', 'whiteAlpha.700')(props), + backgroundColor: mode('blackAlpha.100', 'whiteAlpha.200')(props), + ...transitionProps, + }, + thead: { + ...transitionProps, + }, + td: { + borderColor: 'divider', + ...transitionProps, + }, + }; +}); + +const sizes = { + md: definePartsStyle({ + th: { + px: 4, + fontSize: 'sm', + }, + td: { + p: 4, + }, + }), + sm: definePartsStyle({ + th: { + px: '10px', + py: '10px', + fontSize: 'sm', + }, + td: { + px: '10px', + py: 4, + fontSize: 'sm', + fontWeight: 500, + }, + }), + xs: definePartsStyle({ + th: { + px: '6px', + py: '10px', + fontSize: 'sm', + }, + td: { + px: '6px', + py: 4, + fontSize: 'sm', + fontWeight: 500, + }, + }), +}; + +const variants = { + simple: variantSimple, +}; + +const baseStyle = definePartsStyle({ + th: { + textTransform: 'none', + fontFamily: 'body', + fontWeight: '500', + overflow: 'hidden', + color: 'gray.500', + letterSpacing: 'none', + _first: { + borderTopLeftRadius: '8px', + }, + _last: { + borderTopRightRadius: '8px', + }, + }, + td: { + fontSize: 'md', + verticalAlign: 'top', + }, + table: { + tableLayout: 'fixed', + borderTopLeftRadius: 'base', + borderTopRightRadius: 'base', + overflow: 'unset', + fontVariant: 'normal', + fontVariantLigatures: 'no-contextual', + }, +}); + +const Table = defineMultiStyleConfig({ + baseStyle, + sizes, + variants, +}); + +export default Table; diff --git a/theme/components/Tabs.ts b/theme/components/Tabs.ts new file mode 100644 index 0000000000..e73668761a --- /dev/null +++ b/theme/components/Tabs.ts @@ -0,0 +1,63 @@ +import { tabsAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +import Button from './Button/Button'; + +const variantSoftRounded = definePartsStyle((props) => { + return { + tab: { + borderRadius: 'base', + fontWeight: '600', + color: mode('blue.700', 'gray.400')(props), + _selected: { + color: mode('blue.700', 'gray.50')(props), + bg: mode('blue.50', 'gray.800')(props), + _hover: { + color: mode('blue.700', 'gray.50')(props), + }, + }, + _hover: { + color: 'link_hovered', + }, + _focusVisible: { + boxShadow: { base: 'none', lg: 'outline' }, + }, + }, + }; +}); + +const variantOutline = definePartsStyle((props) => { + return { + tab: { + ...Button.variants?.outline(props), + ...Button.baseStyle, + _selected: Button.variants?.outline(props)._active, + }, + }; +}); + +const sizes = { + sm: definePartsStyle({ + tab: Button.sizes?.sm, + }), + md: definePartsStyle({ + tab: Button.sizes?.md, + }), +}; + +const variants = { + 'soft-rounded': variantSoftRounded, + outline: variantOutline, +}; + +const Tabs = defineMultiStyleConfig({ + sizes, + variants, +}); + +export default Tabs; diff --git a/theme/components/Tag/Tag.pw.tsx b/theme/components/Tag/Tag.pw.tsx new file mode 100644 index 0000000000..28da22a9ef --- /dev/null +++ b/theme/components/Tag/Tag.pw.tsx @@ -0,0 +1,20 @@ +import { Box, Tag } from '@chakra-ui/react'; +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +[ 'blue', 'gray', 'gray-blue', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => { + test(`${ colorScheme } color scheme +@dark-mode`, async({ render }) => { + const component = await render(content); + await expect(component.getByText(/content/i)).toHaveScreenshot(); + }); +}); + +test('with long text', async({ render }) => { + const component = await render( + + this is very looooooooooong text + , + ); + await expect(component.getByText(/this/i)).toHaveScreenshot(); +}); diff --git a/theme/components/Tag/Tag.ts b/theme/components/Tag/Tag.ts new file mode 100644 index 0000000000..6ea4c41d10 --- /dev/null +++ b/theme/components/Tag/Tag.ts @@ -0,0 +1,76 @@ +import { tagAnatomy as parts } from '@chakra-ui/anatomy'; +import { + createMultiStyleConfigHelpers, + defineStyle, +} from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +import getDefaultTransitionProps from '../../utils/getDefaultTransitionProps'; +import Badge from '../Badge'; + +const transitionProps = getDefaultTransitionProps(); + +const { defineMultiStyleConfig, definePartsStyle } = + createMultiStyleConfigHelpers(parts.keys); + +const variants = { + subtle: definePartsStyle((props) => ({ + container: Badge.variants?.subtle(props), + })), + select: definePartsStyle((props) => ({ + container: { + bg: mode('gray.100', 'gray.800')(props), + color: mode('gray.500', 'whiteAlpha.800')(props), + _hover: { + color: 'blue.400', + opacity: 0.76, + }, + [` + &[data-selected=true], + &[data-selected=true][aria-selected=true] + `]: { + bg: mode('blue.500', 'blue.900')(props), + color: 'whiteAlpha.800', + }, + }, + })), +}; + +const sizes = { + sm: definePartsStyle({ + container: { + minH: 6, + minW: 6, + fontSize: 'sm', + px: 1, + py: '2px', + lineHeight: 5, + }, + }), +}; + +const baseStyleContainer = defineStyle({ + display: 'inline-block', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + borderRadius: 'sm', + ...transitionProps, +}); + +const baseStyle = definePartsStyle({ + container: baseStyleContainer, +}); + +const Tag = defineMultiStyleConfig({ + baseStyle, + variants, + sizes, + defaultProps: { + size: 'sm', + variant: 'subtle', + colorScheme: 'gray', + }, +}); + +export default Tag; diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..5fc7bad237 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..daf45113b1 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_cyan-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..05fe7cfea4 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..6afe64a5a7 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..a14814713f Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_green-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..fd9bb6cfc6 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_orange-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..a9b36a72cd Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_purple-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..bbaed04293 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_dark-color-mode_teal-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..ce7946c035 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..1cac3c3c0c Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_cyan-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-blue-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-blue-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..1d150ca7d3 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-blue-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..f37bae331a Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_gray-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..24fb120a47 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_green-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..7a3f917391 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_orange-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..f9b325adf1 Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_purple-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png new file mode 100644 index 0000000000..cccd73491f Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_teal-color-scheme-dark-mode-1.png differ diff --git a/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png new file mode 100644 index 0000000000..ee9d7d6dbd Binary files /dev/null and b/theme/components/Tag/__screenshots__/Tag.pw.tsx_default_with-long-text-1.png differ diff --git a/theme/components/Text.ts b/theme/components/Text.ts new file mode 100644 index 0000000000..435330bc3b --- /dev/null +++ b/theme/components/Text.ts @@ -0,0 +1,32 @@ +import type { SystemStyleInterpolation } from '@chakra-ui/styled-system'; +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; + +const variantPrimary = defineStyle((props) => ({ + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), +})); + +const variantSecondary = defineStyle((props) => ({ + color: mode('gray.500', 'gray.400')(props), +})); + +const variantInherit = { + color: 'inherit', +}; + +const variants: Record = { + primary: variantPrimary, + secondary: variantSecondary, + inherit: variantInherit, +}; + +const defaultProps = { + variant: 'primary', +}; + +const Text = defineStyleConfig({ + defaultProps, + variants, +}); + +export default Text; diff --git a/theme/components/Textarea.ts b/theme/components/Textarea.ts new file mode 100644 index 0000000000..bb0a00ca56 --- /dev/null +++ b/theme/components/Textarea.ts @@ -0,0 +1,38 @@ +import { Textarea as TextareaComponent } from '@chakra-ui/react'; +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; + +import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles'; + +const sizes = { + md: defineStyle({ + fontSize: 'md', + lineHeight: '20px', + h: '160px', + borderRadius: 'base', + }), + lg: defineStyle({ + fontSize: 'md', + lineHeight: '20px', + px: '24px', + py: '28px', + h: '160px', + borderRadius: 'base', + }), +}; + +const Textarea = defineStyleConfig({ + sizes, + variants: { + outline: defineStyle(getOutlinedFieldStyles), + }, + defaultProps: { + variant: 'outline', + }, +}); + +TextareaComponent.defaultProps = { + ...TextareaComponent.defaultProps, + placeholder: ' ', +}; + +export default Textarea; diff --git a/theme/components/Tooltip/Tooltip.pw.tsx b/theme/components/Tooltip/Tooltip.pw.tsx new file mode 100644 index 0000000000..049f9caa20 --- /dev/null +++ b/theme/components/Tooltip/Tooltip.pw.tsx @@ -0,0 +1,38 @@ +import { Box, Tooltip, Icon } from '@chakra-ui/react'; +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +test('base view +@dark-mode', async({ render, page }) => { + const component = await render( + + + trigger + + , + ); + + await component.getByText(/trigger/i).hover(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 40, width: 130, height: 64 } }); +}); + +// was not able to reproduce in tests issue when Icon is used as trigger for tooltip +// https://github.com/chakra-ui/chakra-ui/issues/7107 +test.fixme('with icon', async({ render, page }) => { + const component = await render( + + + + + + + , + ); + + const tooltip = page.getByText(/tooltip content/i); + expect(await tooltip.isVisible()).toBe(false); + + await component.locator('svg[aria-label="Trigger"]').hover(); + expect(await tooltip.isVisible()).toBe(true); +}); diff --git a/theme/components/Tooltip/Tooltip.ts b/theme/components/Tooltip/Tooltip.ts new file mode 100644 index 0000000000..d0bbae57e2 --- /dev/null +++ b/theme/components/Tooltip/Tooltip.ts @@ -0,0 +1,50 @@ +import { Tooltip as TooltipComponent } from '@chakra-ui/react'; +import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; +import { mode, cssVar } from '@chakra-ui/theme-tools'; + +const $bg = cssVar('tooltip-bg'); +const $fg = cssVar('tooltip-fg'); +const $arrowBg = cssVar('popper-arrow-bg'); + +const variantNav = defineStyle((props) => { + return { + bg: mode('blue.50', 'gray.800')(props), + color: 'blue.400', + padding: '15px 12px', + minWidth: '120px', + borderRadius: 'base', + fontSize: '14px', + lineHeight: '20px', + textAlign: 'center', + boxShadow: 'none', + fontWeight: '500', + }; +}); + +const variants = { + nav: variantNav, +}; + +const baseStyle = defineStyle((props) => { + const bg = mode('gray.700', 'gray.200')(props); + const fg = mode('white', 'black')(props); + + return { + bg: $bg.reference, + color: $fg.reference, + [$bg.variable]: `colors.${ bg }`, + [$fg.variable]: `colors.${ fg }`, + [$arrowBg.variable]: $bg.reference, + maxWidth: props.maxWidth || props.maxW || 'calc(100vw - 8px)', + marginX: '4px', + }; +}); + +const Tooltip = defineStyleConfig({ + variants, + baseStyle, +}); + +TooltipComponent.defaultProps = { ...TooltipComponent.defaultProps, hasArrow: true }; + +export default Tooltip; diff --git a/theme/components/Tooltip/__screenshots__/Tooltip.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/theme/components/Tooltip/__screenshots__/Tooltip.pw.tsx_dark-color-mode_base-view-dark-mode-1.png new file mode 100644 index 0000000000..c80b68fb63 Binary files /dev/null and b/theme/components/Tooltip/__screenshots__/Tooltip.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/theme/components/Tooltip/__screenshots__/Tooltip.pw.tsx_default_base-view-dark-mode-1.png b/theme/components/Tooltip/__screenshots__/Tooltip.pw.tsx_default_base-view-dark-mode-1.png new file mode 100644 index 0000000000..c09ef160cb Binary files /dev/null and b/theme/components/Tooltip/__screenshots__/Tooltip.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/theme/components/index.ts b/theme/components/index.ts new file mode 100644 index 0000000000..b32509ccaa --- /dev/null +++ b/theme/components/index.ts @@ -0,0 +1,53 @@ +import Alert from './Alert/Alert'; +import Badge from './Badge'; +import Button from './Button/Button'; +import Checkbox from './Checkbox'; +import Drawer from './Drawer'; +import Form from './Form'; +import FormLabel from './FormLabel'; +import Heading from './Heading'; +import Input from './Input'; +import Link from './Link'; +import Menu from './Menu'; +import Modal from './Modal'; +import Popover from './Popover'; +import Radio from './Radio'; +import Select from './Select'; +import Skeleton from './Skeleton'; +import Spinner from './Spinner'; +import Switch from './Switch'; +import Table from './Table'; +import Tabs from './Tabs'; +import Tag from './Tag/Tag'; +import Text from './Text'; +import Textarea from './Textarea'; +import Tooltip from './Tooltip/Tooltip'; + +const components = { + Alert, + Badge, + Button, + Checkbox, + Drawer, + Heading, + Input, + Form, + FormLabel, + Link, + Menu, + Modal, + Popover, + Radio, + Select, + Skeleton, + Spinner, + Switch, + Tabs, + Table, + Tag, + Text, + Textarea, + Tooltip, +}; + +export default components; diff --git a/theme/config.ts b/theme/config.ts new file mode 100644 index 0000000000..ddca2b54ea --- /dev/null +++ b/theme/config.ts @@ -0,0 +1,11 @@ +import { type ThemeConfig } from '@chakra-ui/react'; + +import appConfig from 'configs/app'; + +const config: ThemeConfig = { + initialColorMode: appConfig.UI.colorTheme.default?.colorMode ?? 'system', + useSystemColorMode: false, + disableTransitionOnChange: false, +}; + +export default config; diff --git a/theme/foundations/borders.ts b/theme/foundations/borders.ts new file mode 100644 index 0000000000..4307c548c1 --- /dev/null +++ b/theme/foundations/borders.ts @@ -0,0 +1,13 @@ +const borders = { + radii: { + none: '0', + sm: '4px', + base: '8px', + md: '12px', + lg: '16px', + xl: '24px', + full: '9999px', + }, +}; + +export default borders; diff --git a/theme/foundations/breakpoints.ts b/theme/foundations/breakpoints.ts new file mode 100644 index 0000000000..aa60d95011 --- /dev/null +++ b/theme/foundations/breakpoints.ts @@ -0,0 +1,11 @@ +const breakpoints = { +// maybe we need them in future + sm: '415px', + // md: '768px', + lg: '1000px', + xl: '1440px', + // these breakpoint are needed just to make others work + '2xl': '3000px', +}; + +export default breakpoints; diff --git a/theme/foundations/colors.ts b/theme/foundations/colors.ts new file mode 100644 index 0000000000..f8b2ca521f --- /dev/null +++ b/theme/foundations/colors.ts @@ -0,0 +1,65 @@ +const colors = { + green: { + '100': '#C6F6D5', + '400': '#48BB78', + '500': '#38A169', + '600': '#25855A', + }, + blue: { + '50': '#EBF8FF', + '100': '#BEE3F8', + '200': '#90CDF4', + '300': '#63B3ED', + '400': '#4299E1', + '500': '#3182CE', + '600': '#2B6CB0', + '700': '#2C5282', + '800': '#2A4365', + '900': '#1A365D', + }, + red: { + '500': '#E53E3E', + '100': '#FED7D7', + }, + orange: { + '100': '#FEEBCB', + }, + gray: { + '50': '#F7FAFC', // <- + '100': '#EDF2F7', + '200': '#E2E8F0', + '300': '#CBD5E0', + '400': '#A0AEC0', + '500': '#718096', + '600': '#4A5568', + '700': '#2D3748', + '800': '#1A202C', + '900': '#171923', + }, + black: '#101112', + white: '#ffffff', + blackAlpha: { + '50': 'RGBA(16, 17, 18, 0.04)', + '100': 'RGBA(16, 17, 18, 0.06)', + '200': 'RGBA(16, 17, 18, 0.08)', + '300': 'RGBA(16, 17, 18, 0.16)', + '400': 'RGBA(16, 17, 18, 0.24)', + '500': 'RGBA(16, 17, 18, 0.36)', + '600': 'RGBA(16, 17, 18, 0.48)', + '700': 'RGBA(16, 17, 18, 0.64)', + '800': 'RGBA(16, 17, 18, 0.80)', + '900': 'RGBA(16, 17, 18, 0.92)', + }, + github: '#171923', + telegram: '#2775CA', + linkedin: '#1564BA', + discord: '#9747FF', + slack: '#1BA27A', + twitter: '#000000', + opensea: '#2081E2', + facebook: '#4460A0', + medium: '#231F20', + reddit: '#FF4500', +}; + +export default colors; diff --git a/theme/foundations/scrollbar.ts b/theme/foundations/scrollbar.ts new file mode 100644 index 0000000000..3d9d68bf11 --- /dev/null +++ b/theme/foundations/scrollbar.ts @@ -0,0 +1,40 @@ +import { getCSSVar } from '@chakra-ui/styled-system'; +import { mode } from '@chakra-ui/theme-tools'; +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; + +const scrollbar = (props: StyleFunctionProps) => { + const bgColor = mode('blackAlpha.300', 'whiteAlpha.300')(props); + const resizerUrl = mode('url(/static/resizer_light.png)', 'url(/static/resizer_dark.png)')(props); + + return { + 'body *::-webkit-scrollbar': { + width: '20px', + }, + 'body *::-webkit-scrollbar-track': { + backgroundColor: 'transparent', + }, + 'body *::-webkit-scrollbar-thumb': { + backgroundColor: bgColor, + borderRadius: '20px', + border: `8px solid rgba(0,0,0,0)`, + backgroundClip: 'content-box', + minHeight: '32px', + }, + 'body *::-webkit-scrollbar-button': { + display: 'none', + }, + 'body *::-webkit-scrollbar-corner': { + backgroundColor: 'transparent', + }, + 'body *::-webkit-resizer': { + backgroundImage: resizerUrl, + backgroundSize: '20px', + }, + 'body *': { + scrollbarWidth: 'thin', + scrollbarColor: `${ getCSSVar(props.theme, 'colors', bgColor) } transparent`, + }, + }; +}; + +export default scrollbar; diff --git a/theme/foundations/semanticTokens.ts b/theme/foundations/semanticTokens.ts new file mode 100644 index 0000000000..0c772a6539 --- /dev/null +++ b/theme/foundations/semanticTokens.ts @@ -0,0 +1,44 @@ +const semanticTokens = { + colors: { + divider: { + 'default': 'blackAlpha.200', + _dark: 'whiteAlpha.200', + }, + text: { + 'default': 'blackAlpha.800', + _dark: 'whiteAlpha.800', + }, + text_secondary: { + 'default': 'gray.500', + _dark: 'gray.400', + }, + link: { + 'default': 'blue.600', + _dark: 'blue.300', + }, + link_hovered: { + 'default': 'blue.400', + }, + icon_link_external: { + 'default': 'gray.300', + _dark: 'gray.500', + }, + icon_info: { + 'default': 'gray.400', + _dark: 'gray.500', + }, + error: { + 'default': 'red.500', + _dark: 'red.500', + }, + dialog_bg: { + 'default': 'white', + _dark: 'gray.900', + }, + }, + shadows: { + action_bar: '0 4px 4px -4px rgb(0 0 0 / 10%), 0 2px 4px -4px rgb(0 0 0 / 6%)', + }, +}; + +export default semanticTokens; diff --git a/theme/foundations/transition.ts b/theme/foundations/transition.ts new file mode 100644 index 0000000000..b120e48137 --- /dev/null +++ b/theme/foundations/transition.ts @@ -0,0 +1,15 @@ +const transitionDuration = { + 'ultra-fast': '50ms', + faster: '100ms', + fast: '150ms', + normal: '200ms', + slow: '300ms', + slower: '400ms', + 'ultra-slow': '500ms', +}; + +const transition = { + duration: transitionDuration, +}; + +export default transition; diff --git a/theme/foundations/typography.ts b/theme/foundations/typography.ts new file mode 100644 index 0000000000..57e59c4191 --- /dev/null +++ b/theme/foundations/typography.ts @@ -0,0 +1,33 @@ +import { theme } from '@chakra-ui/react'; + +export const BODY_TYPEFACE = 'Inter'; +export const HEADING_TYPEFACE = 'Poppins'; + +const typography = { + fonts: { + heading: `${ HEADING_TYPEFACE }, ${ theme.fonts.heading }`, + body: `${ BODY_TYPEFACE }, ${ theme.fonts.body }`, + }, + textStyles: { + h2: { + fontSize: [ '32px' ], + fontWeight: '500', + lineHeight: '40px', + fontFamily: 'heading', + }, + h3: { + fontSize: '24px', + fontWeight: '500', + lineHeight: '32px', + fontFamily: 'heading', + }, + h4: { + fontSize: 'md', + fontWeight: '500', + lineHeight: '24px', + fontFamily: 'heading', + }, + }, +}; + +export default typography; diff --git a/theme/foundations/zIndices.ts b/theme/foundations/zIndices.ts new file mode 100644 index 0000000000..84d6f039f8 --- /dev/null +++ b/theme/foundations/zIndices.ts @@ -0,0 +1,19 @@ +const zIndices = { + hide: -1, + auto: 'auto', + base: 0, + docked: 10, + dropdown: 1000, + sticky: 1100, + sticky1: 1101, + sticky2: 1102, + banner: 1200, + overlay: 1300, + modal: 1400, + popover: 1500, + tooltip: 1550, // otherwise tooltips will not be visible in modals + skipLink: 1600, + toast: 1700, +}; + +export default zIndices; diff --git a/theme/global.ts b/theme/global.ts new file mode 100644 index 0000000000..e50b7eeaf6 --- /dev/null +++ b/theme/global.ts @@ -0,0 +1,30 @@ +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; +import { mode } from '@chakra-ui/theme-tools'; + +import scrollbar from './foundations/scrollbar'; +import addressEntity from './globals/address-entity'; +import getDefaultTransitionProps from './utils/getDefaultTransitionProps'; + +const global = (props: StyleFunctionProps) => ({ + body: { + bg: mode('white', 'black')(props), + ...getDefaultTransitionProps(), + '-webkit-tap-highlight-color': 'transparent', + 'font-variant-ligatures': 'no-contextual', + }, + mark: { + bgColor: mode('green.100', 'green.800')(props), + color: 'inherit', + }, + 'svg *::selection': { + color: 'none', + background: 'none', + }, + form: { + w: '100%', + }, + ...scrollbar(props), + ...addressEntity(props), +}); + +export default global; diff --git a/theme/globals/address-entity.ts b/theme/globals/address-entity.ts new file mode 100644 index 0000000000..25641c3020 --- /dev/null +++ b/theme/globals/address-entity.ts @@ -0,0 +1,37 @@ +import { mode } from '@chakra-ui/theme-tools'; +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; + +const styles = (props: StyleFunctionProps) => { + return { + '.address-entity': { + '&.address-entity_highlighted': { + _before: { + content: `" "`, + position: 'absolute', + py: 1, + pl: 1, + pr: 0, + top: '-5px', + left: '-5px', + width: `100%`, + height: '100%', + borderRadius: 'base', + borderColor: mode('blue.200', 'blue.600')(props), + borderWidth: '1px', + borderStyle: 'dashed', + bgColor: mode('blue.50', 'blue.900')(props), + zIndex: -1, + }, + }, + }, + '.address-entity_no-copy': { + '&.address-entity_highlighted': { + _before: { + pr: 2, + }, + }, + }, + }; +}; + +export default styles; diff --git a/theme/index.ts b/theme/index.ts new file mode 100644 index 0000000000..7d391fe6d6 --- /dev/null +++ b/theme/index.ts @@ -0,0 +1,29 @@ +import { extendTheme } from '@chakra-ui/react'; + +import components from './components/index'; +import config from './config'; +import borders from './foundations/borders'; +import breakpoints from './foundations/breakpoints'; +import colors from './foundations/colors'; +import semanticTokens from './foundations/semanticTokens'; +import transition from './foundations/transition'; +import typography from './foundations/typography'; +import zIndices from './foundations/zIndices'; +import global from './global'; + +const overrides = { + ...typography, + ...borders, + colors, + components, + config, + styles: { + global, + }, + breakpoints, + transition, + zIndices, + semanticTokens, +}; + +export default extendTheme(overrides); diff --git a/theme/utils/getDefaultTransitionProps.ts b/theme/utils/getDefaultTransitionProps.ts new file mode 100644 index 0000000000..5beb452049 --- /dev/null +++ b/theme/utils/getDefaultTransitionProps.ts @@ -0,0 +1,7 @@ +export default function getDefaultTransitionProps(props?: {transitionProperty: string}) { + return { + transitionProperty: `background-color, color, border-color${ props?.transitionProperty ? ', ' + props.transitionProperty : '' }`, + transitionDuration: 'normal', + transitionTimingFunction: 'ease', + }; +} diff --git a/theme/utils/getFormStyles.ts b/theme/utils/getFormStyles.ts new file mode 100644 index 0000000000..5dda16db55 --- /dev/null +++ b/theme/utils/getFormStyles.ts @@ -0,0 +1,55 @@ +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; +import { mode, transparentize } from '@chakra-ui/theme-tools'; + +export default function getFormStyles(props: StyleFunctionProps) { + return { + input: { + empty: { + // there is no text in the empty input + // color: ???, + bgColor: props.bgColor || mode('white', 'black')(props), + borderColor: mode('gray.100', 'gray.700')(props), + }, + hover: { + color: mode('gray.800', 'gray.50')(props), + bgColor: props.bgColor || mode('white', 'black')(props), + borderColor: mode('gray.200', 'gray.500')(props), + }, + focus: { + color: mode('gray.800', 'gray.50')(props), + bgColor: props.bgColor || mode('white', 'black')(props), + borderColor: mode('blue.400', 'blue.400')(props), + }, + filled: { + color: mode('gray.800', 'gray.50')(props), + bgColor: props.bgColor || mode('white', 'black')(props), + borderColor: mode('gray.300', 'gray.600')(props), + }, + readOnly: { + color: mode('gray.800', 'gray.50')(props), + bgColor: mode('gray.200', 'gray.800')(props), + borderColor: mode('gray.200', 'gray.800')(props), + }, + // we use opacity to show the disabled state + disabled: { + opacity: 0.2, + }, + error: { + color: mode('gray.800', 'gray.50')(props), + bgColor: props.bgColor || mode('white', 'black')(props), + borderColor: mode('red.500', 'red.500')(props), + }, + }, + placeholder: { + 'default': { + color: mode('gray.500', 'gray.500')(props), + }, + disabled: { + color: transparentize('gray.500', 0.2)(props.theme), + }, + error: { + color: mode('red.500', 'red.500')(props), + }, + }, + }; +} diff --git a/theme/utils/getOutlinedFieldStyles.ts b/theme/utils/getOutlinedFieldStyles.ts new file mode 100644 index 0000000000..c26ed15d11 --- /dev/null +++ b/theme/utils/getOutlinedFieldStyles.ts @@ -0,0 +1,68 @@ +import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; +import { mode } from '@chakra-ui/theme-tools'; + +import getDefaultTransitionProps from './getDefaultTransitionProps'; +import getFormStyles from './getFormStyles'; + +export default function getOutlinedFieldStyles(props: StyleFunctionProps) { + const formStyles = getFormStyles(props); + const transitionProps = getDefaultTransitionProps(); + + return { + border: '2px solid', + // filled input + ...formStyles.input.filled, + ...transitionProps, + _hover: { + ...formStyles.input.hover, + }, + _readOnly: { + boxShadow: 'none !important', + userSelect: 'all', + pointerEvents: 'none', + ...formStyles.input.readOnly, + _hover: { + ...formStyles.input.readOnly, + }, + _focus: { + ...formStyles.input.readOnly, + }, + }, + _disabled: { + ...formStyles.input.disabled, + cursor: 'not-allowed', + ':-webkit-autofill': { + // background color for disabled input which value was selected from browser autocomplete popup + '-webkit-box-shadow': `0 0 0px 1000px ${ mode('rgba(16, 17, 18, 0.08)', 'rgba(255, 255, 255, 0.08)')(props) } inset`, + }, + }, + _invalid: { + ...formStyles.input.error, + boxShadow: `none`, + _placeholder: { + color: formStyles.placeholder.error.color, + }, + }, + _focusVisible: { + ...formStyles.input.focus, + zIndex: 1, + boxShadow: 'md', + }, + _placeholder: { + color: formStyles.placeholder.default.color, + }, + // not filled input + ':placeholder-shown:not(:focus-visible):not(:hover):not([aria-invalid=true]):not([aria-readonly=true])': { + ...formStyles.input.empty, + }, + + // not filled input with type="date" + ':not(:placeholder-shown)[value=""]:not(:focus-visible):not(:hover):not([aria-invalid=true]):not([aria-readonly=true])': { + ...formStyles.input.empty, + }, + + ':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' }, + ':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' }, + ':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' }, + }; +} diff --git a/tools/preset-sync/index.ts b/tools/preset-sync/index.ts new file mode 100755 index 0000000000..243bd61137 --- /dev/null +++ b/tools/preset-sync/index.ts @@ -0,0 +1,124 @@ +import fs from 'fs'; +import path from 'path'; + +/* eslint-disable no-console */ +const PRESETS = { + arbitrum: 'https://arbitrum.blockscout.com', + base: 'https://base.blockscout.com', + celo_alfajores: 'https://celo-alfajores.blockscout.com', + eth: 'https://eth.blockscout.com', + eth_goerli: 'https://eth-goerli.blockscout.com', + eth_sepolia: 'https://eth-sepolia.blockscout.com', + gnosis: 'https://gnosis.blockscout.com', + optimism: 'https://optimism.blockscout.com', + optimism_sepolia: 'https://optimism-sepolia.blockscout.com', + polygon: 'https://polygon.blockscout.com', + rootstock_testnet: 'https://rootstock-testnet.blockscout.com', + stability_testnet: 'https://stability-testnet.blockscout.com', + zkevm: 'https://zkevm.blockscout.com', + zksync: 'https://zksync.blockscout.com', + // main === staging + main: 'https://eth-sepolia.k8s-dev.blockscout.com', +}; + +const LOCAL_ENVS = { + NEXT_PUBLIC_APP_PROTOCOL: 'http', + NEXT_PUBLIC_APP_HOST: 'localhost', + NEXT_PUBLIC_APP_PORT: '3000', + NEXT_PUBLIC_APP_ENV: 'development', + NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL: 'ws', +}; + +const IGNORED_ENVS = [ + 'NEXT_PUBLIC_GIT_COMMIT_SHA', + 'NEXT_PUBLIC_GIT_TAG', +]; + +function parseScriptArgs() { + const args = process.argv.slice(2); + + const result: Record = {}; + args.forEach(arg => { + const [ key, value ] = arg.split('='); + if (key && value) { + result[key.replace(/^--/, '') as keyof typeof result] = value; + } + }); + + return result; +} + +function getSecretEnvsList() { + const fileContent = fs.readFileSync(path.resolve(__dirname, '../../.env.example'), 'utf8'); + const result = fileContent.split('\n').map((line) => line.split('=')[0]); + return result; +} + +function updateFileContent(envsEntries: Array<[ string, string ]>, presetId: keyof typeof PRESETS) { + const presetEnvsContent = envsEntries + .map(([ key, value ]) => `${ key }=${ value }`) + .join('\n'); + + const chainName = envsEntries.find(([ key ]) => key === 'NEXT_PUBLIC_NETWORK_NAME')?.[1] ?? 'Unknown'; + + const explorerUrl = PRESETS[presetId]; + + const localEnvsContent = Object.entries(LOCAL_ENVS) + .map(([ key, value ]) => `${ key }=${ value }`) + .join('\n'); + + const content = + `# Set of ENVs for ${ chainName } network explorer\n` + + '# ' + explorerUrl + '\n' + + `# This is an auto-generated file. To update all values, run "yarn preset:sync --name=${ presetId }"\n` + + '\n' + + '# Local ENVs\n' + + localEnvsContent + '\n' + + '\n' + + '# Instance ENVs\n' + + presetEnvsContent; + + fs.writeFileSync(path.resolve(__dirname, `../../configs/envs/.env.${ presetId }`), content); +} + +async function updatePresetFile(presetId: keyof typeof PRESETS) { + const secretEnvs = getSecretEnvsList(); + + const instanceUrl = PRESETS[presetId]; + const response = await fetch(`${ instanceUrl }/node-api/config`); + const instanceConfig = await response.json() as Record<'envs', Record>; + + const ignoredEnvs = [ + ...Object.keys(LOCAL_ENVS), + ...IGNORED_ENVS, + ...secretEnvs, + ]; + + const presetEnvsEntries = Object.entries(instanceConfig.envs) + .filter(([ key ]) => !ignoredEnvs.includes(key)); + + updateFileContent(presetEnvsEntries, presetId); +} + +async function run() { + console.log(`🌀 Syncing preset configuration file...`); + + const args = parseScriptArgs(); + if (!args.name) { + console.log('🚨 No "--name" argument is provided. Exiting...'); + return; + } + + const presetId = args.name as keyof typeof PRESETS; + const instanceUrl = PRESETS[presetId]; + if (!instanceUrl) { + console.log(`🚨 No preset with name "${ presetId }" found. Exiting...`); + return; + } + + await updatePresetFile(presetId); + + console.log(`✅ Done!`); +} + +run(); diff --git a/tools/preset-sync/tsconfig.json b/tools/preset-sync/tsconfig.json new file mode 100644 index 0000000000..b9056abc0f --- /dev/null +++ b/tools/preset-sync/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "node16", + "target": "es2022", + "resolveJsonModule": false, + "isolatedModules": false, + "noEmit": false, + "moduleResolution": "node16" + }, + "include": [ + "./index.ts", + ] +} + \ No newline at end of file diff --git a/tools/scripts/dev.preset.sh b/tools/scripts/dev.preset.sh new file mode 100755 index 0000000000..d0a0d49e66 --- /dev/null +++ b/tools/scripts/dev.preset.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +if [ "$#" -ne 1 ]; then + echo "Usage: yarn dev:preset " + exit 1 +fi + +preset_name="$1" +config_file="./configs/envs/.env.${preset_name}" +secrets_file="./configs/envs/.env.secrets" + +if [ ! -f "$config_file" ]; then + echo "Error: File '$config_file' not found." + exit 1 +fi + +# download assets for the running instance +dotenv \ + -e $config_file \ + -- bash -c './deploy/scripts/download_assets.sh ./public/assets/configs' + +yarn svg:build-sprite +echo "" + +# generate envs.js file and run the app +dotenv \ + -v NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) \ + -v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \ + -e $config_file \ + -e $secrets_file \ + -- bash -c './deploy/scripts/make_envs_script.sh && next dev -p $NEXT_PUBLIC_APP_PORT' | +pino-pretty \ No newline at end of file diff --git a/tools/scripts/dev.sh b/tools/scripts/dev.sh new file mode 100755 index 0000000000..e89950eac4 --- /dev/null +++ b/tools/scripts/dev.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# download assets for the running instance +dotenv \ + -e .env.development.local \ + -e .env.local \ + -e .env.development \ + -e .env \ + -- bash -c './deploy/scripts/download_assets.sh ./public/assets/configs' + +yarn svg:build-sprite +echo "" + +# generate envs.js file and run the app +dotenv \ + -v NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) \ + -v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \ + -e .env.secrets \ + -e .env.development.local \ + -e .env.local \ + -e .env.development \ + -e .env \ + -- bash -c './deploy/scripts/make_envs_script.sh && next dev -p $NEXT_PUBLIC_APP_PORT' | +pino-pretty \ No newline at end of file diff --git a/tools/scripts/docker.preset.sh b/tools/scripts/docker.preset.sh new file mode 100755 index 0000000000..58cd446a69 --- /dev/null +++ b/tools/scripts/docker.preset.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ "$#" -ne 1 ]; then + echo "Usage: yarn start:docker:preset " + exit 1 +fi + +preset_name="$1" +config_file="./configs/envs/.env.${preset_name}" +secrets_file="./configs/envs/.env.secrets" + +if [ ! -f "$config_file" ]; then + echo "Error: File '$config_file' not found." + exit 1 +fi + +if [ ! -f "$secrets_file" ]; then + echo "Error: File '$secrets_file' not found." + exit 1 +fi + +docker run -p 3000:3000 --env-file $config_file --env-file $secrets_file blockscout-frontend:local | pino-pretty \ No newline at end of file diff --git a/tools/scripts/favicon-generator.dev.sh b/tools/scripts/favicon-generator.dev.sh new file mode 100755 index 0000000000..16d8f8aeb7 --- /dev/null +++ b/tools/scripts/favicon-generator.dev.sh @@ -0,0 +1,19 @@ +secrets_file="./configs/envs/.env.secrets" +favicon_folder="./public/assets/favicon/" +master_url="https://raw.githubusercontent.com/blockscout/frontend/main/tools/scripts/favicon.svg" + +if [ ! -f "$secrets_file" ]; then + echo "Error: File '$secrets_file' not found." + exit 1 +fi + +dotenv \ + -v MASTER_URL=$master_url \ + -e $secrets_file \ + -- bash -c 'cd ./deploy/tools/favicon-generator && ./script.sh' + +if [ -d "$favicon_folder" ]; then + rm -r "$favicon_folder" +fi +mkdir -p "$favicon_folder" +cp -r ./deploy/tools/favicon-generator/output/* "$favicon_folder" \ No newline at end of file diff --git a/tools/scripts/favicon.svg b/tools/scripts/favicon.svg new file mode 100644 index 0000000000..b69b047ffe --- /dev/null +++ b/tools/scripts/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/tools/scripts/pw.docker.deps.sh b/tools/scripts/pw.docker.deps.sh new file mode 100755 index 0000000000..719930361f --- /dev/null +++ b/tools/scripts/pw.docker.deps.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +yarn install --modules-folder node_modules_linux diff --git a/tools/scripts/pw.docker.sh b/tools/scripts/pw.docker.sh new file mode 100755 index 0000000000..49c763f9ad --- /dev/null +++ b/tools/scripts/pw.docker.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +export NODE_PATH=$(pwd)/node_modules_linux + +yarn test:pw "$@" diff --git a/tools/scripts/pw.sh b/tools/scripts/pw.sh new file mode 100755 index 0000000000..0754f0be1e --- /dev/null +++ b/tools/scripts/pw.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +config_file="./configs/envs/.env.pw" + +rm -rf ./playwright/.cache + +dotenv \ + -e $config_file \ + -- bash -c './deploy/scripts/make_envs_script.sh ./playwright/envs.js' + +yarn svg:build-sprite + +# Check if the "--affected" argument is present in the script args +check_affected_flag() { + local affected_flag=false + + for arg in "$@"; do + if [[ "$arg" = "--affected"* ]]; then + # Extract the value after the equals sign + is_affected_value=${is_affected_arg#*=} + + if [ "$is_affected_value" != "false" ]; then + affected_flag=true + fi + + break + fi + done + + echo "$affected_flag" +} + +# Remove the "--affected" argument from the script args +filter_arguments() { + local args=() + + for arg in "$@"; do + if [[ "$arg" != "--affected"* ]]; then + args+=("$arg") + fi + done + + echo "${args[@]}" +} + +get_files_to_run() { + local is_affected=$1 + local files_to_run="" + + if [ "$is_affected" = true ]; then + affected_tests_file="./playwright/affected-tests.txt" + + if [ -f "$affected_tests_file" ]; then + file_content=$(<"$affected_tests_file") + files_to_run="${file_content//$'\n'/$' '}" + + if [ -z "$files_to_run" ]; then + exit 1 + fi + fi + fi + + echo "$files_to_run" +} + +args=$(filter_arguments "$@") +affected_flag=$(check_affected_flag "$@") +files_to_run=$(get_files_to_run "$affected_flag") +if [ $? -eq 1 ]; then + echo "No affected tests found in the file. Exiting..." + exit 0 +fi + +echo "Running Playwright tests with the following arguments: $args" +echo "Affected flag: $affected_flag" +echo "Files to run: $files_to_run" + +dotenv \ + -v NODE_OPTIONS=\"--max-old-space-size=4096\" \ + -e $config_file \ + -- playwright test -c playwright-ct.config.ts $files_to_run $args + diff --git a/tsconfig.jest.json b/tsconfig.jest.json new file mode 100644 index 0000000000..23a3d957e4 --- /dev/null +++ b/tsconfig.jest.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "incremental": true, + "baseUrl": ".", + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"], + "exclude": ["node_modules", "node_modules_linux"], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..dab9681982 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.node.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"], + "exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator"], +} diff --git a/types/api/account.ts b/types/api/account.ts new file mode 100644 index 0000000000..030a10f0f0 --- /dev/null +++ b/types/api/account.ts @@ -0,0 +1,214 @@ +import type { AddressParam } from './addressParams'; +export interface AddressTag { + address_hash: string; + address: AddressParam; + name: string; + id: string; +} + +export type AddressTags = Array + +export type AddressTagsResponse = { + items: AddressTags; + next_page_params: { + id: number; + items_count: number; + } | null; +} + +export interface ApiKey { + api_key: string; + name: string; +} + +export type ApiKeys = Array + +export interface ModelError { + message: string; +} + +export interface NotificationDirection { + incoming: boolean; + outcoming: boolean; +} + +export interface NotificationSettings { + 'native': NotificationDirection; + 'ERC-20': NotificationDirection; + 'ERC-721': NotificationDirection; + 'ERC-404': NotificationDirection; +} + +export interface NotificationMethods { + email: boolean; +} + +export interface Transaction { + fromAddressHash?: string; + toAddressHash?: string; + createdContractAddressHash?: string; +} + +export interface TransactionTag { + transaction_hash: string; + name: string; + id: string; +} + +export type TransactionTags = Array + +export type TransactionTagsResponse = { + items: TransactionTags; + next_page_params: { + id: number; + items_count: number; + } | null; +} + +export type Transactions = Array + +export interface UserInfo { + name?: string; + nickname?: string; + email: string | null; + avatar?: string; +} + +export interface WatchlistAddress { + address_hash: string; + name: string; + address_balance: string; + exchange_rate: string; + notification_settings: NotificationSettings; + notification_methods: NotificationMethods; + id: string; + address: AddressParam; + tokens_count: number; + tokens_fiat_value: string; + tokens_overflow: boolean; +} + +export interface WatchlistAddressNew { + addressName: string; + notificationSettings: NotificationSettings; +} + +export type WatchlistAddresses = Array + +export type WatchlistResponse = { + items: WatchlistAddresses; + next_page_params: { + id: number; + items_count: number; + } | null; +} + +export type CustomAbis = Array + +export interface CustomAbi { + name: string; + id: string; + contract_address_hash: string; + contract_address: AddressParam; + abi: Array; +} + +export interface AbiItem { + type: 'function'; + stateMutability: 'nonpayable' | 'view'; + payable: boolean; + outputs: Array; + name: string; + inputs: Array; + constant: boolean; +} + +interface AbiInputOutput { + type: 'uint256' | 'address'; + name: string; +} + +export type WatchlistErrors = { + address_hash?: Array; + name?: Array; + watchlist_id?: Array; +} + +export type CustomAbiErrors = { + address_hash?: Array; + name?: Array; + abi?: Array; + identity_id?: Array; +} + +export type ApiKeyErrors = { + name?: Array; + identity_id?: Array; +}; + +export type AddressTagErrors = { + address_hash: Array; + name: Array; + identity_id?: Array; +} + +export type TransactionTagErrors = { + tx_hash: Array; + name: Array; + identity_id?: Array; +} + +export interface VerifiedAddress { + userId: string; + chainId: string; + contractAddress: string; + verifiedDate: string; + metadata: { + tokenName: string | null; + tokenSymbol: string | null; + }; +} + +export interface VerifiedAddressResponse { + verifiedAddresses: Array; +} + +export interface TokenInfoApplicationConfig { + projectSectors: Array; +} + +export interface TokenInfoApplication { + adminComments?: string; + coinGeckoTicker?: string; + coinMarketCapTicker?: string; + comment?: string; + defiLlamaTicker?: string; + discord?: string; + docs?: string; + facebook?: string; + github?: string; + iconUrl: string; + id: string; + linkedin?: string; + medium?: string; + openSea?: string; + projectDescription?: string; + projectEmail: string; + projectName?: string; + projectSector?: string; + projectWebsite: string; + reddit?: string; + requesterEmail: string; + requesterName: string; + slack?: string; + status: 'STATUS_UNKNOWN' | 'IN_PROCESS' | 'APPROVED' | 'REJECTED' | 'UPDATE_REQUIRED'; + support?: string; + telegram?: string; + tokenAddress: string; + twitter?: string; + updatedAt: string; +} + +export interface TokenInfoApplications { + submissions: Array; +} diff --git a/types/api/address.ts b/types/api/address.ts new file mode 100644 index 0000000000..d8235125e0 --- /dev/null +++ b/types/api/address.ts @@ -0,0 +1,253 @@ +import type { Transaction } from 'types/api/transaction'; + +import type { UserTags, AddressImplementation } from './addressParams'; +import type { Block } from './block'; +import type { InternalTransaction } from './internalTransaction'; +import type { MudWorldSchema, MudWorldTable } from './mudWorlds'; +import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; +import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; + +export interface Address extends UserTags { + block_number_balance_updated_at: number | null; + coin_balance: string | null; + creator_address_hash: string | null; + creation_tx_hash: string | null; + exchange_rate: string | null; + ens_domain_name: string | null; + // TODO: if we are happy with tabs-counters method, should we delete has_something fields? + has_beacon_chain_withdrawals?: boolean; + has_decompiled_code: boolean; + has_logs: boolean; + has_token_transfers: boolean; + has_tokens: boolean; + has_validated_blocks: boolean; + hash: string; + implementations: Array | null; + is_contract: boolean; + is_verified: boolean; + name: string | null; + token: TokenInfo | null; + watchlist_address_id: number | null; +} + +export interface AddressCounters { + transactions_count: string; + token_transfers_count: string; + gas_usage_count: string | null; + validations_count: string | null; +} + +export interface AddressTokenBalance { + token: TokenInfo; + token_id: string | null; + value: string; + token_instance: TokenInstance | null; +} + +export type AddressNFT = TokenInstance & { + token: TokenInfo; + token_type: Omit; + value: string; +} + +export type AddressCollection = { + token: TokenInfo; + amount: string; + token_instances: Array>; +} + +export interface AddressTokensResponse { + items: Array; + next_page_params: { + items_count: number; + token_name: string | null; + token_type: TokenType; + value: number; + fiat_value: string | null; + } | null; +} + +export interface AddressNFTsResponse { + items: Array; + next_page_params: { + items_count: number; + token_id: string; + token_type: TokenType; + token_contract_address_hash: string; + } | null; +} + +export interface AddressCollectionsResponse { + items: Array; + next_page_params: { + token_contract_address_hash: string; + token_type: TokenType; + } | null; +} + +export interface AddressTokensBalancesSocketMessage { + overflow: boolean; + token_balances: Array; +} + +export interface AddressTransactionsResponse { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + } | null; +} + +export const AddressFromToFilterValues = [ 'from', 'to' ] as const; + +export type AddressFromToFilter = typeof AddressFromToFilterValues[number] | undefined; + +export type AddressTxsFilters = { + filter: AddressFromToFilter; +} + +export interface AddressTokenTransferResponse { + items: Array; + next_page_params: TokenTransferPagination | null; +} + +export type AddressTokenTransferFilters = { + filter?: AddressFromToFilter; + type?: Array; + token?: string; +} + +export type AddressTokensFilter = { + type: TokenType; +} + +export type AddressNFTTokensFilter = { + type: Array | undefined; +} + +export interface AddressCoinBalanceHistoryItem { + block_number: number; + block_timestamp: string; + delta: string; + transaction_hash: string | null; + value: string; +} + +export interface AddressCoinBalanceHistoryResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + } | null; +} + +// remove after api release +export type AddressCoinBalanceHistoryChartOld = Array<{ + date: string; + value: string; +}> + +export type AddressCoinBalanceHistoryChart = { + items: Array<{ + date: string; + value: string; + }>; + days: number; +}; + +export interface AddressBlocksValidatedResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + }; +} +export interface AddressInternalTxsResponse { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + transaction_index: number; + } | null; +} + +export type AddressWithdrawalsResponse = { + items: Array; + next_page_params: { + index: number; + items_count: number; + }; +} + +export type AddressWithdrawalsItem = { + amount: string; + block_number: number; + index: number; + timestamp: string; + validator_index: number; +} + +export type AddressTabsCounters = { + internal_txs_count: number | null; + logs_count: number | null; + token_balances_count: number | null; + token_transfers_count: number | null; + transactions_count: number | null; + validations_count: number | null; + withdrawals_count: number | null; +} + +// MUD framework +export type AddressMudTableItem = { + schema: MudWorldSchema; + table: MudWorldTable; +} + +export type AddressMudTables = { + items: Array; + next_page_params: { + items_count: number; + table_id: string; + }; +} + +export type AddressMudTablesFilter = { + q?: string; +} + +export type AddressMudRecords = { + items: Array; + schema: MudWorldSchema; + table: MudWorldTable; + next_page_params: { + items_count: number; + key0: string; + key1: string; + key_bytes: string; + }; +} + +export type AddressMudRecordsItem = { + decoded: Record>; + id: string; + is_deleted: boolean; + timestamp: string; +} + +export type AddressMudRecordsFilter = { + filter_key0?: string; + filter_key1?: string; +} + +export type AddressMudRecordsSorting = { + sort: 'key0' | 'key1'; + order: 'asc' | 'desc' | undefined; +} + +export type AddressMudRecord = { + record: AddressMudRecordsItem; + schema: MudWorldSchema; + table: MudWorldTable; +} diff --git a/types/api/addressMetadata.ts b/types/api/addressMetadata.ts new file mode 100644 index 0000000000..e433ca21ef --- /dev/null +++ b/types/api/addressMetadata.ts @@ -0,0 +1,48 @@ +export interface AddressMetadataInfo { + addresses: Record; + reputation: number | null; + }>; +} + +export type AddressMetadataTagType = 'name' | 'generic' | 'classifier' | 'information' | 'note' | 'protocol'; + +// Response model from Metadata microservice API +export interface AddressMetadataTag { + slug: string; + name: string; + tagType: AddressMetadataTagType; + ordinal: number; + meta: string | null; +} + +// Response model from Blockscout API with parsed meta field +export interface AddressMetadataTagApi extends Omit { + meta: { + textColor?: string; + bgColor?: string; + tagIcon?: string; + tagUrl?: string; + tooltipIcon?: string; + tooltipTitle?: string; + tooltipDescription?: string; + tooltipUrl?: string; + appID?: string; + appMarketplaceURL?: string; + appLogoURL?: string; + appActionButtonText?: string; + warpcastHandle?: string; + } | null; +} + +// TAG SUBMISSION + +export interface PublicTagType { + id: string; + type: AddressMetadataTagType; + description: string; +} + +export interface PublicTagTypesResponse { + tagTypes: Array; +} diff --git a/types/api/addressParams.ts b/types/api/addressParams.ts new file mode 100644 index 0000000000..b7bc4393b8 --- /dev/null +++ b/types/api/addressParams.ts @@ -0,0 +1,38 @@ +import type { AddressMetadataTagApi } from './addressMetadata'; + +export interface AddressImplementation { + address: string; + name: string | null; +} + +export interface AddressTag { + label: string; + display_name: string; + address_hash: string; +} + +export interface WatchlistName { + label: string; + display_name: string; +} + +export interface UserTags { + private_tags: Array | null; + watchlist_names: Array | null; + public_tags: Array | null; +} + +export type AddressParamBasic = { + hash: string; + implementations: Array | null; + name: string | null; + is_contract: boolean; + is_verified: boolean | null; + ens_domain_name: string | null; + metadata?: { + reputation: number | null; + tags: Array; + } | null; +} + +export type AddressParam = UserTags & AddressParamBasic; diff --git a/types/api/addresses.ts b/types/api/addresses.ts new file mode 100644 index 0000000000..9c76000760 --- /dev/null +++ b/types/api/addresses.ts @@ -0,0 +1,13 @@ +import type { AddressParam } from './addressParams'; + +export type AddressesItem = AddressParam &{ tx_count: string; coin_balance: string } + +export type AddressesResponse = { + items: Array; + next_page_params: { + fetched_coin_balance: string; + hash: string; + items_count: number; + } | null; + total_supply: string; +} diff --git a/types/api/arbitrumL2.ts b/types/api/arbitrumL2.ts new file mode 100644 index 0000000000..7e6fd25442 --- /dev/null +++ b/types/api/arbitrumL2.ts @@ -0,0 +1,86 @@ +import type { Block } from './block'; +import type { Transaction } from './transaction'; + +export type ArbitrumL2MessagesItem = { + completion_transaction_hash: string | null; + id: number; + origination_address: string; + origination_timestamp: string | null; + origination_transaction_block_number: number; + origination_transaction_hash: string; + status: 'initiated' | 'sent' | 'confirmed' | 'relayed'; +} + +export type ArbitrumL2MessagesResponse = { + items: Array; + next_page_params: { + direction: string; + id: number; + items_count: number; + }; +} + +export type ArbitrumL2TxData = { + hash: string | null; + status: string | null; + timestamp: string | null; +} + +type ArbitrumL2BatchCommitmentTx = { + block_number: number; + hash: string; + status: string; + timestamp: string; +} + +export type ArbitrumL2TxnBatchesItem = { + blocks_count: number; + commitment_transaction: ArbitrumL2BatchCommitmentTx; + number: number; + transactions_count: number; +} + +export type ArbitrumL2TxnBatchesResponse = { + items: Array; + next_page_params: { + number: number; + items_count: number; + } | null; +} + +export type ArbitrumL2TxnBatch = { + after_acc: string; + before_acc: string; + commitment_transaction: ArbitrumL2BatchCommitmentTx; + end_block: number; + start_block: number; + number: number; + transactions_count: number; +} + +export type ArbitrumL2BatchTxs = { + items: Array; + next_page_params: { + batch_number: string; + block_number: number; + index: number; + items_count: number; + } | null; +} + +export type ArbitrumL2BatchBlocks = { + items: Array; + next_page_params: { + batch_number: string; + block_number: number; + items_count: number; + } | null; +} + +export const ARBITRUM_L2_TX_BATCH_STATUSES = [ + 'Processed on rollup' as const, + 'Sent to base' as const, + 'Confirmed on base' as const, +]; + +export type ArbitrumBatchStatus = typeof ARBITRUM_L2_TX_BATCH_STATUSES[number]; diff --git a/types/api/blobs.ts b/types/api/blobs.ts new file mode 100644 index 0000000000..7b8abb55fd --- /dev/null +++ b/types/api/blobs.ts @@ -0,0 +1,18 @@ +export interface TxBlob { + hash: string; + blob_data: string | null; + kzg_commitment: string | null; + kzg_proof: string | null; +} + +export type TxBlobs = { + items: Array; + next_page_params: null; +}; + +export interface Blob extends TxBlob { + transaction_hashes: Array<{ + block_consensus: boolean; + transaction_hash: string; + }>; +} diff --git a/types/api/block.ts b/types/api/block.ts new file mode 100644 index 0000000000..f18dcfbfd4 --- /dev/null +++ b/types/api/block.ts @@ -0,0 +1,114 @@ +import type { AddressParam } from 'types/api/addressParams'; +import type { Reward } from 'types/api/reward'; +import type { Transaction } from 'types/api/transaction'; + +import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2'; +import type { ZkSyncBatchesItem } from './zkSyncL2'; + +export type BlockType = 'block' | 'reorg' | 'uncle'; + +export interface Block { + height: number; + timestamp: string; + tx_count: number; + miner: AddressParam; + size: number; + hash: string; + parent_hash: string; + difficulty: string; + total_difficulty: string | null; + gas_used: string | null; + gas_limit: string; + nonce: string; + base_fee_per_gas: string | null; + burnt_fees: string | null; + priority_fee: string | null; + extra_data: string | null; + state_root: string | null; + rewards?: Array; + gas_target_percentage: number | null; + gas_used_percentage: number | null; + burnt_fees_percentage: number | null; + type: BlockType; + tx_fees: string | null; + uncles_hashes: Array; + withdrawals_count?: number; + // ROOTSTOCK FIELDS + bitcoin_merged_mining_coinbase_transaction?: string | null; + bitcoin_merged_mining_header?: string | null; + bitcoin_merged_mining_merkle_proof?: string | null; + hash_for_merged_mining?: string | null; + minimum_gas_price?: string | null; + // BLOB FIELDS + blob_gas_price?: string; + blob_gas_used?: string; + burnt_blob_fees?: string; + excess_blob_gas?: string; + blob_tx_count?: number; + // ZKSYNC FIELDS + zksync?: Omit & { + 'batch_number': number | null; + }; + arbitrum?: ArbitrumBlockData; +} + +type ArbitrumBlockData = { + 'batch_number': number; + 'commitment_transaction': ArbitrumL2TxData; + 'confirmation_transaction': ArbitrumL2TxData; + 'delayed_messages': number; + 'l1_block_height': number; + 'send_count': number; + 'send_root': string; + 'status': ArbitrumBatchStatus; +} + +export interface BlocksResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + } | null; +} + +export interface BlockTransactionsResponse { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + index: number; + } | null; +} + +export interface NewBlockSocketResponse { + average_block_time: string; + block: Block; +} + +export interface BlockFilters { + type?: BlockType; +} + +export type BlockWithdrawalsResponse = { + items: Array; + next_page_params: { + index: number; + items_count: number; + } | null; +} + +export type BlockWithdrawalsItem = { + amount: string; + index: number; + receiver: AddressParam; + validator_index: number; +} + +export interface BlockCountdownResponse { + result: { + CountdownBlock: string; + CurrentBlock: string; + EstimateTimeInSec: string; + RemainingBlock: string; + } | null; +} diff --git a/types/api/charts.ts b/types/api/charts.ts new file mode 100644 index 0000000000..a5504ded12 --- /dev/null +++ b/types/api/charts.ts @@ -0,0 +1,25 @@ +export interface ChartTransactionItem { + date: string; + tx_count: number | null; +} + +export interface ChartMarketItem { + date: string; + closing_price: string | null; + market_cap?: string | null; + tvl?: string | null; +} + +export interface ChartTransactionResponse { + chart_data: Array; +} + +export interface ChartMarketResponse { + available_supply: string; + chart_data: Array; +} + +export interface ChartSecondaryCoinPriceResponse { + available_supply: string; + chart_data: Array; +} diff --git a/types/api/configs.ts b/types/api/configs.ts new file mode 100644 index 0000000000..c04e29c359 --- /dev/null +++ b/types/api/configs.ts @@ -0,0 +1,3 @@ +export interface BackendVersionConfig { + backend_version: string; +} diff --git a/types/api/contract.ts b/types/api/contract.ts new file mode 100644 index 0000000000..845aa84b32 --- /dev/null +++ b/types/api/contract.ts @@ -0,0 +1,151 @@ +import type { Abi, AbiType } from 'abitype'; + +export type SmartContractMethodArgType = AbiType; +export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; + +export type SmartContractLicenseType = +'none' | +'unlicense' | +'mit' | +'gnu_gpl_v2' | +'gnu_gpl_v3' | +'gnu_lgpl_v2_1' | +'gnu_lgpl_v3' | +'bsd_2_clause' | +'bsd_3_clause' | +'mpl_2_0' | +'osl_3_0' | +'apache_2_0' | +'gnu_agpl_v3' | +'bsl_1_1'; + +export interface SmartContract { + deployed_bytecode: string | null; + creation_bytecode: string | null; + is_self_destructed: boolean; + abi: Abi | null; + compiler_version: string | null; + evm_version: string | null; + optimization_enabled: boolean | null; + optimization_runs: number | null; + name: string | null; + verified_at: string | null; + is_blueprint: boolean | null; + is_verified: boolean | null; + is_verified_via_eth_bytecode_db: boolean | null; + is_changed_bytecode: boolean | null; + + // sourcify info >>> + is_verified_via_sourcify: boolean | null; + is_fully_verified: boolean | null; + is_partially_verified: boolean | null; + sourcify_repo_url: string | null; + // <<<< + source_code: string | null; + constructor_args: string | null; + decoded_constructor_args: Array | null; + can_be_visualized_via_sol2uml: boolean | null; + file_path: string; + additional_sources: Array<{ file_path: string; source_code: string }>; + external_libraries: Array | null; + compiler_settings?: { + evmVersion?: string; + remappings?: Array; + }; + verified_twin_address_hash: string | null; + minimal_proxy_address_hash: string | null; + language: string | null; + license_type: SmartContractLicenseType | null; + certified?: boolean; +} + +export type SmartContractDecodedConstructorArg = [ + string, + { + internalType: SmartContractMethodArgType; + name: string; + type: SmartContractMethodArgType; + } +] + +export interface SmartContractExternalLibrary { + address_hash: string; + name: string; +} + +// VERIFICATION + +export type SmartContractVerificationMethodApi = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' +| 'vyper-code' | 'vyper-multi-part' | 'vyper-standard-input'; + +export interface SmartContractVerificationConfigRaw { + solidity_compiler_versions: Array; + solidity_evm_versions: Array; + verification_options: Array; + vyper_compiler_versions: Array; + vyper_evm_versions: Array; + is_rust_verifier_microservice_enabled: boolean; + license_types: Record; +} + +export type SmartContractVerificationResponse = { + status: 'error'; + errors: SmartContractVerificationError; +} | { + status: 'success'; +} + +export interface SmartContractVerificationError { + contract_source_code?: Array; + files?: Array; + interfaces?: Array; + compiler_version?: Array; + constructor_arguments?: Array; + name?: Array; +} + +export type SolidityscanReport = { + scan_report: { + contractname: string; + scan_status: string; + scan_summary: { + issue_severity_distribution: { + critical: number; + gas: number; + high: number; + informational: number; + low: number; + medium: number; + }; + lines_analyzed_count: number; + scan_time_taken: number; + score: string; + score_v2: string; + threat_score: string; + }; + scanner_reference_url: string; + }; +} + +type SmartContractSecurityAudit = { + audit_company_name: string; + audit_publish_date: string; + audit_report_url: string; +} + +export type SmartContractSecurityAudits = { + items: Array; +} + +export type SmartContractSecurityAuditSubmission = { + 'address_hash': string; + 'submitter_name': string; + 'submitter_email': string; + 'is_project_owner': boolean; + 'project_name': string; + 'project_url': string; + 'audit_company_name': string; + 'audit_report_url': string; + 'audit_publish_date': string; + 'comment'?: string; +} diff --git a/types/api/contracts.ts b/types/api/contracts.ts new file mode 100644 index 0000000000..75998004e9 --- /dev/null +++ b/types/api/contracts.ts @@ -0,0 +1,36 @@ +import type { AddressParam } from './addressParams'; +import type { SmartContractLicenseType } from './contract'; + +export interface VerifiedContract { + address: AddressParam; + certified?: boolean; + coin_balance: string; + compiler_version: string; + language: 'vyper' | 'yul' | 'solidity'; + has_constructor_args: boolean; + optimization_enabled: boolean; + tx_count: number | null; + verified_at: string; + market_cap: string | null; + license_type: SmartContractLicenseType | null; +} + +export interface VerifiedContractsResponse { + items: Array; + next_page_params: { + items_count: string; + smart_contract_id: string; + } | null; +} + +export interface VerifiedContractsFilters { + q: string | undefined; + filter: 'vyper' | 'solidity' | 'yul' | undefined; +} + +export type VerifiedContractsCounters = { + new_smart_contracts_24h: string; + new_verified_smart_contracts_24h: string; + smart_contracts: string; + verified_smart_contracts: string; +} diff --git a/types/api/decodedInput.ts b/types/api/decodedInput.ts new file mode 100644 index 0000000000..d390de03e0 --- /dev/null +++ b/types/api/decodedInput.ts @@ -0,0 +1,12 @@ +export interface DecodedInput { + method_call: string; + method_id: string; + parameters: Array; +} + +export interface DecodedInputParams { + name: string; + type: string; + value: string | Array | Record; + indexed?: boolean; +} diff --git a/types/api/ens.ts b/types/api/ens.ts new file mode 100644 index 0000000000..16ac03ee31 --- /dev/null +++ b/types/api/ens.ts @@ -0,0 +1,16 @@ +import type * as bens from '@blockscout/bens-types'; + +export interface EnsAddressLookupFilters extends Pick { + protocols: Array | undefined; +} + +export interface EnsDomainLookupFilters extends Pick { + protocols: Array | undefined; +} + +export interface EnsLookupSorting { + sort: 'registration_date'; + order: Exclude; +} + +export type EnsDomainLookupFiltersOptions = Array<'resolved_to' | 'owned_by' | 'with_inactive'>; diff --git a/types/api/fee.ts b/types/api/fee.ts new file mode 100644 index 0000000000..82ad5c5b8b --- /dev/null +++ b/types/api/fee.ts @@ -0,0 +1,4 @@ +export interface Fee { + type: string; + value: string | null; +} diff --git a/types/api/indexingStatus.ts b/types/api/indexingStatus.ts new file mode 100644 index 0000000000..8d186785fc --- /dev/null +++ b/types/api/indexingStatus.ts @@ -0,0 +1,6 @@ +export type IndexingStatus = { + finished_indexing: boolean; + finished_indexing_blocks: boolean; + indexed_blocks_ratio: string; + indexed_internal_transactions_ratio: string; +} diff --git a/types/api/internalTransaction.ts b/types/api/internalTransaction.ts new file mode 100644 index 0000000000..29b9d42d21 --- /dev/null +++ b/types/api/internalTransaction.ts @@ -0,0 +1,36 @@ +import type { AddressParam } from './addressParams'; + +export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward' + +export type InternalTransaction = ( + { + to: AddressParam; + created_contract: null; + } | + { + to: null; + created_contract: AddressParam; + } +) & { + error: string | null; + success: boolean; + type: TxInternalsType; + transaction_hash: string; + from: AddressParam; + value: string; + index: number; + block: number; + timestamp: string; + gas_limit: string; +} + +export interface InternalTransactionsResponse { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + transaction_hash: string; + transaction_index: number; + } | null; +} diff --git a/types/api/log.ts b/types/api/log.ts new file mode 100644 index 0000000000..eff0c95fa5 --- /dev/null +++ b/types/api/log.ts @@ -0,0 +1,30 @@ +import type { AddressParam } from './addressParams'; +import type { DecodedInput } from './decodedInput'; + +export interface Log { + address: AddressParam; + topics: Array; + data: string; + index: number; + decoded: DecodedInput | null; + tx_hash: string | null; +} + +export interface LogsResponseTx { + items: Array; + next_page_params: { + index: number; + items_count: number; + transaction_hash: string; + } | null; +} + +export interface LogsResponseAddress { + items: Array; + next_page_params: { + index: number; + items_count: number; + transaction_index: number; + block_number: number; + } | null; +} diff --git a/types/api/mudWorlds.ts b/types/api/mudWorlds.ts new file mode 100644 index 0000000000..b9c5eb4d91 --- /dev/null +++ b/types/api/mudWorlds.ts @@ -0,0 +1,30 @@ +import type { AddressParam } from './addressParams'; + +export type MudWorldsResponse = { + items: Array; + next_page_params: { + items_count: number; + world: string; + }; +} + +export type MudWorldItem = { + address: AddressParam; + coin_balance: string; + tx_count: number | null; +} + +export type MudWorldSchema = { + key_names: Array; + key_types: Array; + value_names: Array; + value_types: Array; +}; + +export type MudWorldTable = { + table_full_name: string; + table_id: string; + table_name: string; + table_namespace: string; + table_type: string; +} diff --git a/types/api/noves.ts b/types/api/noves.ts new file mode 100644 index 0000000000..2f85ca6391 --- /dev/null +++ b/types/api/noves.ts @@ -0,0 +1,125 @@ +export interface NovesResponseData { + txTypeVersion: number; + chain: string; + accountAddress: string; + classificationData: NovesClassificationData; + rawTransactionData: NovesRawTransactionData; +} + +export interface NovesClassificationData { + type: string; + typeFormatted?: string; + description: string; + sent: Array; + received: Array; + approved?: Approved; + protocol?: { + name: string | null; + }; + source: { + type: string | null; + }; + message?: string; + deployedContractAddress?: string; +} + +export interface Approved { + amount: string; + spender: string; + token?: NovesToken; + nft?: NovesNft; +} + +export interface NovesSentReceived { + action: string; + actionFormatted?: string; + amount: string; + to: NovesTo; + from: NovesFrom; + token?: NovesToken; + nft?: NovesNft; +} + +export interface NovesToken { + symbol: string; + name: string; + decimals: number; + address: string; + id?: string; +} + +export interface NovesNft { + name: string; + id: string; + symbol: string; + address: string; +} + +export interface NovesFrom { + name: string | null; + address: string; +} + +export interface NovesTo { + name: string | null; + address: string | null; +} + +export interface NovesRawTransactionData { + transactionHash: string; + fromAddress: string; + toAddress: string; + blockNumber: number; + gas: number; + gasPrice: number; + transactionFee: NovesTransactionFee | number; + timestamp: number; +} + +export interface NovesTransactionFee { + amount: string; + currency?: string; + token?: { + decimals: number; + symbol: string; + }; +} + +export interface NovesAccountHistoryResponse { + hasNextPage: boolean; + items: Array; + pageNumber: number; + pageSize: number; + next_page_params?: { + startBlock: string; + endBlock: string; + pageNumber: number; + pageSize: number; + ignoreTransactions: string; + viewAsAccountAddress: string; + }; +} + +export const NovesHistoryFilterValues = [ 'received', 'sent' ] as const; + +export type NovesHistoryFilterValue = typeof NovesHistoryFilterValues[number] | undefined; + +export interface NovesHistoryFilters { + filter?: NovesHistoryFilterValue; +} + +export interface NovesDescribeResponse { + type: string; + description: string; +} + +export interface NovesDescribeTxsResponse { + txHash: string; + type: string; + description: string; +}[]; + +export interface NovesTxTranslation { + data?: NovesDescribeTxsResponse; + isLoading: boolean; +} diff --git a/types/api/optimisticL2.ts b/types/api/optimisticL2.ts new file mode 100644 index 0000000000..3940715533 --- /dev/null +++ b/types/api/optimisticL2.ts @@ -0,0 +1,98 @@ +import type { AddressParam } from './addressParams'; + +export type OptimisticL2DepositsItem = { + l1_block_number: number; + l1_tx_hash: string; + l1_block_timestamp: string; + l1_tx_origin: string; + l2_tx_gas_limit: string; + l2_tx_hash: string; +} + +export type OptimisticL2DepositsResponse = { + items: Array; + next_page_params: { + items_count: number; + l1_block_number: number; + tx_hash: string; + }; +} + +export type OptimisticL2OutputRootsItem = { + l1_block_number: number; + l1_timestamp: string; + l1_tx_hash: string; + l2_block_number: number; + l2_output_index: number; + output_root: string; +} + +export type OptimisticL2OutputRootsResponse = { + items: Array; + next_page_params: { + index: number; + items_count: number; + }; +} + +export type OptimisticL2TxnBatchesItem = { + l1_tx_hashes: Array; + l1_timestamp: string; + l2_block_number: number; + tx_count: number; +} + +export type OptimisticL2TxnBatchesResponse = { + items: Array; + next_page_params: { + block_number: number; + items_count: number; + }; +} + +export type OptimisticL2WithdrawalsItem = { + 'challenge_period_end': string | null; + 'from': AddressParam | null; + 'l1_tx_hash': string | null; + 'l2_timestamp': string | null; + 'l2_tx_hash': string; + 'msg_nonce': number; + 'msg_nonce_version': number; + 'status': string; +} + +export type OptimisticL2WithdrawalStatus = + 'Waiting for state root' | + 'Ready to prove' | + 'In challenge period' | + 'Waiting a game to resolve' | + 'Ready to prove' | + 'Proven' | + 'Ready for relay' | + 'Relayed'; + +export type OptimisticL2WithdrawalsResponse = { + items: Array; + 'next_page_params': { + 'items_count': number; + 'nonce': string; + }; +} + +export type OptimisticL2DisputeGamesResponse = { + items: Array; + 'next_page_params': { + 'items_count': number; + 'index': number; + }; +} + +export type OptimisticL2DisputeGamesItem = { + contract_address: string; + created_at: string; + game_type: number; + index: number; + l2_block_number: number; + resolved_at: string | null; + status: string; +} diff --git a/types/api/rawTrace.ts b/types/api/rawTrace.ts new file mode 100644 index 0000000000..b46a844d8d --- /dev/null +++ b/types/api/rawTrace.ts @@ -0,0 +1,20 @@ +export interface RawTrace { + action: { + callType: string; + from: string; + gas: string; + input: string; + to: string; + value: string; + }; + result: { + gasUsed: string; + output: string; + }; + error: string | null; + subtraces: number; + traceAddress: Array; + type: string; +} + +export type RawTracesResponse = Array; diff --git a/types/api/reward.ts b/types/api/reward.ts new file mode 100644 index 0000000000..5800c77e43 --- /dev/null +++ b/types/api/reward.ts @@ -0,0 +1,4 @@ +export interface Reward { + reward: string; + type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward' | 'POA Mania Reward'; +} diff --git a/types/api/search.ts b/types/api/search.ts new file mode 100644 index 0000000000..7d738e7cbd --- /dev/null +++ b/types/api/search.ts @@ -0,0 +1,112 @@ +import type { TokenType } from 'types/api/token'; + +export type SearchResultType = 'token' | 'address' | 'block' | 'transaction' | 'contract'; + +export interface SearchResultToken { + type: 'token'; + name: string; + symbol: string; + address: string; + token_url: string; + address_url: string; + icon_url: string | null; + token_type: TokenType; + exchange_rate: string | null; + total_supply: string | null; + is_verified_via_admin_panel: boolean; + is_smart_contract_verified: boolean; +} + +export interface SearchResultAddressOrContract { + type: 'address' | 'contract'; + name: string | null; + address: string; + is_smart_contract_verified: boolean; + certified?: true; + url?: string; // not used by the frontend, we build the url ourselves + ens_info?: { + address_hash: string; + expiry_date?: string; + name: string; + names_count: number; + }; +} + +export interface SearchResultDomain { + type: 'ens_domain'; + name: string | null; + address: string; + is_smart_contract_verified: boolean; + url?: string; // not used by the frontend, we build the url ourselves + ens_info: { + address_hash: string; + expiry_date?: string; + name: string; + names_count: number; + }; +} + +export interface SearchResultLabel { + type: 'label'; + address: string; + name: string; + is_smart_contract_verified: boolean; + url?: string; // not used by the frontend, we build the url ourselves +} + +export interface SearchResultBlock { + type: 'block'; + block_type?: 'block' | 'reorg' | 'uncle'; + block_number: number | string; + block_hash: string; + timestamp: string; + url?: string; // not used by the frontend, we build the url ourselves +} + +export interface SearchResultTx { + type: 'transaction'; + tx_hash: string; + timestamp: string; + url?: string; // not used by the frontend, we build the url ourselves +} + +export interface SearchResultBlob { + type: 'blob'; + blob_hash: string; + timestamp: null; +} + +export interface SearchResultUserOp { + type: 'user_operation'; + user_operation_hash: string; + timestamp: string; + url?: string; // not used by the frontend, we build the url ourselves +} + +export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp | +SearchResultBlob | SearchResultDomain; + +export interface SearchResult { + items: Array; + next_page_params: { + 'address_hash': string | null; + 'block_hash': string | null; + 'holder_count': number | null; + 'inserted_at': string | null; + 'item_type': SearchResultType; + 'items_count': number; + 'name': string; + 'q': string; + 'tx_hash': string | null; + } | null; +} + +export interface SearchResultFilters { + q: string; +} + +export interface SearchRedirectResult { + parameter: string | null; + redirect: boolean; + type: 'address' | 'block' | 'transaction' | 'user_operation' | 'blob' | null; +} diff --git a/types/api/shibarium.ts b/types/api/shibarium.ts new file mode 100644 index 0000000000..e23d9cf811 --- /dev/null +++ b/types/api/shibarium.ts @@ -0,0 +1,33 @@ +import type { AddressParam } from './addressParams'; + +export type ShibariumDepositsItem = { + l1_block_number: number; + l1_transaction_hash: string; + l2_transaction_hash: string; + timestamp: string; + user: AddressParam | string; +} + +export type ShibariumDepositsResponse = { + items: Array; + next_page_params: { + items_count: number; + block_number: number; + }; +} + +export type ShibariumWithdrawalsItem = { + l1_transaction_hash: string; + l2_block_number: number; + l2_transaction_hash: string; + timestamp: string; + user: AddressParam | string; +} + +export type ShibariumWithdrawalsResponse = { + items: Array; + next_page_params: { + items_count: number; + block_number: number; + }; +} diff --git a/types/api/stats.ts b/types/api/stats.ts new file mode 100644 index 0000000000..3d75a5f3e9 --- /dev/null +++ b/types/api/stats.ts @@ -0,0 +1,36 @@ +export type HomeStats = { + total_blocks: string; + total_addresses: string; + total_transactions: string; + average_block_time: number; + coin_image?: string | null; + coin_price: string | null; + coin_price_change_percentage: number | null; // e.g -6.22 + total_gas_used: string; + transactions_today: string | null; + gas_used_today: string; + gas_prices: GasPrices | null; + gas_price_updated_at: string | null; + gas_prices_update_in: number; + static_gas_price: string | null; + market_cap: string | null; + network_utilization_percentage: number; + tvl: string | null; + rootstock_locked_btc?: string | null; + last_output_root_size?: string | null; + secondary_coin_price?: string | null; +} + +export type GasPrices = { + average: GasPriceInfo | null; + fast: GasPriceInfo | null; + slow: GasPriceInfo | null; +} + +export interface GasPriceInfo { + fiat_price: string | null; + price: number | null; + time: number | null; + base_fee: number | null; + priority_fee: number | null; +} diff --git a/types/api/token.ts b/types/api/token.ts new file mode 100644 index 0000000000..58d4e484fb --- /dev/null +++ b/types/api/token.ts @@ -0,0 +1,86 @@ +import type { TokenInfoApplication } from './account'; +import type { AddressParam } from './addressParams'; + +export type NFTTokenType = 'ERC-721' | 'ERC-1155' | 'ERC-404'; +export type TokenType = 'ERC-20' | NFTTokenType; + +export interface TokenInfo { + address: string; + type: T; + symbol: string | null; + name: string | null; + decimals: string | null; + holders: string | null; + exchange_rate: string | null; + total_supply: string | null; + icon_url: string | null; + circulating_market_cap: string | null; + // bridged token fields + is_bridged?: boolean | null; + bridge_type?: string | null; + origin_chain_id?: string | null; + foreign_address?: string | null; +} + +export interface TokenCounters { + token_holders_count: string; + transfers_count: string; +} + +export interface TokenHolders { + items: Array; + next_page_params: TokenHoldersPagination | null; +} + +export type TokenHolder = TokenHolderERC20ERC721 | TokenHolderERC1155; + +export type TokenHolderBase = { + address: AddressParam; + value: string; +} + +export type TokenHolderERC20ERC721 = TokenHolderBase + +export type TokenHolderERC1155 = TokenHolderBase & { + token_id: string; +} + +export type TokenHoldersPagination = { + items_count: number; + value: string; +} + +export interface TokenInstance { + is_unique: boolean; + id: string; + holder_address_hash: string | null; + image_url: string | null; + animation_url: string | null; + external_app_url: string | null; + metadata: Record | null; + owner: AddressParam | null; +} + +export interface TokenInstanceMetadataSocketMessage { + token_id: number; + fetched_metadata: TokenInstance['metadata']; +} + +export interface TokenInstanceTransfersCount { + transfers_count: number; +} + +export interface TokenInventoryResponse { + items: Array; + next_page_params: TokenInventoryPagination | null; +} + +export type TokenInventoryPagination = { + unique_token: number; +} + +export type TokenVerifiedInfo = Omit; + +export type TokenInventoryFilters = { + holder_address_hash?: string; +} diff --git a/types/api/tokenTransfer.ts b/types/api/tokenTransfer.ts new file mode 100644 index 0000000000..bb580516dd --- /dev/null +++ b/types/api/tokenTransfer.ts @@ -0,0 +1,72 @@ +import type { AddressParam } from './addressParams'; +import type { TokenInfo, TokenType } from './token'; + +export type Erc20TotalPayload = { + decimals: string | null; + value: string; +} + +export type Erc721TotalPayload = { + token_id: string | null; +} + +export type Erc1155TotalPayload = { + decimals: string | null; + value: string; + token_id: string | null; +} + +export type Erc404TotalPayload = { + decimals: string; + value: string; + token_id: null; +} | { + token_id: string; +}; + +export type TokenTransfer = ( + { + token: TokenInfo<'ERC-20'>; + total: Erc20TotalPayload; + } | + { + token: TokenInfo<'ERC-721'>; + total: Erc721TotalPayload; + } | + { + token: TokenInfo<'ERC-1155'>; + total: Erc1155TotalPayload; + } | + { + token: TokenInfo<'ERC-404'>; + total: Erc404TotalPayload; + } +) & TokenTransferBase + +export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload; + +interface TokenTransferBase { + type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting'; + tx_hash: string; + from: AddressParam; + to: AddressParam; + timestamp: string; + block_hash: string; + log_index: string; + method?: string; +} + +export type TokenTransferPagination = { + block_number: number; + index: number; + items_count: number; +} + +export interface TokenTransferResponse { + items: Array; + next_page_params: TokenTransferPagination | null; +} + +export interface TokenTransferFilters { + type: Array; +} diff --git a/types/api/tokens.ts b/types/api/tokens.ts new file mode 100644 index 0000000000..6538e690ec --- /dev/null +++ b/types/api/tokens.ts @@ -0,0 +1,37 @@ +import type { TokenInfo, TokenType } from './token'; +import type { TokenTransfer } from './tokenTransfer'; + +export type TokensResponse = { + items: Array; + next_page_params: { + holder_count: number; + items_count: number; + name: string; + market_cap: string | null; + } | null; +} + +export type TokensFilters = { q: string; type: Array | undefined }; + +export type TokensBridgedFilters = { q: string; chain_ids: Array | undefined }; + +export interface TokenInstanceTransferResponse { + items: Array; + next_page_params: TokenInstanceTransferPagination | null; +} + +export interface TokenInstanceTransferPagination { + block_number: number; + index: number; + items_count: number; + token_id: string; +} + +export interface TokensSorting { + sort: 'fiat_value' | 'holder_count' | 'circulating_market_cap'; + order: 'asc' | 'desc'; +} + +export type TokensSortingField = TokensSorting['sort']; + +export type TokensSortingValue = `${ TokensSortingField }-${ TokensSorting['order'] }`; diff --git a/types/api/transaction.ts b/types/api/transaction.ts new file mode 100644 index 0000000000..fee1043bfa --- /dev/null +++ b/types/api/transaction.ts @@ -0,0 +1,182 @@ +import type { AddressParam } from './addressParams'; +import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2'; +import type { BlockTransactionsResponse } from './block'; +import type { DecodedInput } from './decodedInput'; +import type { Fee } from './fee'; +import type { NovesTxTranslation } from './noves'; +import type { OptimisticL2WithdrawalStatus } from './optimisticL2'; +import type { TokenInfo } from './token'; +import type { TokenTransfer } from './tokenTransfer'; +import type { TxAction } from './txAction'; +import type { ZkSyncBatchesItem } from './zkSyncL2'; + +export type TransactionRevertReason = { + raw: string; +} | DecodedInput; + +type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_price' | 'hash' | 'max_fee_per_gas' | +'max_priority_fee_per_gas' | 'method' | 'nonce' | 'raw_input' | 'to' | 'type' | 'value'; + +export interface OpWithdrawal { + l1_transaction_hash: string; + nonce: number; + status: OptimisticL2WithdrawalStatus; +} + +export type Transaction = { + to: AddressParam | null; + created_contract: AddressParam | null; + hash: string; + result: string; + confirmations: number; + status: 'ok' | 'error' | null | undefined; + block: number | null; + timestamp: string | null; + confirmation_duration: Array | null; + from: AddressParam; + value: string; + fee: Fee; + gas_price: string | null; + type: number | null; + gas_used: string | null; + gas_limit: string; + max_fee_per_gas: string | null; + max_priority_fee_per_gas: string | null; + priority_fee: string | null; + base_fee_per_gas: string | null; + tx_burnt_fee: string | null; + nonce: number; + position: number | null; + revert_reason: TransactionRevertReason | null; + raw_input: string; + decoded_input: DecodedInput | null; + token_transfers: Array | null; + token_transfers_overflow: boolean; + exchange_rate: string | null; + method: string | null; + tx_types: Array; + tx_tag: string | null; + actions: Array; + l1_fee?: string; + l1_fee_scalar?: string; + l1_gas_price?: string; + l1_gas_used?: string; + has_error_in_internal_txs: boolean | null; + // optimism fields + op_withdrawals?: Array; + // SUAVE fields + execution_node?: AddressParam | null; + allowed_peekers?: Array; + wrapped?: Pick; + // Stability fields + stability_fee?: { + dapp_address: AddressParam; + dapp_fee: string; + token: TokenInfo; + total_fee: string; + validator_address: AddressParam; + validator_fee: string; + }; + // Celo fields + celo?: { + gas_token: TokenInfo<'ERC-20'> | null; + }; + // zkEvm fields + zkevm_verify_hash?: string; + zkevm_batch_number?: number; + zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number]; + zkevm_sequence_hash?: string; + // zkSync FIELDS + zksync?: Omit & { + 'batch_number': number | null; + }; + // blob tx fields + blob_versioned_hashes?: Array; + blob_gas_used?: string; + blob_gas_price?: string; + burnt_blob_fee?: string; + max_fee_per_blob_gas?: string; + // Noves-fi + translation?: NovesTxTranslation; + arbitrum?: ArbitrumTransactionData; +} + +type ArbitrumTransactionData = { + batch_number: number; + commitment_transaction: ArbitrumL2TxData; + confirmation_transaction: ArbitrumL2TxData; + contains_message: 'incoming' | 'outcoming' | null; + gas_used_for_l1: string; + gas_used_for_l2: string; + network_fee: string; + poster_fee: string; + status: ArbitrumBatchStatus; +} + +export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ]; + +export interface TransactionsStats { + pending_transactions_count: string; + transaction_fees_avg_24h: string; + transaction_fees_sum_24h: string; + transactions_count_24h: string; +} + +export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; + +export interface TransactionsResponseValidated { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + filter: 'validated'; + } | null; +} + +export interface TransactionsResponsePending { + items: Array; + next_page_params: { + inserted_at: string; + hash: string; + filter: 'pending'; + } | null; +} + +export interface TransactionsResponseWithBlobs { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: number; + } | null; +} + +export interface TransactionsResponseWatchlist { + items: Array; + next_page_params: { + block_number: number; + index: number; + items_count: 50; + } | null; +} + +export type TransactionType = 'rootstock_remasc' | +'rootstock_bridge' | +'token_transfer' | +'contract_creation' | +'contract_call' | +'token_creation' | +'coin_transfer' | +'blob_transaction' + +export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse; + +export interface TransactionsSorting { + sort: 'value' | 'fee'; + order: 'asc' | 'desc'; +} + +export type TransactionsSortingField = TransactionsSorting['sort']; + +export type TransactionsSortingValue = `${ TransactionsSortingField }-${ TransactionsSorting['order'] }`; diff --git a/types/api/txAction.ts b/types/api/txAction.ts new file mode 100644 index 0000000000..e56c0ccf2a --- /dev/null +++ b/types/api/txAction.ts @@ -0,0 +1,26 @@ +export interface TxActionGeneral { + type: 'mint' | 'burn' | 'collect' | 'swap'; + data: { + amount0: string; + symbol0: string; + address0: string; + amount1: string; + symbol1: string; + address1: string; + }; +} + +export interface TxActionNft { + type: 'mint_nft'; + data: { + name: string; + symbol: string; + address: string; + to: string; + ids: Array; + }; +} + +export type TxAction = { + protocol: 'uniswap_v3'; +} & (TxActionGeneral | TxActionNft) diff --git a/types/api/txInterpretation.ts b/types/api/txInterpretation.ts new file mode 100644 index 0000000000..87ef1d0dae --- /dev/null +++ b/types/api/txInterpretation.ts @@ -0,0 +1,59 @@ +import type { AddressParam } from 'types/api/addressParams'; +import type { TokenInfo } from 'types/api/token'; + +export interface TxInterpretationResponse { + data: { + summaries: Array; + }; +} + +export type TxInterpretationSummary = { + summary_template: string; + summary_template_variables: Record; +} + +export type TxInterpretationVariable = + TxInterpretationVariableString | + TxInterpretationVariableCurrency | + TxInterpretationVariableTimestamp | + TxInterpretationVariableToken | + TxInterpretationVariableAddress | + TxInterpretationVariableDomain | + TxInterpretationVariableMethod; + +export type TxInterpretationVariableType = 'string' | 'currency' | 'timestamp' | 'token' | 'address' | 'domain' | 'method'; + +export type TxInterpretationVariableString = { + type: 'string'; + value: string; +} + +export type TxInterpretationVariableCurrency = { + type: 'currency'; + value: string; +} + +export type TxInterpretationVariableTimestamp = { + type: 'timestamp'; + value: string; +} + +export type TxInterpretationVariableToken = { + type: 'token'; + value: TokenInfo; +} + +export type TxInterpretationVariableAddress = { + type: 'address'; + value: AddressParam; +} + +export type TxInterpretationVariableDomain = { + type: 'domain'; + value: string; +} + +export type TxInterpretationVariableMethod = { + type: 'method'; + value: string; +} diff --git a/types/api/txStateChanges.ts b/types/api/txStateChanges.ts new file mode 100644 index 0000000000..284a921699 --- /dev/null +++ b/types/api/txStateChanges.ts @@ -0,0 +1,57 @@ +import type { AddressParam } from './addressParams'; +import type { TokenInfo } from './token'; +import type { Erc721TotalPayload } from './tokenTransfer'; + +export type TxStateChange = (TxStateChangeCoin | TxStateChangeToken) & { + address: AddressParam; + is_miner: boolean; + balance_before: string | null; + balance_after: string | null; +} + +export interface TxStateChangeCoin { + type: 'coin'; + change: string; + token: null; +} + +export type TxStateChangeToken = TxStateChangeTokenErc20 | TxStateChangeTokenErc721 | TxStateChangeTokenErc1155; + +type ChangeDirection = 'from' | 'to'; + +export interface TxStateChangeTokenErc20 { + type: 'token'; + token: TokenInfo<'ERC-20'>; + change: string; +} + +export interface TxStateChangeTokenErc721 { + type: 'token'; + token: TokenInfo<'ERC-721'>; + change: Array<{ + direction: ChangeDirection; + total: Erc721TotalPayload; + }>; +} + +export interface TxStateChangeTokenErc1155 { + type: 'token'; + token: TokenInfo<'ERC-1155'>; + change: string; + token_id: string; +} + +export interface TxStateChangeTokenErc404 { + type: 'token'; + token: TokenInfo<'ERC-404'>; + change: string; + token_id: string; +} + +export type TxStateChanges = { + items: Array; + next_page_params: { + items_count: number; + state_changes: null; + }; +}; diff --git a/types/api/txsFilters.ts b/types/api/txsFilters.ts new file mode 100644 index 0000000000..17347c9cdc --- /dev/null +++ b/types/api/txsFilters.ts @@ -0,0 +1,13 @@ +export type TTxsFilters = { + filter: 'pending' | 'validated'; + type?: Array; + method?: Array; +} + +export type TTxsWithBlobsFilters = { + type: 'blob_transaction'; +} + +export type TypeFilter = 'token_transfer' | 'contract_creation' | 'contract_call' | 'coin_transfer' | 'token_creation' | 'blob_transaction'; + +export type MethodFilter = 'approve' | 'transfer' | 'multicall' | 'mint' | 'commit'; diff --git a/types/api/userOps.ts b/types/api/userOps.ts new file mode 100644 index 0000000000..2f7dcfdc26 --- /dev/null +++ b/types/api/userOps.ts @@ -0,0 +1,81 @@ +import type { AddressParamBasic } from './addressParams'; +import type { DecodedInput } from './decodedInput'; + +export type UserOpsItem = { + hash: string; + block_number: string; + transaction_hash: string; + address: string | AddressParamBasic; + timestamp: string; + status: boolean; + fee: string; +} + +export type UserOpsResponse = { + items: Array; + next_page_params: { + page_token: string; + page_size: number; + } | null; +} + +export type UserOpSponsorType = 'paymaster_hybrid' | 'paymaster_sponsor' | 'wallet_balance' | 'wallet_deposit'; + +export type UserOp = { + hash: string; + sender: string | AddressParamBasic; + status: boolean; + revert_reason: string | null; + timestamp: string | null; + fee: string; + gas: string; + transaction_hash: string; + block_number: string; + block_hash: string; + entry_point: string | AddressParamBasic; + call_gas_limit: string; + verification_gas_limit: string; + pre_verification_gas: string; + max_fee_per_gas: string; + max_priority_fee_per_gas: string; + aggregator: string | null; + aggregator_signature: string | null; + bundler: string | AddressParamBasic; + factory: string | null; + paymaster: string | AddressParamBasic | null; + sponsor_type: UserOpSponsorType; + signature: string; + nonce: string; + call_data: string; + decoded_call_data: DecodedInput | null; + execute_call_data: string | null; + decoded_execute_call_data: DecodedInput | null; + user_logs_start_index: number; + user_logs_count: number; + raw: { + account_gas_limits?: string; + call_data: string; + call_gas_limit: string; + gas_fees?: string; + init_code: string; + max_fee_per_gas: string; + max_priority_fee_per_gas: string; + nonce: string; + paymaster_and_data: string; + pre_verification_gas: string; + sender: string; + signature: string; + verification_gas_limit: string; + }; + gas_price: string; + gas_used: string; +} + +export type UserOpsFilters = { + transaction_hash?: string; + sender?: string; +} + +export type UserOpsAccount = { + total_ops: number; +} diff --git a/types/api/validators.ts b/types/api/validators.ts new file mode 100644 index 0000000000..ad6e33de96 --- /dev/null +++ b/types/api/validators.ts @@ -0,0 +1,38 @@ +import type { AddressParam } from './addressParams'; + +export interface Validator { + address: AddressParam; + blocks_validated_count: number; + state: 'active' | 'probation' | 'inactive'; +} + +export interface ValidatorsResponse { + items: Array; + next_page_params: { + 'address_hash': string; + 'blocks_validated': string; + 'items_count': string; + 'state': Validator['state']; + } | null; +} + +export interface ValidatorsCountersResponse { + active_validators_counter: string; + active_validators_percentage: number; + new_validators_counter_24h: string; + validators_counter: string; +} + +export interface ValidatorsFilters { + // address_hash: string | undefined; // right now API doesn't support filtering by address_hash + state_filter: Validator['state'] | undefined; +} + +export interface ValidatorsSorting { + sort: 'state' | 'blocks_validated'; + order: 'asc' | 'desc'; +} + +export type ValidatorsSortingField = ValidatorsSorting['sort']; + +export type ValidatorsSortingValue = `${ ValidatorsSortingField }-${ ValidatorsSorting['order'] }`; diff --git a/types/api/verifiedContracts.ts b/types/api/verifiedContracts.ts new file mode 100644 index 0000000000..12d45f31fc --- /dev/null +++ b/types/api/verifiedContracts.ts @@ -0,0 +1,8 @@ +export interface VerifiedContractsSorting { + sort: 'balance' | 'txs_count'; + order: 'asc' | 'desc'; +} + +export type VerifiedContractsSortingField = VerifiedContractsSorting['sort']; + +export type VerifiedContractsSortingValue = `${ VerifiedContractsSortingField }-${ VerifiedContractsSorting['order'] }`; diff --git a/types/api/withdrawals.ts b/types/api/withdrawals.ts new file mode 100644 index 0000000000..f9f280b442 --- /dev/null +++ b/types/api/withdrawals.ts @@ -0,0 +1,23 @@ +import type { AddressParam } from './addressParams'; + +export type WithdrawalsResponse = { + items: Array; + next_page_params: { + index: number; + items_count: number; + }; +} + +export type WithdrawalsItem = { + amount: string; + block_number: number; + index: number; + receiver: AddressParam; + timestamp: string; + validator_index: number; +} + +export type WithdrawalsCounters = { + withdrawal_count: string; + withdrawal_sum: string; +} diff --git a/types/api/zkEvmL2.ts b/types/api/zkEvmL2.ts new file mode 100644 index 0000000000..04ebc265c0 --- /dev/null +++ b/types/api/zkEvmL2.ts @@ -0,0 +1,76 @@ +import type { Transaction } from './transaction'; + +export type ZkEvmL2DepositsItem = { + block_number: number; + index: number; + l1_transaction_hash: string; + l2_transaction_hash: string | null; + timestamp: string; + value: string; + symbol: string; +} + +export type ZkEvmL2DepositsResponse = { + items: Array; + next_page_params: { + items_count: number; + index: number; + }; +} + +export type ZkEvmL2WithdrawalsItem = { + block_number: number; + index: number; + l1_transaction_hash: string | null; + l2_transaction_hash: string; + timestamp: string; + value: string; + symbol: string; +} + +export type ZkEvmL2WithdrawalsResponse = { + items: Array; + next_page_params: { + items_count: number; + index: number; + }; +} + +export type ZkEvmL2TxnBatchesItem = { + number: number; + verify_tx_hash: string | null; + sequence_tx_hash: string | null; + status: string; + timestamp: string | null; + tx_count: number; +} + +export type ZkEvmL2TxnBatchesResponse = { + items: Array; + next_page_params: { + number: number; + items_count: number; + } | null; +} + +export const ZKEVM_L2_TX_BATCH_STATUSES = [ 'Unfinalized', 'L1 Sequence Confirmed', 'Finalized' ]; + +export type ZkEvmL2TxnBatch = { + acc_input_hash: string; + global_exit_root: string; + number: number; + sequence_tx_hash: string; + state_root: string; + status: typeof ZKEVM_L2_TX_BATCH_STATUSES[number]; + timestamp: string | null; + transactions: Array; + verify_tx_hash: string; +} + +export type ZkEvmL2TxnBatchTxs = { + items: Array; + // API responce doesn't have next_page_params option, but we need to add it to the type for consistency + next_page_params: null; +} + +export type NewZkEvmBatchSocketResponse = { batch: ZkEvmL2TxnBatchesItem }; diff --git a/types/api/zkSyncL2.ts b/types/api/zkSyncL2.ts new file mode 100644 index 0000000000..4d038477ff --- /dev/null +++ b/types/api/zkSyncL2.ts @@ -0,0 +1,52 @@ +import type { Transaction } from './transaction'; + +export const ZKSYNC_L2_TX_BATCH_STATUSES = [ + 'Processed on L2' as const, + 'Sealed on L2' as const, + 'Sent to L1' as const, + 'Validated on L1' as const, + 'Executed on L1' as const, +]; + +export type ZkSyncBatchStatus = typeof ZKSYNC_L2_TX_BATCH_STATUSES[number]; + +export interface ZkSyncBatchesItem { + commit_transaction_hash: string | null; + commit_transaction_timestamp: string | null; + execute_transaction_hash: string | null; + execute_transaction_timestamp: string | null; + number: number; + prove_transaction_hash: string | null; + prove_transaction_timestamp: string | null; + status: ZkSyncBatchStatus; + timestamp: string; + tx_count: number; +} + +export type ZkSyncBatchesResponse = { + items: Array; + next_page_params: { + number: number; + items_count: number; + } | null; +} + +export interface ZkSyncBatch extends Omit { + start_block: number; + end_block: number; + l1_gas_price: string; + l1_tx_count: number; + l2_fair_gas_price: string; + l2_tx_count: number; + root_hash: string; +} + +export type ZkSyncBatchTxs = { + items: Array; + next_page_params: { + batch_number: string; + block_number: number; + index: number; + items_count: number; + } | null; +} diff --git a/types/client/account.ts b/types/client/account.ts new file mode 100644 index 0000000000..ebeefc95c7 --- /dev/null +++ b/types/client/account.ts @@ -0,0 +1,3 @@ +export interface CsrfData { + token: string; +} diff --git a/types/client/adButlerConfig.ts b/types/client/adButlerConfig.ts new file mode 100644 index 0000000000..39dcb385b7 --- /dev/null +++ b/types/client/adButlerConfig.ts @@ -0,0 +1,5 @@ +export type AdButlerConfig = { + id: string; + width: string; + height: string; +} diff --git a/types/client/adProviders.ts b/types/client/adProviders.ts new file mode 100644 index 0000000000..df3962b9ec --- /dev/null +++ b/types/client/adProviders.ts @@ -0,0 +1,17 @@ +import type { ArrayElement } from 'types/utils'; + +export const SUPPORTED_AD_BANNER_PROVIDERS = [ + 'slise', + 'adbutler', + 'coinzilla', + 'hype', + // 'getit', // temporary disabled + 'none', +] as const; +export type AdBannerProviders = ArrayElement; + +export const SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS = [ 'adbutler' ] as const; +export type AdBannerAdditionalProviders = ArrayElement; + +export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const; +export type AdTextProviders = ArrayElement; diff --git a/types/client/address.ts b/types/client/address.ts new file mode 100644 index 0000000000..e7a181ee3b --- /dev/null +++ b/types/client/address.ts @@ -0,0 +1,15 @@ +import type { AddressFromToFilter } from 'types/api/address'; + +export type CsvExportParams = { + type: 'transactions' | 'internal-transactions' | 'token-transfers'; + filterType?: 'address'; + filterValue?: AddressFromToFilter; +} | { + type: 'logs'; + filterType?: 'topic'; + filterValue?: string; +} | { + type: 'holders'; + filterType?: undefined; + filterValue?: undefined; +} diff --git a/types/client/addressMetadata.ts b/types/client/addressMetadata.ts new file mode 100644 index 0000000000..8281e1cf1b --- /dev/null +++ b/types/client/addressMetadata.ts @@ -0,0 +1,10 @@ +import type { AddressMetadataTagApi } from 'types/api/addressMetadata'; + +export interface AddressMetadataInfoFormatted { + addresses: Record; + reputation: number | null; + }>; +} + +export type AddressMetadataTagFormatted = AddressMetadataTagApi; diff --git a/types/client/contract.ts b/types/client/contract.ts new file mode 100644 index 0000000000..66fca3531c --- /dev/null +++ b/types/client/contract.ts @@ -0,0 +1,27 @@ +import type { SmartContractLicenseType, SmartContractVerificationConfigRaw, SmartContractVerificationMethodApi } from 'types/api/contract'; + +export interface ContractCodeIde { + title: string; + url: string; + icon_url: string; +} + +export interface ContractLicense { + type: SmartContractLicenseType; + url: string; + label: string; + title: string; +} + +export const SMART_CONTRACT_EXTRA_VERIFICATION_METHODS = [ + 'solidity-hardhat' as const, + 'solidity-foundry' as const, +]; + +export type SmartContractVerificationMethodExtra = (typeof SMART_CONTRACT_EXTRA_VERIFICATION_METHODS)[number]; + +export type SmartContractVerificationMethod = SmartContractVerificationMethodApi | SmartContractVerificationMethodExtra; + +export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { + verification_options: Array; +} diff --git a/types/client/deFiDropdown.ts b/types/client/deFiDropdown.ts new file mode 100644 index 0000000000..2d087f5a1b --- /dev/null +++ b/types/client/deFiDropdown.ts @@ -0,0 +1,9 @@ +import type { IconName } from 'ui/shared/IconSvg'; + +export type DeFiDropdownItem = { + text: string; + icon: IconName; +} & ( + { dappId: string; url?: never } | + { url: string; dappId?: never } +); diff --git a/types/client/gasRefuelProviderConfig.ts b/types/client/gasRefuelProviderConfig.ts new file mode 100644 index 0000000000..d135952480 --- /dev/null +++ b/types/client/gasRefuelProviderConfig.ts @@ -0,0 +1,6 @@ +export type GasRefuelProviderConfig = { + name: string; + dapp_id?: string; + url_template: string; + logo?: string; +}; diff --git a/types/client/gasTracker.ts b/types/client/gasTracker.ts new file mode 100644 index 0000000000..f998202e95 --- /dev/null +++ b/types/client/gasTracker.ts @@ -0,0 +1,6 @@ +export const GAS_UNITS = [ + 'usd', + 'gwei', +] as const; + +export type GasUnit = typeof GAS_UNITS[number]; diff --git a/types/client/marketplace.ts b/types/client/marketplace.ts new file mode 100644 index 0000000000..9b34c46206 --- /dev/null +++ b/types/client/marketplace.ts @@ -0,0 +1,71 @@ +import type { SolidityscanReport } from 'types/api/contract'; + +export type MarketplaceAppPreview = { + id: string; + external?: boolean; + title: string; + logo: string; + logoDarkMode?: string; + shortDescription: string; + categories: Array; + url: string; + internalWallet?: boolean; + priority?: number; +} + +export type MarketplaceAppSocialInfo = { + twitter?: string; + telegram?: string; + github?: string | Array; + discord?: string; +} + +export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocialInfo & { + author: string; + description: string; + site?: string; +} + +export type AppRating = { + recordId: string; + value: number | undefined; +} + +export type MarketplaceAppWithSecurityReport = MarketplaceAppOverview & { + securityReport?: MarketplaceAppSecurityReport; + rating?: AppRating; +} + +export enum MarketplaceCategory { + ALL = 'All', + FAVORITES = 'Favorites', +} + +export enum ContractListTypes { + ANALYZED = 'Analyzed', + ALL = 'All', + VERIFIED = 'Verified', +} + +export type MarketplaceAppSecurityReport = { + overallInfo: { + verifiedNumber: number; + totalContractsNumber: number; + solidityScanContractsNumber: number; + securityScore: number; + totalIssues?: number; + issueSeverityDistribution: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution']; + }; + contractsData: Array<{ + address: string; + isVerified: boolean; + solidityScanReport?: SolidityscanReport['scan_report'] | null; + }>; +} + +export type MarketplaceAppSecurityReportRaw = { + appName: string; + chainsData: { + [chainId: string]: MarketplaceAppSecurityReport; + }; +} diff --git a/types/client/multichainProviderConfig.ts b/types/client/multichainProviderConfig.ts new file mode 100644 index 0000000000..fbdced386f --- /dev/null +++ b/types/client/multichainProviderConfig.ts @@ -0,0 +1,6 @@ +export type MultichainProviderConfig = { + name: string; + dapp_id?: string; + url_template: string; + logo?: string; +}; diff --git a/types/client/navigation.ts b/types/client/navigation.ts new file mode 100644 index 0000000000..8d9881eb8a --- /dev/null +++ b/types/client/navigation.ts @@ -0,0 +1,39 @@ +import type React from 'react'; + +import type { Route } from 'nextjs-routes'; + +import type { IconName } from 'ui/shared/IconSvg'; + +type NavIconOrComponent = { + icon?: IconName; +} | { + iconComponent?: React.FC<{ size?: number; className?: string }>; +}; + +type NavItemCommon = { + text: string; +} & NavIconOrComponent; + +export type NavItemInternal = NavItemCommon & { + nextRoute: Route; + isActive?: boolean; +} + +export type NavItemExternal = { + text: string; + url: string; +} + +export type NavItem = NavItemInternal | NavItemExternal + +export type NavGroupItem = NavItemCommon & { + isActive?: boolean; + subItems: Array | Array>; +} + +import type { ArrayElement } from '../utils'; + +export const NAVIGATION_LINK_IDS = [ 'rpc_api', 'eth_rpc_api' ] as const; +export type NavigationLinkId = ArrayElement; + +export type NavigationLayout = 'vertical' | 'horizontal'; diff --git a/types/client/rollup.ts b/types/client/rollup.ts new file mode 100644 index 0000000000..a2775e289b --- /dev/null +++ b/types/client/rollup.ts @@ -0,0 +1,11 @@ +import type { ArrayElement } from 'types/utils'; + +export const ROLLUP_TYPES = [ + 'optimistic', + 'arbitrum', + 'shibarium', + 'zkEvm', + 'zkSync', +] as const; + +export type RollupType = ArrayElement; diff --git a/types/client/search.ts b/types/client/search.ts new file mode 100644 index 0000000000..ca02af1e3e --- /dev/null +++ b/types/client/search.ts @@ -0,0 +1,14 @@ +import type * as api from 'types/api/search'; + +export interface SearchResultFutureBlock { + type: 'block'; + block_type: 'block'; + block_number: number | string; + block_hash: string; + timestamp: undefined; + url?: string; // not used by the frontend, we build the url ourselves +} + +export type SearchResultBlock = api.SearchResultBlock | SearchResultFutureBlock; + +export type SearchResultItem = api.SearchResultItem | SearchResultBlock; diff --git a/types/client/stats.ts b/types/client/stats.ts new file mode 100644 index 0000000000..fb7ee36e99 --- /dev/null +++ b/types/client/stats.ts @@ -0,0 +1,9 @@ +export type StatsInterval = { id: StatsIntervalIds; title: string } +export type StatsIntervalIds = keyof typeof StatsIntervalId; +export enum StatsIntervalId { + 'all', + 'oneMonth', + 'threeMonths', + 'sixMonths', + 'oneYear', +} diff --git a/types/client/token.ts b/types/client/token.ts new file mode 100644 index 0000000000..06aaaa47b2 --- /dev/null +++ b/types/client/token.ts @@ -0,0 +1,24 @@ +export interface Metadata { + name?: string; + description?: string; + attributes?: Array; +} + +export interface MetadataAttributes { + value: string; + trait_type: string; + value_type?: 'URL'; +} + +export interface BridgedTokenChain { + id: string; + title: string; + short_title: string; + base_url: string; +} + +export interface TokenBridge { + type: string; + title: string; + short_title: string; +} diff --git a/types/client/txInterpretation.ts b/types/client/txInterpretation.ts new file mode 100644 index 0000000000..23f55ed217 --- /dev/null +++ b/types/client/txInterpretation.ts @@ -0,0 +1,9 @@ +import type { ArrayElement } from 'types/utils'; + +export const PROVIDERS = [ + 'blockscout', + 'noves', + 'none', +] as const; + +export type Provider = ArrayElement; diff --git a/types/client/validators.ts b/types/client/validators.ts new file mode 100644 index 0000000000..9fdc949d5d --- /dev/null +++ b/types/client/validators.ts @@ -0,0 +1,7 @@ +import type { ArrayElement } from 'types/utils'; + +export const VALIDATORS_CHAIN_TYPE = [ + 'stability', +] as const; + +export type ValidatorsChainType = ArrayElement; diff --git a/types/client/wallets.ts b/types/client/wallets.ts new file mode 100644 index 0000000000..a112cd766f --- /dev/null +++ b/types/client/wallets.ts @@ -0,0 +1,16 @@ +import type { ArrayElement } from 'types/utils'; + +import type { IconName } from 'ui/shared/IconSvg'; + +export const SUPPORTED_WALLETS = [ + 'metamask', + 'coinbase', + 'token_pocket', +] as const; + +export type WalletType = ArrayElement; + +export interface WalletInfo { + name: string; + icon: IconName; +} diff --git a/types/footerLinks.ts b/types/footerLinks.ts new file mode 100644 index 0000000000..7a86b91f1b --- /dev/null +++ b/types/footerLinks.ts @@ -0,0 +1,9 @@ +export type CustomLink = { + text: string; + url: string; +} + +export type CustomLinksGroup = { + title: string; + links: Array; +} diff --git a/types/homepage.ts b/types/homepage.ts new file mode 100644 index 0000000000..2492134e97 --- /dev/null +++ b/types/homepage.ts @@ -0,0 +1,2 @@ +export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; +export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number]; diff --git a/types/networks.ts b/types/networks.ts new file mode 100644 index 0000000000..159f506634 --- /dev/null +++ b/types/networks.ts @@ -0,0 +1,27 @@ +import type { ArrayElement } from 'types/utils'; + +export const NETWORK_GROUPS = [ 'Mainnets', 'Testnets', 'Other' ] as const; +export type NetworkGroup = ArrayElement; + +export interface FeaturedNetwork { + title: string; + url: string; + group: NetworkGroup; + icon?: string; + isActive?: boolean; + invertIconInDarkMode?: boolean; +} + +export interface NetworkExplorer { + logo?: string; + title: string; + baseUrl: string; + paths: { + tx?: string; + address?: string; + token?: string; + block?: string; + }; +} + +export type NetworkVerificationType = 'mining' | 'validation'; diff --git a/types/settings.ts b/types/settings.ts new file mode 100644 index 0000000000..992205c4de --- /dev/null +++ b/types/settings.ts @@ -0,0 +1,2 @@ +export const COLOR_THEME_IDS = [ 'light', 'dim', 'midnight', 'dark' ] as const; +export type ColorThemeId = typeof COLOR_THEME_IDS[number]; diff --git a/types/unit.ts b/types/unit.ts new file mode 100644 index 0000000000..7faa4639c3 --- /dev/null +++ b/types/unit.ts @@ -0,0 +1 @@ +export type Unit = 'wei' | 'gwei' | 'ether'; diff --git a/types/utils.ts b/types/utils.ts new file mode 100644 index 0000000000..6cb7a82094 --- /dev/null +++ b/types/utils.ts @@ -0,0 +1,19 @@ +export type ArrayElement = ArrType extends ReadonlyArray + ? ElementType + : never; + +export type ExcludeNull = T extends null ? never : T; + +export type ExcludeUndefined = T extends undefined ? never : T; + +export type KeysOfObjectOrNull = keyof ExcludeNull; + +/** Combines members of an intersection into a readable type. */ +// https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg +export type Evaluate = { [key in keyof Type]: Type[key] } & unknown + +// Keeps in the object type only those properties that have the provided type (e.g only numbers) +export type PickByType = Record< +{[K in keyof T]: T[K] extends X ? K : never}[keyof T], +X +>; diff --git a/types/views/address.ts b/types/views/address.ts new file mode 100644 index 0000000000..271e661e02 --- /dev/null +++ b/types/views/address.ts @@ -0,0 +1,16 @@ +import type { ArrayElement } from 'types/utils'; + +export const IDENTICON_TYPES = [ + 'github', + 'jazzicon', + 'gradient_avatar', + 'blockie', +] as const; + +export type IdenticonType = ArrayElement; + +export const ADDRESS_VIEWS_IDS = [ + 'top_accounts', +] as const; + +export type AddressViewId = ArrayElement; diff --git a/types/views/block.ts b/types/views/block.ts new file mode 100644 index 0000000000..838dc523b7 --- /dev/null +++ b/types/views/block.ts @@ -0,0 +1,12 @@ +import type { ArrayElement } from 'types/utils'; + +export const BLOCK_FIELDS_IDS = [ + 'burnt_fees', + 'total_reward', + 'nonce', + 'miner', + 'L1_status', + 'batch', +] as const; + +export type BlockFieldId = ArrayElement; diff --git a/types/views/nft.ts b/types/views/nft.ts new file mode 100644 index 0000000000..6d1f00e302 --- /dev/null +++ b/types/views/nft.ts @@ -0,0 +1,6 @@ +export interface NftMarketplaceItem { + name: string; + collection_url: string; + instance_url: string; + logo_url: string; +} diff --git a/types/views/tx.ts b/types/views/tx.ts new file mode 100644 index 0000000000..d3a30adcc5 --- /dev/null +++ b/types/views/tx.ts @@ -0,0 +1,20 @@ +import type { ArrayElement } from 'types/utils'; + +export const TX_FIELDS_IDS = [ + 'value', + 'fee_currency', + 'gas_price', + 'tx_fee', + 'gas_fees', + 'burnt_fees', + 'L1_status', + 'batch', +] as const; + +export type TxFieldsId = ArrayElement; + +export const TX_ADDITIONAL_FIELDS_IDS = [ + 'fee_per_gas', +] as const; + +export type TxAdditionalFieldsId = ArrayElement; diff --git a/types/web3.ts b/types/web3.ts new file mode 100644 index 0000000000..1eab7a8cfa --- /dev/null +++ b/types/web3.ts @@ -0,0 +1,65 @@ +// copied from node_modules/@wagmi/core/src/connectors/injected.ts +import type { EIP1193Provider } from 'viem'; + +import type { Evaluate } from './utils'; + +type WalletProviderFlags = + | 'isApexWallet' + | 'isAvalanche' + | 'isBackpack' + | 'isBifrost' + | 'isBitKeep' + | 'isBitski' + | 'isBlockWallet' + | 'isBraveWallet' + | 'isCoinbaseWallet' + | 'isDawn' + | 'isEnkrypt' + | 'isExodus' + | 'isFrame' + | 'isFrontier' + | 'isGamestop' + | 'isHyperPay' + | 'isImToken' + | 'isKuCoinWallet' + | 'isMathWallet' + | 'isMetaMask' + | 'isOkxWallet' + | 'isOKExWallet' + | 'isOneInchAndroidWallet' + | 'isOneInchIOSWallet' + | 'isOpera' + | 'isPhantom' + | 'isPortal' + | 'isRabby' + | 'isRainbow' + | 'isStatus' + | 'isTally' + | 'isTokenPocket' + | 'isTokenary' + | 'isTrust' + | 'isTrustWallet' + | 'isXDEFI' + | 'isZerion' + +export type WalletProvider = Evaluate< +EIP1193Provider & { + [key in WalletProviderFlags]?: true | undefined +} & { + providers?: Array | undefined; + + /** Only exists in MetaMask as of 2022/04/03 */ + _events?: { connect?: (() => void) | undefined } | undefined; + + /** Only exists in MetaMask as of 2022/04/03 */ + _state?: + | { + accounts?: Array; + initialized?: boolean; + isConnected?: boolean; + isPermanentlyDisconnected?: boolean; + isUnlocked?: boolean; + } + | undefined; +} +> diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx new file mode 100644 index 0000000000..1165ee214d --- /dev/null +++ b/ui/address/AddressAccountHistory.tsx @@ -0,0 +1,134 @@ +import { Box, Hide, Show, Table, + Tbody, Th, Tr } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { NovesHistoryFilterValue } from 'types/api/noves'; +import { NovesHistoryFilterValues } from 'types/api/noves'; + +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; +import { generateListStub } from 'stubs/utils'; +import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAccountHistoryTableItem'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import { getFromToValue } from 'ui/shared/Noves/utils'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import TheadSticky from 'ui/shared/TheadSticky'; + +import AddressAccountHistoryListItem from './accountHistory/AddressAccountHistoryListItem'; +import AccountHistoryFilter from './AddressAccountHistoryFilter'; + +const getFilterValue = (getFilterValueFromQuery).bind(null, NovesHistoryFilterValues); + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressAccountHistory = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); + + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + + const { data, isError, pagination, isPlaceholderData } = useQueryWithPages({ + resourceName: 'noves_address_history', + pathParams: { address: currentAddress }, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'noves_address_history'>(NOVES_TRANSLATE, 10, { hasNextPage: false, pageNumber: 1, pageSize: 10 }), + }, + }); + + const handleFilterChange = React.useCallback((val: string | Array) => { + + const newVal = getFilterValue(val); + setFilterValue(newVal); + }, [ ]); + + if (!isMounted || !shouldRender) { + return null; + } + + const actionBar = ( + + + + + + ); + + const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? getFromToValue(i, currentAddress) === filterValue : i); + + const content = ( + + + { filteredData?.map((item, i) => ( + + )) } + + + + + + + + + + + + + { filteredData?.map((item, i) => ( + + )) } + +
+ Age + + Action + + From/To +
+
+
+ ); + + return ( + + ); +}; + +export default AddressAccountHistory; diff --git a/ui/address/AddressAccountHistoryFilter.tsx b/ui/address/AddressAccountHistoryFilter.tsx new file mode 100644 index 0000000000..b321051b27 --- /dev/null +++ b/ui/address/AddressAccountHistoryFilter.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import type { NovesHistoryFilterValue } from 'types/api/noves'; + +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio'; + +const OPTIONS = [ + { value: 'all', label: 'All' }, + { value: 'received', label: 'Received from' }, + { value: 'sent', label: 'Sent to' }, +]; + +interface Props { + hasActiveFilter: boolean; + defaultFilter: NovesHistoryFilterValue; + onFilterChange: (nextValue: string | Array) => void; + isLoading?: boolean; +} + +const AccountHistoryFilter = ({ onFilterChange, defaultFilter, hasActiveFilter, isLoading }: Props) => { + const isInitialLoading = useIsInitialLoading(isLoading); + + return ( + + ); +}; + +export default React.memo(AccountHistoryFilter); diff --git a/ui/address/AddressBlocksValidated.tsx b/ui/address/AddressBlocksValidated.tsx new file mode 100644 index 0000000000..18d68e00ff --- /dev/null +++ b/ui/address/AddressBlocksValidated.tsx @@ -0,0 +1,153 @@ +import { Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SocketMessage } from 'lib/socket/types'; +import type { AddressBlocksValidatedResponse } from 'types/api/address'; + +import config from 'configs/app'; +import { getResourceKey } from 'lib/api/useApiQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; +import { currencyUnits } from 'lib/units'; +import { BLOCK } from 'stubs/block'; +import { generateListStub } from 'stubs/utils'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import SocketAlert from 'ui/shared/SocketAlert'; +import { default as Thead } from 'ui/shared/TheadSticky'; + +import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem'; +import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem'; + +interface Props { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const [ socketAlert, setSocketAlert ] = React.useState(false); + const queryClient = useQueryClient(); + const router = useRouter(); + const isMounted = useIsMounted(); + + const addressHash = String(router.query.hash); + const query = useQueryWithPages({ + resourceName: 'address_blocks_validated', + pathParams: { hash: addressHash }, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'address_blocks_validated'>( + BLOCK, + 50, + { + next_page_params: { + block_number: 9060562, + items_count: 50, + }, + }, + ), + }, + }); + + const handleSocketError = React.useCallback(() => { + setSocketAlert(true); + }, []); + + const handleNewSocketMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { + setSocketAlert(false); + + queryClient.setQueryData( + getResourceKey('address_blocks_validated', { pathParams: { hash: addressHash } }), + (prevData: AddressBlocksValidatedResponse | undefined) => { + if (!prevData) { + return; + } + + return { + ...prevData, + items: [ payload.block, ...prevData.items ], + }; + }); + }, [ addressHash, queryClient ]); + + const channel = useSocketChannel({ + topic: `blocks:${ addressHash.toLowerCase() }`, + onSocketClose: handleSocketError, + onSocketError: handleSocketError, + isDisabled: !addressHash || query.isPlaceholderData || query.pagination.page !== 1, + }); + useSocketMessage({ + channel, + event: 'new_block', + handler: handleNewSocketMessage, + }); + + if (!isMounted || !shouldRender) { + return null; + } + + const content = query.data?.items ? ( + <> + { socketAlert && } + + + + + + + + + { !config.UI.views.block.hiddenFields?.total_reward && + } + + + + { query.data.items.map((item, index) => ( + + )) } + +
BlockAgeTxnGas usedReward { currencyUnits.ether }
+
+ + { query.data.items.map((item, index) => ( + + )) } + + + ) : null; + + const actionBar = query.pagination.isVisible ? ( + + + + ) : null; + + return ( + + ); +}; + +export default React.memo(AddressBlocksValidated); diff --git a/ui/address/AddressCoinBalance.pw.tsx b/ui/address/AddressCoinBalance.pw.tsx new file mode 100644 index 0000000000..3950798784 --- /dev/null +++ b/ui/address/AddressCoinBalance.pw.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory'; +import { test, expect } from 'playwright/lib'; + +import AddressCoinBalance from './AddressCoinBalance'; + +const addressHash = '0x1234'; +const hooksConfig = { + router: { + query: { hash: addressHash }, + }, +}; + +test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse }) => { + await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } }); + await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } }); + const component = await render(, { hooksConfig }); + await page.waitForFunction(() => { + return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1'; + }); + await page.mouse.move(100, 100); + await page.mouse.move(240, 100); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/address/AddressCoinBalance.tsx b/ui/address/AddressCoinBalance.tsx new file mode 100644 index 0000000000..ce5b5e9389 --- /dev/null +++ b/ui/address/AddressCoinBalance.tsx @@ -0,0 +1,104 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SocketMessage } from 'lib/socket/types'; +import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; + +import { getResourceKey } from 'lib/api/useApiQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; +import { ADDRESS_COIN_BALANCE } from 'stubs/address'; +import { generateListStub } from 'stubs/utils'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import SocketAlert from 'ui/shared/SocketAlert'; + +import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart'; +import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory'; + +type Props = { + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressCoinBalance = ({ shouldRender = true, isQueryEnabled = true }: Props) => { + const [ socketAlert, setSocketAlert ] = React.useState(false); + const queryClient = useQueryClient(); + const router = useRouter(); + const isMounted = useIsMounted(); + + const scrollRef = React.useRef(null); + + const addressHash = getQueryParamString(router.query.hash); + const coinBalanceQuery = useQueryWithPages({ + resourceName: 'address_coin_balance', + pathParams: { hash: addressHash }, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'address_coin_balance'>( + ADDRESS_COIN_BALANCE, + 50, + { + next_page_params: { + block_number: 8009880, + items_count: 50, + }, + }, + ), + }, + }); + + const handleSocketError = React.useCallback(() => { + setSocketAlert(true); + }, []); + + const handleNewSocketMessage: SocketMessage.AddressCoinBalance['handler'] = React.useCallback((payload) => { + setSocketAlert(false); + + queryClient.setQueryData( + getResourceKey('address_coin_balance', { pathParams: { hash: addressHash } }), + (prevData: AddressCoinBalanceHistoryResponse | undefined) => { + if (!prevData) { + return; + } + + return { + ...prevData, + items: [ + payload.coin_balance, + ...prevData.items, + ], + }; + }); + }, [ addressHash, queryClient ]); + + const channel = useSocketChannel({ + topic: `addresses:${ addressHash.toLowerCase() }`, + onSocketClose: handleSocketError, + onSocketError: handleSocketError, + isDisabled: !addressHash || coinBalanceQuery.isPlaceholderData || coinBalanceQuery.pagination.page !== 1, + }); + useSocketMessage({ + channel, + event: 'coin_balance', + handler: handleNewSocketMessage, + }); + + if (!isMounted || !shouldRender) { + return null; + } + + return ( + <> + { socketAlert && } + +

+ + + ); +}; + +export default React.memo(AddressCoinBalance); diff --git a/ui/address/AddressContract.pw.tsx b/ui/address/AddressContract.pw.tsx new file mode 100644 index 0000000000..5374c4ec21 --- /dev/null +++ b/ui/address/AddressContract.pw.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import * as addressMock from 'mocks/address/address'; +import * as contractInfoMock from 'mocks/contract/info'; +import * as contractMethodsMock from 'mocks/contract/methods'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; + +import AddressContract from './AddressContract.pwstory'; + +const hash = addressMock.contract.hash; + +test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('address', addressMock.contract, { pathParams: { hash } }); + await mockApiResponse( + 'contract', + { ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] }, + { pathParams: { hash } }, + ); +}); + +test.describe('ABI functionality', () => { + test('read', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'read_contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible(); + await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click(); + await expect(component.getByRole('button', { name: 'Read' })).toBeVisible(); + }); + + test('read, no wallet client', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'read_contract' }, + }, + }; + await mockEnvs(ENVS_MAP.noWalletClient); + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden(); + await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click(); + await expect(component.getByRole('button', { name: 'Read' })).toBeVisible(); + }); + + test('write', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'write_contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible(); + await component.getByText('setReserveInterestRateStrategyAddress').click(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Write' })).toBeEnabled(); + + await component.getByText('pause').click(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeHidden(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeEnabled(); + }); + + test('write, no wallet client', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'write_contract' }, + }, + }; + await mockEnvs(ENVS_MAP.noWalletClient); + + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden(); + await component.getByText('setReserveInterestRateStrategyAddress').click(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Write' })).toBeDisabled(); + + await component.getByText('pause').click(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeHidden(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeDisabled(); + }); +}); diff --git a/ui/address/AddressContract.pwstory.tsx b/ui/address/AddressContract.pwstory.tsx new file mode 100644 index 0000000000..c558dd9ca2 --- /dev/null +++ b/ui/address/AddressContract.pwstory.tsx @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import useContractTabs from 'lib/hooks/useContractTabs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import AddressContract from './AddressContract'; + +const AddressContractPwStory = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + const addressQuery = useApiQuery('address', { pathParams: { hash } }); + const { tabs } = useContractTabs(addressQuery.data, false); + return ; +}; + +export default AddressContractPwStory; diff --git a/ui/address/AddressContract.tsx b/ui/address/AddressContract.tsx new file mode 100644 index 0000000000..a349532a7a --- /dev/null +++ b/ui/address/AddressContract.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import type { RoutedSubTab } from 'ui/shared/Tabs/types'; + +import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; + +interface Props { + tabs: Array; + isLoading: boolean; + shouldRender?: boolean; +} + +const TAB_LIST_PROPS = { + columnGap: 3, +}; + +const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => { + if (!shouldRender) { + return null; + } + + return ( + + ); +}; + +export default React.memo(AddressContract); diff --git a/ui/address/AddressCsvExportLink.tsx b/ui/address/AddressCsvExportLink.tsx new file mode 100644 index 0000000000..313de665c8 --- /dev/null +++ b/ui/address/AddressCsvExportLink.tsx @@ -0,0 +1,57 @@ +import { chakra, Tooltip, Hide, Skeleton, Flex } from '@chakra-ui/react'; +import React from 'react'; + +import type { CsvExportParams } from 'types/client/address'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/links/LinkInternal'; + +interface Props { + address: string; + params: CsvExportParams; + className?: string; + isLoading?: boolean; +} + +const AddressCsvExportLink = ({ className, address, params, isLoading }: Props) => { + const isMobile = useIsMobile(); + const isInitialLoading = useIsInitialLoading(isLoading); + + if (!config.features.csvExport.isEnabled) { + return null; + } + + if (isInitialLoading) { + return ( + + + + + + + ); + } + + return ( + + + + Download CSV + + + ); +}; + +export default React.memo(chakra(AddressCsvExportLink)); diff --git a/ui/address/AddressDetails.pw.tsx b/ui/address/AddressDetails.pw.tsx new file mode 100644 index 0000000000..7d49ae723d --- /dev/null +++ b/ui/address/AddressDetails.pw.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import * as addressMock from 'mocks/address/address'; +import * as countersMock from 'mocks/address/counters'; +import * as tokensMock from 'mocks/address/tokens'; +import { test, expect, devices } from 'playwright/lib'; +import * as pwConfig from 'playwright/utils/config'; + +import AddressDetails from './AddressDetails'; +import MockAddressPage from './testUtils/MockAddressPage'; +import type { AddressQuery } from './utils/useAddressQuery'; + +const ADDRESS_HASH = addressMock.hash; +const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH }, + }, +}; + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + + test('contract', async({ render, mockApiResponse, page }) => { + await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } }); + + const component = await render(, { hooksConfig }); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(pwConfig.adsBannerSelector) ], + maskColor: pwConfig.maskColor, + }); + }); + + test('validator', async({ render, page, mockApiResponse }) => { + await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } }); + + const component = await render(, { hooksConfig }); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(pwConfig.adsBannerSelector) ], + maskColor: pwConfig.maskColor, + }); + }); + +}); + +test('contract', async({ render, page, mockApiResponse }) => { + await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } }); + + const component = await render(, { hooksConfig }); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(pwConfig.adsBannerSelector) ], + maskColor: pwConfig.maskColor, + }); +}); + +// there's an unexpected timeout occurred in this test +test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => { + await mockApiResponse('address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_counters', countersMock.forToken, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' }, times: 1 }); + await mockApiResponse('address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' }, times: 1 }); + await mockApiResponse('address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' }, times: 1 }); + await mockApiResponse('address_tokens', tokensMock.erc404List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' }, times: 1 }); + await injectMetaMaskProvider(); + + const component = await render( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(pwConfig.adsBannerSelector) ], + maskColor: pwConfig.maskColor, + }); +}); + +test('validator', async({ render, mockApiResponse, page }) => { + await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } }); + + const component = await render(, { hooksConfig }); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(pwConfig.adsBannerSelector) ], + maskColor: pwConfig.maskColor, + }); +}); diff --git a/ui/address/AddressDetails.tsx b/ui/address/AddressDetails.tsx new file mode 100644 index 0000000000..a0e3bc93b9 --- /dev/null +++ b/ui/address/AddressDetails.tsx @@ -0,0 +1,266 @@ +import { Box, Text, Grid } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import config from 'configs/app'; +import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import AddressCounterItem from 'ui/address/details/AddressCounterItem'; +import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem'; +import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; + +import AddressBalance from './details/AddressBalance'; +import AddressImplementations from './details/AddressImplementations'; +import AddressNameInfo from './details/AddressNameInfo'; +import AddressNetWorth from './details/AddressNetWorth'; +import TokenSelect from './tokenSelect/TokenSelect'; +import useAddressCountersQuery from './utils/useAddressCountersQuery'; +import type { AddressQuery } from './utils/useAddressQuery'; + +interface Props { + addressQuery: AddressQuery; + scrollRef?: React.RefObject; +} + +const AddressDetails = ({ addressQuery, scrollRef }: Props) => { + const router = useRouter(); + + const addressHash = getQueryParamString(router.query.hash); + + const countersQuery = useAddressCountersQuery({ + hash: addressHash, + addressQuery, + }); + + const handleCounterItemClick = React.useCallback(() => { + window.setTimeout(() => { + // cannot do scroll instantly, have to wait a little + scrollRef?.current?.scrollIntoView({ behavior: 'smooth' }); + }, 500); + }, [ scrollRef ]); + + const error404Data = React.useMemo(() => ({ + hash: addressHash || '', + is_contract: false, + implementations: null, + token: null, + watchlist_address_id: null, + watchlist_names: null, + creation_tx_hash: null, + block_number_balance_updated_at: null, + name: null, + exchange_rate: null, + coin_balance: null, + has_tokens: true, + has_token_transfers: true, + has_validated_blocks: false, + }), [ addressHash ]); + + const isMounted = useIsMounted(); + + // error handling (except 404 codes) + if (addressQuery.isError) { + if (isCustomAppError(addressQuery.error)) { + const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404; + if (!is404Error) { + throwOnResourceLoadError(addressQuery); + } + } else { + return ; + } + } + + const data = addressQuery.isError ? error404Data : addressQuery.data; + + if (!data || !isMounted) { + return null; + } + + return ( + <> + { addressQuery.isDegradedData && } + + + + { data.is_contract && data.creation_tx_hash && data.creator_address_hash && ( + <> + + Creator + + + + at txn + + + + ) } + { data.is_contract && data.implementations && data.implementations?.length > 0 && ( + + ) } + + + + { data.has_tokens && ( + <> + + Tokens + + + { addressQuery.data ? : 0 } + + + ) } + { (config.features.multichainButton.isEnabled || (data.exchange_rate && data.has_tokens)) && ( + <> + + Net worth + + + + + + ) + } + + + Transactions + + + { addressQuery.data ? ( + + ) : + 0 } + + + { data.has_token_transfers && ( + <> + + Transfers + + + { addressQuery.data ? ( + + ) : + 0 } + + + ) } + + { countersQuery.data?.gas_usage_count && ( + <> + + Gas used + + + { addressQuery.data ? ( + + ) : + 0 } + + + ) } + + { data.has_validated_blocks && ( + <> + + Blocks validated + + + { addressQuery.data ? ( + + ) : + 0 } + + + ) } + + { data.block_number_balance_updated_at && ( + <> + + Last balance update + + + + + + ) } + + + + + ); +}; + +export default React.memo(AddressDetails); diff --git a/ui/address/AddressInternalTxs.pw.tsx b/ui/address/AddressInternalTxs.pw.tsx new file mode 100644 index 0000000000..cce88f4c99 --- /dev/null +++ b/ui/address/AddressInternalTxs.pw.tsx @@ -0,0 +1,25 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import * as internalTxsMock from 'mocks/txs/internalTxs'; +import { test, expect } from 'playwright/lib'; + +import AddressInternalTxs from './AddressInternalTxs'; + +const ADDRESS_HASH = internalTxsMock.base.from.hash; +const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH }, + }, +}; + +test('base view +@mobile', async({ render, mockApiResponse }) => { + await mockApiResponse('address_internal_txs', internalTxsMock.baseResponse, { pathParams: { hash: ADDRESS_HASH } }); + const component = await render( + + + , + { hooksConfig }, + ); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/address/AddressInternalTxs.tsx b/ui/address/AddressInternalTxs.tsx new file mode 100644 index 0000000000..f870a237ba --- /dev/null +++ b/ui/address/AddressInternalTxs.tsx @@ -0,0 +1,112 @@ +import { Show, Hide } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { AddressFromToFilter } from 'types/api/address'; +import { AddressFromToFilterValues } from 'types/api/address'; + +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import { apos } from 'lib/html-entities'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { INTERNAL_TX } from 'stubs/internalTx'; +import { generateListStub } from 'stubs/utils'; +import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +import AddressCsvExportLink from './AddressCsvExportLink'; +import AddressTxsFilter from './AddressTxsFilter'; +import AddressIntTxsList from './internals/AddressIntTxsList'; + +const getFilterValue = (getFilterValueFromQuery).bind(null, AddressFromToFilterValues); + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} +const AddressInternalTxs = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + + const hash = getQueryParamString(router.query.hash); + + const { data, isPlaceholderData, isError, pagination, onFilterChange } = useQueryWithPages({ + resourceName: 'address_internal_txs', + pathParams: { hash }, + filters: { filter: filterValue }, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'address_internal_txs'>( + INTERNAL_TX, + 50, + { + next_page_params: { + block_number: 8987561, + index: 2, + items_count: 50, + transaction_index: 67, + }, + }, + ), + }, + }); + + const handleFilterChange = React.useCallback((val: string | Array) => { + const newVal = getFilterValue(val); + setFilterValue(newVal); + onFilterChange({ filter: newVal }); + }, [ onFilterChange ]); + + if (!isMounted || !shouldRender) { + return null; + } + + const content = data?.items ? ( + <> + + + + + + + + ) : null ; + + const actionBar = ( + + + + + + ); + + return ( + + ); +}; + +export default AddressInternalTxs; diff --git a/ui/address/AddressLogs.tsx b/ui/address/AddressLogs.tsx new file mode 100644 index 0000000000..be1db07c6b --- /dev/null +++ b/ui/address/AddressLogs.tsx @@ -0,0 +1,70 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { LOG } from 'stubs/log'; +import { generateListStub } from 'stubs/utils'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import LogItem from 'ui/shared/logs/LogItem'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +import AddressCsvExportLink from './AddressCsvExportLink'; + +type Props ={ + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressLogs = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const hash = getQueryParamString(router.query.hash); + const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ + resourceName: 'address_logs', + pathParams: { hash }, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'address_logs'>(LOG, 3, { next_page_params: { + block_number: 9005750, + index: 42, + items_count: 50, + transaction_index: 23, + } }), + }, + }); + + const actionBar = ( + + + + + ); + + if (!isMounted || !shouldRender) { + return null; + } + + const content = data?.items ? data.items.map((item, index) => ) : null; + + return ( + + ); +}; + +export default AddressLogs; diff --git a/ui/address/AddressMud.tsx b/ui/address/AddressMud.tsx new file mode 100644 index 0000000000..1848571125 --- /dev/null +++ b/ui/address/AddressMud.tsx @@ -0,0 +1,37 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useIsMounted from 'lib/hooks/useIsMounted'; + +import AddressMudRecord from './mud/AddressMudRecord'; +import AddressMudTable from './mud/AddressMudTable'; +import AddressMudTables from './mud/AddressMudTables'; + +type Props ={ + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressMud = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const isMounted = useIsMounted(); + const router = useRouter(); + const tableId = router.query.table_id?.toString(); + const recordId = router.query.record_id?.toString(); + + if (!isMounted || !shouldRender) { + return null; + } + + if (tableId && recordId) { + return ; + } + + if (tableId) { + return ; + } + + return ; +}; + +export default AddressMud; diff --git a/ui/address/AddressTokenTransfers.pw.tsx b/ui/address/AddressTokenTransfers.pw.tsx new file mode 100644 index 0000000000..f1cd2ffb00 --- /dev/null +++ b/ui/address/AddressTokenTransfers.pw.tsx @@ -0,0 +1,232 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect, devices } from 'playwright/lib'; + +import AddressTokenTransfers from './AddressTokenTransfers'; + +const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; +const TOKEN_HASH = '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; +const hooksConfig = { + router: { + query: { hash: CURRENT_ADDRESS, token: TOKEN_HASH }, + }, +}; + +// FIXME +// test cases which use socket cannot run in parallel since the socket server always run on the same port +test.describe.configure({ mode: 'serial' }); + +const tokenTransfersWithPagination = { + items: [ tokenTransferMock.erc1155A ], + next_page_params: { block_number: 1, index: 1, items_count: 1 }, +}; +const tokenTransfersWoPagination = { + items: [ tokenTransferMock.erc1155A ], + next_page_params: null, +}; + +test('with token filter and pagination', async({ render, mockApiResponse }) => { + await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { token: TOKEN_HASH }, + }); + const component = await render( + + + , + { hooksConfig }, + ); + await expect(component).toHaveScreenshot(); +}); + +test('with token filter and no pagination', async({ render, mockApiResponse }) => { + await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { token: TOKEN_HASH }, + }); + const component = await render( + + + , + { hooksConfig }, + ); + await expect(component).toHaveScreenshot(); +}); + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + + test('with token filter and pagination', async({ render, mockApiResponse }) => { + await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { token: TOKEN_HASH }, + }); + const component = await render( + + + , + { hooksConfig }, + ); + await expect(component).toHaveScreenshot(); + }); + + test('with token filter and no pagination', async({ render, mockApiResponse }) => { + await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { token: TOKEN_HASH }, + }); + const component = await render( + + + , + { hooksConfig }, + ); + await expect(component).toHaveScreenshot(); + }); +}); + +test.describe('socket', () => { + test('without overload', async({ render, mockApiResponse, createSocket, page }) => { + const hooksConfigNoToken = { + router: { + query: { hash: CURRENT_ADDRESS }, + }, + }; + await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { type: [] }, + }); + await render( + + + , + { hooksConfig: hooksConfigNoToken }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(4); + }); + + test('with overload', async({ render, mockApiResponse, page, createSocket }) => { + const hooksConfigNoToken = { + router: { + query: { hash: CURRENT_ADDRESS }, + }, + }; + await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { type: [] }, + }); + await render( + + + , + { hooksConfig: hooksConfigNoToken }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + + const counter = await page.locator('tbody tr:nth-child(1)').textContent(); + expect(counter?.startsWith('1 ')).toBe(true); + }); + + test('without overload, with filters', async({ render, mockApiResponse, page, createSocket }) => { + const hooksConfigWithFilter = { + router: { + query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' }, + }, + }; + await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { type: 'ERC-1155' }, + }); + + await render( + + + , + { hooksConfig: hooksConfigWithFilter }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20 ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + }); + + test('with overload, with filters', async({ render, mockApiResponse, page, createSocket }) => { + const hooksConfigWithFilter = { + router: { + query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' }, + }, + }; + await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { + pathParams: { hash: CURRENT_ADDRESS }, + queryParams: { type: 'ERC-1155' }, + }); + + await render( + + + , + { hooksConfig: hooksConfigWithFilter }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage( + socket, + channel, + 'token_transfer', + { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20, tokenTransferMock.erc1155C, tokenTransferMock.erc721 ] }, + ); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + + const counter = await page.locator('tbody tr:nth-child(1)').textContent(); + expect(counter?.startsWith('1 ')).toBe(true); + }); +}); diff --git a/ui/address/AddressTokenTransfers.tsx b/ui/address/AddressTokenTransfers.tsx new file mode 100644 index 0000000000..8230b921ba --- /dev/null +++ b/ui/address/AddressTokenTransfers.tsx @@ -0,0 +1,295 @@ +import { Flex, Hide, Show, Text } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SocketMessage } from 'lib/socket/types'; +import { AddressFromToFilterValues } from 'types/api/address'; +import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address'; +import type { TokenType } from 'types/api/token'; +import type { TokenTransfer } from 'types/api/tokenTransfer'; + +import { getResourceKey } from 'lib/api/useApiQuery'; +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import { apos } from 'lib/html-entities'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; +import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; +import { getTokenTransfersStub } from 'stubs/token'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import HashStringShorten from 'ui/shared/HashStringShorten'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import ResetIconButton from 'ui/shared/ResetIconButton'; +import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; +import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; +import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; +import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; + +import AddressCsvExportLink from './AddressCsvExportLink'; + +type Filters = { + type: Array; + filter: AddressFromToFilter | undefined; +} + +const getTokenFilterValue = (getFilterValuesFromQuery).bind(null, TOKEN_TYPE_IDS); +const getAddressFilterValue = (getFilterValueFromQuery).bind(null, AddressFromToFilterValues); + +const OVERLOAD_COUNT = 75; + +const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: string) => { + if (filters.filter) { + if (filters.filter === 'from' && tokenTransfer.from.hash !== address) { + return false; + } + if (filters.filter === 'to' && tokenTransfer.to.hash !== address) { + return false; + } + } + if (filters.type && filters.type.length) { + if (!filters.type.includes(tokenTransfer.token.type)) { + return false; + } + } + + return true; +}; + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; + // for tests only + overloadCount?: number; +} + +const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const queryClient = useQueryClient(); + const isMobile = useIsMobile(); + const isMounted = useIsMounted(); + + const currentAddress = getQueryParamString(router.query.hash); + + const [ socketAlert, setSocketAlert ] = React.useState(''); + const [ newItemsCount, setNewItemsCount ] = React.useState(0); + + const tokenFilter = getQueryParamString(router.query.token) || undefined; + + const [ filters, setFilters ] = React.useState( + { + type: getTokenFilterValue(router.query.type) || [], + filter: getAddressFilterValue(router.query.filter), + }, + ); + + const { isError, isPlaceholderData, data, pagination, onFilterChange } = useQueryWithPages({ + resourceName: 'address_token_transfers', + pathParams: { hash: currentAddress }, + filters: tokenFilter ? { token: tokenFilter } : filters, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: getTokenTransfersStub(undefined, { + block_number: 7793535, + index: 46, + items_count: 50, + }), + }, + }); + + const handleTypeFilterChange = React.useCallback((nextValue: Array) => { + onFilterChange({ ...filters, type: nextValue }); + setFilters((prevState) => ({ ...prevState, type: nextValue })); + }, [ filters, onFilterChange ]); + + const handleAddressFilterChange = React.useCallback((nextValue: string) => { + const filterVal = getAddressFilterValue(nextValue); + onFilterChange({ ...filters, filter: filterVal }); + setFilters((prevState) => ({ ...prevState, filter: filterVal })); + }, [ filters, onFilterChange ]); + + const resetTokenFilter = React.useCallback(() => { + onFilterChange({}); + }, [ onFilterChange ]); + + const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { + setSocketAlert(''); + + const newItems: Array = []; + let newCount = 0; + + payload.token_transfers.forEach(transfer => { + if (data?.items && data.items.length + newItems.length >= overloadCount) { + if (matchFilters(filters, transfer, currentAddress)) { + newCount++; + } + } else { + if (matchFilters(filters, transfer, currentAddress)) { + newItems.push(transfer); + } + } + }); + + if (newCount > 0) { + setNewItemsCount(prev => prev + newCount); + } + + if (newItems.length > 0) { + queryClient.setQueryData( + getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }), + (prevData: AddressTokenTransferResponse | undefined) => { + if (!prevData) { + return; + } + + return { + ...prevData, + items: [ + ...newItems, + ...prevData.items, + ], + }; + }, + ); + } + + }; + + const handleSocketClose = React.useCallback(() => { + setSocketAlert('Connection is lost. Please refresh the page to load new token transfers.'); + }, []); + + const handleSocketError = React.useCallback(() => { + setSocketAlert('An error has occurred while fetching new token transfers. Please refresh the page.'); + }, []); + + const channel = useSocketChannel({ + topic: `addresses:${ currentAddress.toLowerCase() }`, + onSocketClose: handleSocketClose, + onSocketError: handleSocketError, + isDisabled: pagination.page !== 1 || Boolean(tokenFilter), + }); + + useSocketMessage({ + channel, + event: 'token_transfer', + handler: handleNewSocketMessage, + }); + + const tokenData = React.useMemo(() => ({ + address: tokenFilter || '', + name: '', + icon_url: '', + symbol: '', + type: 'ERC-20' as const, + }), [ tokenFilter ]); + + if (!isMounted || !shouldRender) { + return null; + } + + const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); + const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; + + const content = data?.items ? ( + <> + + + + + { pagination.page === 1 && !tokenFilter && ( + + ) } + + + + ) : null; + + const tokenFilterComponent = tokenFilter && ( + + Filtered by token + + + { isMobile ? : tokenFilter } + + + + ); + + const actionBar = ( + <> + { isMobile && tokenFilterComponent } + { !isActionBarHidden && ( + + { !isMobile && tokenFilterComponent } + { !tokenFilter && ( + + ) } + { currentAddress && ( + + ) } + + + ) } + + ); + + return ( + + ); +}; + +export default AddressTokenTransfers; diff --git a/ui/address/AddressTokens.pw.tsx b/ui/address/AddressTokens.pw.tsx new file mode 100644 index 0000000000..3666588528 --- /dev/null +++ b/ui/address/AddressTokens.pw.tsx @@ -0,0 +1,255 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressTokensResponse } from 'types/api/address'; + +import * as addressMock from 'mocks/address/address'; +import * as tokensMock from 'mocks/address/tokens'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect, devices } from 'playwright/lib'; + +import AddressTokens from './AddressTokens'; + +const ADDRESS_HASH = addressMock.validator.hash; + +const nextPageParams = { + items_count: 50, + token_name: 'aaa', + token_type: 'ERC-20' as const, + value: 1, + fiat_value: '1', +}; + +test.beforeEach(async({ mockApiResponse }) => { + const response20: AddressTokensResponse = { + items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ], + next_page_params: nextPageParams, + }; + const response721: AddressTokensResponse = { + items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ], + next_page_params: nextPageParams, + }; + const response1155: AddressTokensResponse = { + items: [ tokensMock.erc1155a, tokensMock.erc1155b ], + next_page_params: nextPageParams, + }; + const response404: AddressTokensResponse = { + items: [ tokensMock.erc404a, tokensMock.erc404b ], + next_page_params: nextPageParams, + }; + + await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } }); + await mockApiResponse('address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } }); + await mockApiResponse('address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } }); + await mockApiResponse('address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } }); + await mockApiResponse('address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } }); + await mockApiResponse('address_nfts', tokensMock.nfts, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } }); + await mockApiResponse('address_collections', tokensMock.collections, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } }); +}); + +test('erc20 +@dark-mode', async({ render }) => { + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' }, + isReady: true, + }, + }; + + const component = await render( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); +}); + +test('collections +@dark-mode', async({ render }) => { + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, + isReady: true, + }, + }; + + const component = await render( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); +}); + +test('nfts +@dark-mode', async({ render }) => { + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, + isReady: true, + }, + }; + + const component = await render( + + + , + { hooksConfig }, + ); + + await component.getByText('List').click(); + + await expect(component).toHaveScreenshot(); +}); + +test.describe('mobile', () => { + test.use({ viewport: devices['iPhone 13 Pro'].viewport }); + + test('erc20', async({ render }) => { + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' }, + isReady: true, + }, + }; + + const component = await render( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); + }); + + test('nfts', async({ render }) => { + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, + isReady: true, + }, + }; + + const component = await render( + + + , + { hooksConfig }, + ); + + await component.getByLabel('list').click(); + + await expect(component).toHaveScreenshot(); + }); + + test('collections', async({ render }) => { + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, + isReady: true, + }, + }; + + const component = await render( + + + , + { hooksConfig }, + ); + + await expect(component).toHaveScreenshot(); + }); +}); + +test.describe('update balances via socket', () => { + test.describe.configure({ mode: 'serial' }); + + test('', async({ render, page, createSocket, mockApiResponse }) => { + test.slow(); + + const hooksConfig = { + router: { + query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' }, + isReady: true, + }, + }; + + const response20 = { + items: [ tokensMock.erc20a, tokensMock.erc20b ], + next_page_params: null, + }; + const response721 = { + items: [ tokensMock.erc721a, tokensMock.erc721b ], + next_page_params: null, + }; + const response1155 = { + items: [ tokensMock.erc1155a ], + next_page_params: null, + }; + const response404 = { + items: [ tokensMock.erc404a ], + next_page_params: null, + }; + + const erc20ApiUrl = await mockApiResponse('address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } }); + const erc721ApiUrl = await mockApiResponse('address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } }); + const erc1155ApiUrl = await mockApiResponse('address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } }); + const erc404ApiUrl = await mockApiResponse('address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } }); + + const component = await render( + + + , + { hooksConfig }, + { withSocket: true }, + ); + + await page.waitForResponse(erc20ApiUrl); + await page.waitForResponse(erc721ApiUrl); + await page.waitForResponse(erc1155ApiUrl); + await page.waitForResponse(erc404ApiUrl); + + await expect(component).toHaveScreenshot(); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ ADDRESS_HASH.toLowerCase() }`); + socketServer.sendMessage(socket, channel, 'updated_token_balances_erc_20', { + overflow: false, + token_balances: [ + { + ...tokensMock.erc20a, + token: { + ...tokensMock.erc20a.token, + exchange_rate: '0.01', + }, + }, + { + ...tokensMock.erc20c, + value: '9852000000000000', + token: { + ...tokensMock.erc20c.token, + address: '0xE2cf36D00C57e01371b94B4206ae2CF841931Adc', + name: 'Tether USD', + symbol: 'USDT', + }, + }, + ], + }); + socketServer.sendMessage(socket, channel, 'updated_token_balances_erc_721', { + overflow: false, + token_balances: [ + { + ...tokensMock.erc721c, + token: { + ...tokensMock.erc721c.token, + exchange_rate: '20', + }, + }, + ], + }); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/address/AddressTokens.tsx b/ui/address/AddressTokens.tsx new file mode 100644 index 0000000000..d1aefe2c37 --- /dev/null +++ b/ui/address/AddressTokens.tsx @@ -0,0 +1,188 @@ +import { Box, HStack } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { NFTTokenType } from 'types/api/token'; +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import config from 'configs/app'; +import { useAppContext } from 'lib/contexts/app'; +import * as cookies from 'lib/cookies'; +import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; +import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address'; +import { generateListStub } from 'stubs/utils'; +import PopoverFilter from 'ui/shared/filters/PopoverFilter'; +import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup'; +import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; + +import AddressCollections from './tokens/AddressCollections'; +import AddressNFTs from './tokens/AddressNFTs'; +import ERC20Tokens from './tokens/ERC20Tokens'; +import TokenBalances from './tokens/TokenBalances'; + +type TNftDisplayType = 'collection' | 'list'; + +const TAB_LIST_PROPS = { + mt: 1, + mb: { base: 6, lg: 1 }, + py: 5, + columnGap: 3, +}; + +const TAB_LIST_PROPS_MOBILE = { + my: 8, + columnGap: 3, +}; + +const getTokenFilterValue = (getFilterValuesFromQuery).bind(null, NFT_TOKEN_TYPE_IDS); + +type Props = { + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressTokens = ({ shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMobile = useIsMobile(); + const isMounted = useIsMounted(); + + const scrollRef = React.useRef(null); + + const displayTypeCookie = cookies.get(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, useAppContext().cookies); + const [ nftDisplayType, setNftDisplayType ] = React.useState(displayTypeCookie === 'list' ? 'list' : 'collection'); + const [ tokenTypes, setTokenTypes ] = React.useState | undefined>(getTokenFilterValue(router.query.type) || []); + + const tab = getQueryParamString(router.query.tab); + const hash = getQueryParamString(router.query.hash); + + const erc20Query = useQueryWithPages({ + resourceName: 'address_tokens', + pathParams: { hash }, + filters: { type: 'ERC-20' }, + scrollRef, + options: { + enabled: isQueryEnabled && (!tab || tab === 'tokens_erc20'), + refetchOnMount: false, + placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }), + }, + }); + + const collectionsQuery = useQueryWithPages({ + resourceName: 'address_collections', + pathParams: { hash }, + scrollRef, + options: { + enabled: isQueryEnabled && tab === 'tokens_nfts' && nftDisplayType === 'collection', + placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }), + }, + filters: { type: tokenTypes }, + }); + + const nftsQuery = useQueryWithPages({ + resourceName: 'address_nfts', + pathParams: { hash }, + scrollRef, + options: { + enabled: isQueryEnabled && tab === 'tokens_nfts' && nftDisplayType === 'list', + placeholderData: generateListStub<'address_nfts'>(ADDRESS_NFT_1155, 10, { next_page_params: null }), + }, + filters: { type: tokenTypes }, + }); + + const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => { + cookies.set(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, val); + setNftDisplayType(val); + }, []); + + const handleTokenTypesChange = React.useCallback((value: Array) => { + nftsQuery.onFilterChange({ type: value }); + collectionsQuery.onFilterChange({ type: value }); + setTokenTypes(value); + }, [ nftsQuery, collectionsQuery ]); + + if (!isMounted || !shouldRender) { + return null; + } + + const nftTypeFilter = ( + + nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/> + + ); + + const hasActiveFilters = Boolean(tokenTypes?.length); + + const tabs = [ + { id: 'tokens_erc20', title: `${ config.chain.tokenStandard }-20`, component: }, + { + id: 'tokens_nfts', + title: 'NFTs', + component: nftDisplayType === 'list' ? + : + , + }, + ]; + + const nftDisplayTypeRadio = ( + + onChange={ handleNFTsDisplayTypeChange } + defaultValue={ nftDisplayType } + name="type" + options={ [ + { title: 'By collection', value: 'collection', icon: 'collection', onlyIcon: isMobile }, + { title: 'List', value: 'list', icon: 'apps', onlyIcon: isMobile }, + ] } + /> + ); + + let pagination: PaginationParams | undefined; + + if (tab === 'tokens_nfts') { + pagination = nftDisplayType === 'list' ? nftsQuery.pagination : collectionsQuery.pagination; + } else { + pagination = erc20Query.pagination; + } + + const hasNftData = + (!nftsQuery.isPlaceholderData && nftsQuery.data?.items.length) || + (!collectionsQuery.isPlaceholderData && collectionsQuery.data?.items.length); + + const isNftTab = tab !== 'tokens' && tab !== 'tokens_erc20'; + + const rightSlot = ( + <> + + { isNftTab && (hasNftData || hasActiveFilters) && nftDisplayTypeRadio } + { isNftTab && (hasNftData || hasActiveFilters) && nftTypeFilter } + + { pagination.isVisible && !isMobile && } + + ); + + return ( + <> + + { /* should stay before tabs to scroll up with pagination */ } + + + + ); +}; + +export default AddressTokens; diff --git a/ui/address/AddressTxs.pw.tsx b/ui/address/AddressTxs.pw.tsx new file mode 100644 index 0000000000..487b88a008 --- /dev/null +++ b/ui/address/AddressTxs.pw.tsx @@ -0,0 +1,225 @@ +import { Box } from '@chakra-ui/react'; +import type { Locator } from '@playwright/test'; +import React from 'react'; + +import * as txMock from 'mocks/txs/tx'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; +import * as pwConfig from 'playwright/utils/config'; + +import AddressTxs from './AddressTxs'; + +const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; + +const hooksConfig = { + router: { + query: { hash: CURRENT_ADDRESS }, + }, +}; +const DEFAULT_PAGINATION = { block_number: 1, index: 1, items_count: 1 }; + +test.describe('base view', () => { + let component: Locator; + + test.beforeEach(async({ render, mockApiResponse }) => { + await mockApiResponse( + 'address_txs', + { + items: [ + txMock.base, + { ...txMock.base, hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194' }, + ], + next_page_params: DEFAULT_PAGINATION, + }, + { pathParams: { hash: CURRENT_ADDRESS } }, + ); + component = await render( + + + , + { hooksConfig }, + ); + }); + + test('+@mobile', async() => { + await expect(component).toHaveScreenshot(); + }); + + test.describe('screen xl', () => { + test.use({ viewport: pwConfig.viewport.xl }); + + test('', async() => { + test.slow(); + await expect(component).toHaveScreenshot(); + }); + }); +}); + +test.describe('socket', () => { + // FIXME + // test cases which use socket cannot run in parallel since the socket server always run on the same port + test.describe.configure({ mode: 'serial' }); + + test('without overload', async({ render, mockApiResponse, page, createSocket }) => { + await mockApiResponse( + 'address_txs', + { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION }, + { pathParams: { hash: CURRENT_ADDRESS } }, + ); + + await render( + + + , + { hooksConfig }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(4); + }); + + test('with update', async({ render, mockApiResponse, page, createSocket }) => { + await mockApiResponse( + 'address_txs', + { items: [ txMock.pending ], next_page_params: DEFAULT_PAGINATION }, + { pathParams: { hash: CURRENT_ADDRESS } }, + ); + + await render( + + + , + { hooksConfig }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base, txMock.base2 ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + }); + + test('with overload', async({ render, mockApiResponse, page, createSocket }) => { + await mockApiResponse( + 'address_txs', + { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION }, + { pathParams: { hash: CURRENT_ADDRESS } }, + ); + + await render( + + + , + { hooksConfig }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + + const counter = await page.locator('tbody tr:nth-child(1)').textContent(); + expect(counter?.startsWith('2 ')).toBe(true); + }); + + test('without overload, with filters', async({ render, mockApiResponse, page, createSocket }) => { + const hooksConfigWithFilter = { + router: { + query: { hash: CURRENT_ADDRESS, filter: 'from' }, + }, + }; + + await mockApiResponse( + 'address_txs', + { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION }, + { pathParams: { hash: CURRENT_ADDRESS }, queryParams: { filter: 'from' } }, + ); + + await render( + + + , + { hooksConfig: hooksConfigWithFilter }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + }); + + test('with overload, with filters', async({ render, mockApiResponse, page, createSocket }) => { + const hooksConfigWithFilter = { + router: { + query: { hash: CURRENT_ADDRESS, filter: 'from' }, + }, + }; + + await mockApiResponse( + 'address_txs', + { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION }, + { pathParams: { hash: CURRENT_ADDRESS }, queryParams: { filter: 'from' } }, + ); + + await render( + + + , + { hooksConfig: hooksConfigWithFilter }, + { withSocket: true }, + ); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`); + + const itemsCount = await page.locator('tbody tr').count(); + expect(itemsCount).toBe(2); + + socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] }); + + await page.waitForSelector('tbody tr:nth-child(3)'); + + const itemsCountNew = await page.locator('tbody tr').count(); + expect(itemsCountNew).toBe(3); + + const counter = await page.locator('tbody tr:nth-child(1)').textContent(); + expect(counter?.startsWith('1 ')).toBe(true); + }); +}); diff --git a/ui/address/AddressTxs.tsx b/ui/address/AddressTxs.tsx new file mode 100644 index 0000000000..869d6c3422 --- /dev/null +++ b/ui/address/AddressTxs.tsx @@ -0,0 +1,212 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { SocketMessage } from 'lib/socket/types'; +import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address'; +import { AddressFromToFilterValues } from 'types/api/address'; +import type { Transaction, TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction'; + +import { getResourceKey } from 'lib/api/useApiQuery'; +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; +import { TX } from 'stubs/tx'; +import { generateListStub } from 'stubs/utils'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; +import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; +import { sortTxsFromSocket } from 'ui/txs/sortTxs'; +import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting'; +import { SORT_OPTIONS } from 'ui/txs/useTxsSort'; + +import AddressCsvExportLink from './AddressCsvExportLink'; +import AddressTxsFilter from './AddressTxsFilter'; + +const OVERLOAD_COUNT = 75; + +const getFilterValue = (getFilterValueFromQuery).bind(null, AddressFromToFilterValues); + +const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => { + if (!filterValue) { + return true; + } + + if (filterValue === 'from') { + return transaction.from.hash === address; + } + + if (filterValue === 'to') { + return transaction.to?.hash === address; + } +}; + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; + // for tests only + overloadCount?: number; +} + +const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const queryClient = useQueryClient(); + const isMounted = useIsMounted(); + + const [ socketAlert, setSocketAlert ] = React.useState(''); + const [ newItemsCount, setNewItemsCount ] = React.useState(0); + const [ sort, setSort ] = React.useState(getSortValueFromQuery(router.query, SORT_OPTIONS)); + + const isMobile = useIsMobile(); + const currentAddress = getQueryParamString(router.query.hash); + + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + + const addressTxsQuery = useQueryWithPages({ + resourceName: 'address_txs', + pathParams: { hash: currentAddress }, + filters: { filter: filterValue }, + sorting: getSortParamsFromValue(sort), + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'address_txs'>(TX, 50, { next_page_params: { + block_number: 9005713, + index: 5, + items_count: 50, + } }), + }, + }); + + const handleFilterChange = React.useCallback((val: string | Array) => { + + const newVal = getFilterValue(val); + setFilterValue(newVal); + addressTxsQuery.onFilterChange({ filter: newVal }); + }, [ addressTxsQuery ]); + + const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => { + setSocketAlert(''); + + queryClient.setQueryData( + getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }), + (prevData: AddressTransactionsResponse | undefined) => { + if (!prevData) { + return; + } + + const newItems: Array = []; + let newCount = 0; + + payload.transactions.forEach(tx => { + const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash); + + if (currIndex > -1) { + prevData.items[currIndex] = tx; + } else { + if (matchFilter(filterValue, tx, currentAddress)) { + if (newItems.length + prevData.items.length >= overloadCount) { + newCount++; + } else { + newItems.push(tx); + } + } + } + }); + + if (newCount > 0) { + setNewItemsCount(prev => prev + newCount); + } + + return { + ...prevData, + items: [ + ...newItems, + ...prevData.items, + ].sort(sortTxsFromSocket(sort)), + }; + }); + }, [ currentAddress, filterValue, overloadCount, queryClient, sort ]); + + const handleSocketClose = React.useCallback(() => { + setSocketAlert('Connection is lost. Please refresh the page to load new transactions.'); + }, []); + + const handleSocketError = React.useCallback(() => { + setSocketAlert('An error has occurred while fetching new transactions. Please refresh the page.'); + }, []); + + const channel = useSocketChannel({ + topic: `addresses:${ currentAddress?.toLowerCase() }`, + onSocketClose: handleSocketClose, + onSocketError: handleSocketError, + isDisabled: addressTxsQuery.pagination.page !== 1 || addressTxsQuery.isPlaceholderData, + }); + + useSocketMessage({ + channel, + event: 'transaction', + handler: handleNewSocketMessage, + }); + + useSocketMessage({ + channel, + event: 'pending_transaction', + handler: handleNewSocketMessage, + }); + + if (!isMounted || !shouldRender) { + return null; + } + + const filter = ( + + ); + + const csvExportLink = ( + + ); + + return ( + <> + { !isMobile && ( + + { filter } + { currentAddress && csvExportLink } + + + ) } + + + ); +}; + +export default AddressTxs; diff --git a/ui/address/AddressTxsFilter.tsx b/ui/address/AddressTxsFilter.tsx new file mode 100644 index 0000000000..d3bdbd61fe --- /dev/null +++ b/ui/address/AddressTxsFilter.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import type { AddressFromToFilter } from 'types/api/address'; + +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio'; + +const OPTIONS = [ + { value: 'all', label: 'All' }, + { value: 'from', label: 'Outgoing transactions' }, + { value: 'to', label: 'Incoming transactions' }, +]; + +interface Props { + hasActiveFilter: boolean; + defaultFilter: AddressFromToFilter; + onFilterChange: (nextValue: string | Array) => void; + isLoading?: boolean; +} + +const AddressTxsFilter = ({ onFilterChange, defaultFilter, hasActiveFilter, isLoading }: Props) => { + const isInitialLoading = useIsInitialLoading(isLoading); + + return ( + + ); +}; + +export default React.memo(AddressTxsFilter); diff --git a/ui/address/AddressUserOps.tsx b/ui/address/AddressUserOps.tsx new file mode 100644 index 0000000000..2ef5a48bde --- /dev/null +++ b/ui/address/AddressUserOps.tsx @@ -0,0 +1,43 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { USER_OPS_ITEM } from 'stubs/userOps'; +import { generateListStub } from 'stubs/utils'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import UserOpsContent from 'ui/userOps/UserOpsContent'; + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} + +const AddressUserOps = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const hash = getQueryParamString(router.query.hash); + + const userOpsQuery = useQueryWithPages({ + resourceName: 'user_ops', + scrollRef, + options: { + enabled: isQueryEnabled && Boolean(hash), + placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 50, { next_page_params: { + page_token: '10355938,0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6', + page_size: 50, + } }), + }, + filters: { sender: hash }, + }); + + if (!isMounted || !shouldRender) { + return null; + } + + return ; +}; + +export default AddressUserOps; diff --git a/ui/address/AddressWithdrawals.tsx b/ui/address/AddressWithdrawals.tsx new file mode 100644 index 0000000000..600914f8b3 --- /dev/null +++ b/ui/address/AddressWithdrawals.tsx @@ -0,0 +1,84 @@ +import { Show, Hide } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import useIsMounted from 'lib/hooks/useIsMounted'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { generateListStub } from 'stubs/utils'; +import { WITHDRAWAL } from 'stubs/withdrawals'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem'; +import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable'; + +type Props = { + scrollRef?: React.RefObject; + shouldRender?: boolean; + isQueryEnabled?: boolean; +} +const AddressWithdrawals = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => { + const router = useRouter(); + const isMounted = useIsMounted(); + + const hash = getQueryParamString(router.query.hash); + + const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({ + resourceName: 'address_withdrawals', + pathParams: { hash }, + scrollRef, + options: { + enabled: isQueryEnabled, + placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, { next_page_params: { + index: 5, + items_count: 50, + } }), + }, + }); + + if (!isMounted || !shouldRender) { + return null; + } + + const content = data?.items ? ( + <> + + { data.items.map((item, index) => ( + + )) } + + + + + + ) : null ; + + const actionBar = pagination.isVisible ? ( + + + + ) : null; + + return ( + + ); +}; + +export default AddressWithdrawals; diff --git a/ui/address/SolidityscanReport.pw.tsx b/ui/address/SolidityscanReport.pw.tsx new file mode 100644 index 0000000000..f0e43ffbff --- /dev/null +++ b/ui/address/SolidityscanReport.pw.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import * as solidityscanReportMock from 'mocks/contract/solidityscanReport'; +import { test, expect } from 'playwright/lib'; + +import SolidityscanReport from './SolidityscanReport'; + +const addressHash = 'hash'; + +test('average report +@dark-mode +@mobile', async({ render, mockApiResponse, page }) => { + await mockApiResponse( + 'contract_solidityscan_report', + solidityscanReportMock.solidityscanReportAverage, + { pathParams: { hash: addressHash } }, + ); + const component = await render(); + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } }); + + await component.getByLabel('SolidityScan score').click(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } }); +}); + +test('great report', async({ render, mockApiResponse, page }) => { + await mockApiResponse( + 'contract_solidityscan_report', + solidityscanReportMock.solidityscanReportGreat, + { pathParams: { hash: addressHash } }, + ); + + const component = await render( + , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } }); + + await component.getByLabel('SolidityScan score').click(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } }); +}); + +test('low report', async({ render, mockApiResponse, page }) => { + await mockApiResponse( + 'contract_solidityscan_report', + solidityscanReportMock.solidityscanReportLow, + { pathParams: { hash: addressHash } }, + ); + + const component = await render( + , + ); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } }); + + await component.getByLabel('SolidityScan score').click(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } }); +}); diff --git a/ui/address/SolidityscanReport.tsx b/ui/address/SolidityscanReport.tsx new file mode 100644 index 0000000000..78eef956c1 --- /dev/null +++ b/ui/address/SolidityscanReport.tsx @@ -0,0 +1,73 @@ +import { Box, Text, Icon, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; +import React from 'react'; + +// This icon doesn't work properly when it is in the sprite +// Probably because of the gradient +// eslint-disable-next-line no-restricted-imports +import solidityScanIcon from 'icons/brands/solidity_scan.svg'; +import useApiQuery from 'lib/api/useApiQuery'; +import { SOLIDITYSCAN_REPORT } from 'stubs/contract'; +import Popover from 'ui/shared/chakra/Popover'; +import LinkExternal from 'ui/shared/links/LinkExternal'; +import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton'; +import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails'; +import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore'; + +interface Props { + hash: string; +} + +const SolidityscanReport = ({ hash }: Props) => { + const { isOpen, onToggle, onClose } = useDisclosure(); + + const { data, isPlaceholderData, isError } = useApiQuery('contract_solidityscan_report', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash), + placeholderData: SOLIDITYSCAN_REPORT, + retry: 0, + }, + }); + + const score = Number(data?.scan_report.scan_summary.score_v2); + + if (isError || !score) { + return null; + } + + const vulnerabilities = data?.scan_report.scan_summary.issue_severity_distribution; + const vulnerabilitiesCounts = vulnerabilities ? Object.values(vulnerabilities) : []; + const vulnerabilitiesCount = vulnerabilitiesCounts.reduce((acc, val) => acc + val, 0); + + return ( + + + + + + + + Contract analyzed for 240+ vulnerability patterns by + + SolidityScan + + + { vulnerabilities && vulnerabilitiesCount > 0 && ( + + Vulnerabilities distribution + + + ) } + View full report + + + + ); +}; + +export default React.memo(SolidityscanReport); diff --git a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png new file mode 100644 index 0000000000..4b2d50bcea Binary files /dev/null and b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_dark-color-mode_base-view-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png new file mode 100644 index 0000000000..58053b91e2 Binary files /dev/null and b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_default_base-view-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png new file mode 100644 index 0000000000..c86acec370 Binary files /dev/null and b/ui/address/__screenshots__/AddressCoinBalance.pw.tsx_mobile_base-view-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png new file mode 100644 index 0000000000..d78a586bf8 Binary files /dev/null and b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_contract-1.png differ diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png new file mode 100644 index 0000000000..316486202e Binary files /dev/null and b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-contract-1.png differ diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png new file mode 100644 index 0000000000..263b930fac Binary files /dev/null and b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_mobile-validator-1.png differ diff --git a/ui/address/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png new file mode 100644 index 0000000000..9a95c7b31f Binary files /dev/null and b/ui/address/__screenshots__/AddressDetails.pw.tsx_default_validator-1.png differ diff --git a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..06f9558e68 Binary files /dev/null and b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..b6f13d887f Binary files /dev/null and b/ui/address/__screenshots__/AddressInternalTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png new file mode 100644 index 0000000000..67ebd515ff Binary files /dev/null and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png new file mode 100644 index 0000000000..9ce16fb447 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png new file mode 100644 index 0000000000..3ed3b7d7bd Binary files /dev/null and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png new file mode 100644 index 0000000000..0a3332be93 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png new file mode 100644 index 0000000000..79c7e3e819 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png new file mode 100644 index 0000000000..b985ff98c0 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png new file mode 100644 index 0000000000..ac0c8b5935 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png new file mode 100644 index 0000000000..bbf9ed9894 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png new file mode 100644 index 0000000000..e8c1ab9d5b Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png new file mode 100644 index 0000000000..b9975934e7 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png new file mode 100644 index 0000000000..516642dc50 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png new file mode 100644 index 0000000000..fd147da3b8 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png new file mode 100644 index 0000000000..93b1606fa8 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png new file mode 100644 index 0000000000..af7d4ec144 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png new file mode 100644 index 0000000000..11053aa141 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..1d12e88f17 Binary files /dev/null and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png new file mode 100644 index 0000000000..e9ddf6c991 Binary files /dev/null and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..1d796de779 Binary files /dev/null and b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-1.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-1.png new file mode 100644 index 0000000000..cb130891fb Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png new file mode 100644 index 0000000000..35ddd924f3 Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_dark-color-mode_average-report-dark-mode-mobile-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-1.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-1.png new file mode 100644 index 0000000000..0462c76ca1 Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png new file mode 100644 index 0000000000..af76254bf1 Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_average-report-dark-mode-mobile-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-1.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-1.png new file mode 100644 index 0000000000..2cf3772046 Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png new file mode 100644 index 0000000000..058eebe139 Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_great-report-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-1.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-1.png new file mode 100644 index 0000000000..89796e448b Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png new file mode 100644 index 0000000000..058084c19c Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_default_low-report-2.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-1.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-1.png new file mode 100644 index 0000000000..9441baa4ca Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-1.png differ diff --git a/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png new file mode 100644 index 0000000000..0dae9f2a78 Binary files /dev/null and b/ui/address/__screenshots__/SolidityscanReport.pw.tsx_mobile_average-report-dark-mode-mobile-2.png differ diff --git a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx new file mode 100644 index 0000000000..01bec19066 --- /dev/null +++ b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx @@ -0,0 +1,69 @@ +import { Box, Flex, Skeleton, Text } from '@chakra-ui/react'; +import React, { useMemo } from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/links/LinkInternal'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; +import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; + +type Props = { + isPlaceholderData: boolean; + tx: NovesResponseData; + currentAddress: string; +}; + +const AddressAccountHistoryListItem = (props: Props) => { + + const parsedDescription = useMemo(() => { + const description = props.tx.classificationData.description; + + return description.endsWith('.') ? description.substring(0, description.length - 1) : description; + }, [ props.tx.classificationData.description ]); + + return ( + + + + + + + + Action + + + + + + + + { parsedDescription } + + + + + + + + ); +}; + +export default React.memo(AddressAccountHistoryListItem); diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx new file mode 100644 index 0000000000..6e5ee2505a --- /dev/null +++ b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx @@ -0,0 +1,68 @@ +import { Td, Tr, Skeleton, Box } from '@chakra-ui/react'; +import React, { useMemo } from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/links/LinkInternal'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; +import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; + +type Props = { + isPlaceholderData: boolean; + tx: NovesResponseData; + currentAddress: string; +}; + +const AddressAccountHistoryTableItem = (props: Props) => { + + const parsedDescription = useMemo(() => { + const description = props.tx.classificationData.description; + + return description.endsWith('.') ? description.substring(0, description.length - 1) : description; + }, [ props.tx.classificationData.description ]); + + return ( + + + + + + + + + + + { parsedDescription } + + + + + + + + + + + ); +}; + +export default React.memo(AddressAccountHistoryTableItem); diff --git a/ui/address/blocksValidated/AddressBlocksValidatedListItem.tsx b/ui/address/blocksValidated/AddressBlocksValidatedListItem.tsx new file mode 100644 index 0000000000..81065d7f27 --- /dev/null +++ b/ui/address/blocksValidated/AddressBlocksValidatedListItem.tsx @@ -0,0 +1,65 @@ +import { Flex, Skeleton } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { Block } from 'types/api/block'; + +import config from 'configs/app'; +import getBlockTotalReward from 'lib/block/getBlockTotalReward'; +import { currencyUnits } from 'lib/units'; +import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; + +type Props = Block & { + page: number; + isLoading: boolean; +}; + +const AddressBlocksValidatedListItem = (props: Props) => { + const totalReward = getBlockTotalReward(props); + + return ( + + + + + + + Txn + + { props.tx_count } + + + + Gas used + { BigNumber(props.gas_used || 0).toFormat() } + + + { !config.UI.views.block.hiddenFields?.total_reward && ( + + Reward { currencyUnits.ether } + { totalReward.toFixed() } + + ) } + + ); +}; + +export default React.memo(AddressBlocksValidatedListItem); diff --git a/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx b/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx new file mode 100644 index 0000000000..e08dbc7db7 --- /dev/null +++ b/ui/address/blocksValidated/AddressBlocksValidatedTableItem.tsx @@ -0,0 +1,70 @@ +import { Td, Tr, Flex, Skeleton } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { Block } from 'types/api/block'; + +import config from 'configs/app'; +import getBlockTotalReward from 'lib/block/getBlockTotalReward'; +import BlockGasUsed from 'ui/shared/block/BlockGasUsed'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; + +type Props = Block & { + page: number; + isLoading: boolean; +}; + +const AddressBlocksValidatedTableItem = (props: Props) => { + const totalReward = getBlockTotalReward(props); + + return ( + + + + + + + + + + { props.tx_count } + + + + + + { BigNumber(props.gas_used || 0).toFormat() } + + + + + { !config.UI.views.block.hiddenFields?.total_reward && ( + + + { totalReward.toFixed() } + + + ) } + + ); +}; + +export default React.memo(AddressBlocksValidatedTableItem); diff --git a/ui/address/coinBalance/AddressCoinBalanceChart.tsx b/ui/address/coinBalance/AddressCoinBalanceChart.tsx new file mode 100644 index 0000000000..15003058a4 --- /dev/null +++ b/ui/address/coinBalance/AddressCoinBalanceChart.tsx @@ -0,0 +1,43 @@ +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import { currencyUnits } from 'lib/units'; +import ChartWidget from 'ui/shared/chart/ChartWidget'; + +interface Props { + addressHash: string; +} + +const AddressCoinBalanceChart = ({ addressHash }: Props) => { + const { data, isPending, isError } = useApiQuery('address_coin_balance_chart', { + pathParams: { hash: addressHash }, + }); + + const items = React.useMemo(() => { + if (!data) { + return undefined; + } + + const dataItems = 'items' in data ? data.items : data; + return dataItems.map(({ date, value }) => ({ + date: new Date(date), + value: BigNumber(value).div(10 ** config.chain.currency.decimals).toNumber(), + })); + }, [ data ]); + + return ( + + ); +}; + +export default React.memo(AddressCoinBalanceChart); diff --git a/ui/address/coinBalance/AddressCoinBalanceHistory.tsx b/ui/address/coinBalance/AddressCoinBalanceHistory.tsx new file mode 100644 index 0000000000..06563f58cb --- /dev/null +++ b/ui/address/coinBalance/AddressCoinBalanceHistory.tsx @@ -0,0 +1,82 @@ +import { Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import React from 'react'; + +import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import type { ResourceError } from 'lib/api/resources'; +import { currencyUnits } from 'lib/units'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import Pagination from 'ui/shared/pagination/Pagination'; +import { default as Thead } from 'ui/shared/TheadSticky'; + +import AddressCoinBalanceListItem from './AddressCoinBalanceListItem'; +import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem'; + +interface Props { + query: UseQueryResult> & { + pagination: PaginationParams; + }; +} + +const AddressCoinBalanceHistory = ({ query }: Props) => { + + const content = query.data?.items ? ( + <> + + + + + + + + + + + + + { query.data.items.map((item, index) => ( + + )) } + +
BlockTxnAgeBalance { currencyUnits.ether }Delta
+
+ + { query.data.items.map((item, index) => ( + + )) } + + + ) : null; + + const actionBar = query.pagination.isVisible ? ( + + + + ) : null; + + return ( + + ); +}; + +export default React.memo(AddressCoinBalanceHistory); diff --git a/ui/address/coinBalance/AddressCoinBalanceListItem.tsx b/ui/address/coinBalance/AddressCoinBalanceListItem.tsx new file mode 100644 index 0000000000..4f9a21dc74 --- /dev/null +++ b/ui/address/coinBalance/AddressCoinBalanceListItem.tsx @@ -0,0 +1,74 @@ +import { Text, Stat, StatHelpText, StatArrow, Flex, Skeleton } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; + +import { WEI, ZERO } from 'lib/consts'; +import { currencyUnits } from 'lib/units'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; + +type Props = AddressCoinBalanceHistoryItem & { + page: number; + isLoading: boolean; +}; + +const AddressCoinBalanceListItem = (props: Props) => { + const deltaBn = BigNumber(props.delta).div(WEI); + const isPositiveDelta = deltaBn.gte(ZERO); + + return ( + + + + { BigNumber(props.value).div(WEI).dp(8).toFormat() } { currencyUnits.ether } + + + + + + + { deltaBn.dp(8).toFormat() } + + + + + + + Block + + + { props.transaction_hash && ( + + Txs + + + ) } + + Age + + + + ); +}; + +export default React.memo(AddressCoinBalanceListItem); diff --git a/ui/address/coinBalance/AddressCoinBalanceTableItem.tsx b/ui/address/coinBalance/AddressCoinBalanceTableItem.tsx new file mode 100644 index 0000000000..eff93d79a2 --- /dev/null +++ b/ui/address/coinBalance/AddressCoinBalanceTableItem.tsx @@ -0,0 +1,74 @@ +import { Td, Tr, Text, Stat, StatHelpText, StatArrow, Skeleton } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; + +import { WEI, ZERO } from 'lib/consts'; +import BlockEntity from 'ui/shared/entities/block/BlockEntity'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; +import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; + +type Props = AddressCoinBalanceHistoryItem & { + page: number; + isLoading: boolean; +}; + +const AddressCoinBalanceTableItem = (props: Props) => { + const deltaBn = BigNumber(props.delta).div(WEI); + const isPositiveDelta = deltaBn.gte(ZERO); + + return ( + + + + + + { props.transaction_hash && ( + + ) } + + + + + + + { BigNumber(props.value).div(WEI).dp(8).toFormat() } + + + + + + + + + { deltaBn.dp(8).toFormat() } + + + + + + + ); +}; + +export default React.memo(AddressCoinBalanceTableItem); diff --git a/ui/address/contract/ContractCode.pw.tsx b/ui/address/contract/ContractCode.pw.tsx new file mode 100644 index 0000000000..b076c5c8c7 --- /dev/null +++ b/ui/address/contract/ContractCode.pw.tsx @@ -0,0 +1,150 @@ +import React from 'react'; + +import * as addressMock from 'mocks/address/address'; +import { contractAudits } from 'mocks/contract/audits'; +import * as contractMock from 'mocks/contract/info'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; + +import ContractCode from './specs/ContractCode'; + +const hooksConfig = { + router: { + query: { hash: addressMock.contract.hash, tab: 'contract_code' }, + }, +}; + +// FIXME +// test cases which use socket cannot run in parallel since the socket server always run on the same port +test.describe.configure({ mode: 'serial' }); + +let addressApiUrl: string; + +test.beforeEach(async({ mockApiResponse, page }) => { + await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => { + route.abort(); + }); + addressApiUrl = await mockApiResponse('address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } }); +}); + +test('full view +@mobile +@dark-mode', async({ render, mockApiResponse, createSocket }) => { + await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } }); + + const component = await render(, { hooksConfig }, { withSocket: true }); + await createSocket(); + + await expect(component).toHaveScreenshot(); +}); + +test('verified with changed byte code socket', async({ render, mockApiResponse, createSocket }) => { + await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } }); + + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + socketServer.sendMessage(socket, channel, 'changed_bytecode', {}); + + await expect(component).toHaveScreenshot(); +}); + +test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse, createSocket, page }) => { + const contractApiUrl = await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + await page.waitForResponse(contractApiUrl); + socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', {}); + const request = await page.waitForRequest(addressApiUrl); + + expect(request).toBeTruthy(); +}); + +test('verified with multiple sources', async({ render, page, mockApiResponse }) => { + await mockApiResponse('contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + + const section = page.locator('section', { hasText: 'Contract source code' }); + await expect(section).toHaveScreenshot(); + + await page.getByRole('button', { name: 'View external libraries' }).click(); + await expect(section).toHaveScreenshot(); + + await page.getByRole('button', { name: 'Open source code in IDE' }).click(); + await expect(section).toHaveScreenshot(); +}); + +test('verified via sourcify', async({ render, mockApiResponse, page }) => { + await mockApiResponse('contract', contractMock.verifiedViaSourcify, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } }); +}); + +test('verified via eth bytecode db', async({ render, mockApiResponse, page }) => { + await mockApiResponse('contract', contractMock.verifiedViaEthBytecodeDb, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } }); +}); + +test('self destructed', async({ render, mockApiResponse, page }) => { + await mockApiResponse('contract', contractMock.selfDestructed, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }, { withSocket: true }); + + const section = page.locator('section', { hasText: 'Contract creation code' }); + await expect(section).toHaveScreenshot(); +}); + +test('with twin address alert +@mobile', async({ render, mockApiResponse }) => { + await mockApiResponse('contract', contractMock.withTwinAddress, { pathParams: { hash: addressMock.contract.hash } }); + const component = await render(, { hooksConfig }, { withSocket: true }); + + await expect(component.getByRole('alert')).toHaveScreenshot(); +}); + +test('with proxy address alert +@mobile', async({ render, mockApiResponse }) => { + await mockApiResponse('contract', contractMock.withProxyAddress, { pathParams: { hash: addressMock.contract.hash } }); + const component = await render(, { hooksConfig }, { withSocket: true }); + + await expect(component.getByRole('alert')).toHaveScreenshot(); +}); + +test('with certified icon +@mobile', async({ render, mockApiResponse, page }) => { + await mockApiResponse('contract', contractMock.certified, { pathParams: { hash: addressMock.contract.hash } }); + await render(, { hooksConfig }); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 120 } }); +}); + +test('non verified', async({ render, mockApiResponse }) => { + await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); + const component = await render(, { hooksConfig }, { withSocket: true }); + + await expect(component).toHaveScreenshot(); +}); + +test.describe('with audits feature', () => { + + test.beforeEach(async({ mockEnvs }) => { + await mockEnvs(ENVS_MAP.hasContractAuditReports); + }); + + test('no audits', async({ render, mockApiResponse }) => { + await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse('contract_security_audits', { items: [] }, { pathParams: { hash: addressMock.contract.hash } }); + const component = await render(, { hooksConfig }, { withSocket: true }); + + await expect(component).toHaveScreenshot(); + }); + + test('has audits', async({ render, mockApiResponse }) => { + await mockApiResponse('contract', contractMock.verified, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse('contract_security_audits', contractAudits, { pathParams: { hash: addressMock.contract.hash } }); + const component = await render(, { hooksConfig }, { withSocket: true }); + + await expect(component).toHaveScreenshot(); + }); +}); diff --git a/ui/address/contract/ContractCode.tsx b/ui/address/contract/ContractCode.tsx new file mode 100644 index 0000000000..5e23c33a28 --- /dev/null +++ b/ui/address/contract/ContractCode.tsx @@ -0,0 +1,354 @@ +import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; +import type { Channel } from 'phoenix'; +import React from 'react'; + +import type { SocketMessage } from 'lib/socket/types'; +import type { Address as AddressInfo } from 'types/api/address'; +import type { SmartContract } from 'types/api/contract'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import { getResourceKey } from 'lib/api/useApiQuery'; +import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; +import dayjs from 'lib/date/dayjs'; +import useSocketMessage from 'lib/socket/useSocketMessage'; +import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; +import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import Hint from 'ui/shared/Hint'; +import LinkExternal from 'ui/shared/links/LinkExternal'; +import LinkInternal from 'ui/shared/links/LinkInternal'; +import RawDataSnippet from 'ui/shared/RawDataSnippet'; + +import ContractSecurityAudits from './ContractSecurityAudits'; +import ContractSourceCode from './ContractSourceCode'; + +type Props = { + addressHash?: string; + contractQuery: UseQueryResult>; + channel: Channel | undefined; +} + +type InfoItemProps = { + label: string; + content: string | React.ReactNode; + className?: string; + isLoading: boolean; + hint?: string; +} + +const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoItemProps) => ( + + + + { label } + { hint && ( + + ) } + + + { content } + +)); + +const ContractCode = ({ addressHash, contractQuery, channel }: Props) => { + const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState(); + + const queryClient = useQueryClient(); + const addressInfo = queryClient.getQueryData(getResourceKey('address', { pathParams: { hash: addressHash } })); + + const { data, isPlaceholderData, isError } = contractQuery; + + const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => { + setIsChangedBytecodeSocket(true); + }, [ ]); + + const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => { + queryClient.refetchQueries({ + queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }), + }); + queryClient.refetchQueries({ + queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }), + }); + }, [ addressHash, queryClient ]); + + useSocketMessage({ + channel, + event: 'changed_bytecode', + handler: handleChangedBytecodeMessage, + }); + useSocketMessage({ + channel, + event: 'smart_contract_was_verified', + handler: handleContractWasVerifiedMessage, + }); + + if (isError) { + return ; + } + + const canBeVerified = !data?.is_self_destructed && !data?.is_verified; + + const verificationButton = isPlaceholderData ? ( + + ) : ( + + ); + + const licenseLink = (() => { + if (!data?.license_type) { + return null; + } + + const license = CONTRACT_LICENSES.find((license) => license.type === data.license_type); + if (!license || license.type === 'none') { + return null; + } + + return ( + + { license.label } + + ); + })(); + + const constructorArgs = (() => { + if (!data?.decoded_constructor_args) { + return data?.constructor_args; + } + + const decoded = data.decoded_constructor_args + .map(([ value, { name, type } ], index) => { + const valueEl = type === 'address' ? ( + + ) : { value }; + return ( + + Arg [{ index }] { name || '' } ({ type }): + { valueEl } + + ); + }); + + return ( + <> + { data.constructor_args } +

+ { decoded } + + ); + })(); + + const verificationAlert = (() => { + if (data?.is_verified_via_eth_bytecode_db) { + return ( + + This contract has been { data.is_partially_verified ? 'partially ' : '' }verified using + + Blockscout Bytecode Database + + + ); + } + + if (data?.is_verified_via_sourcify) { + return ( + + This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. + { data.sourcify_repo_url && View contract in Sourcify repository } + + ); + } + + return null; + })(); + + const contractNameWithCertifiedIcon = data?.is_verified ? ( + + { data.name } + { data.certified && } + + ) : null; + + return ( + <> + + { data?.is_blueprint && ( + + This is an + + ERC-5202 Blueprint contract + + + ) } + { data?.is_verified && ( + + + Contract Source Code Verified ({ data.is_partially_verified ? 'Partial' : 'Exact' } Match) + { data.is_partially_verified ? verificationButton : null } + + + ) } + { verificationAlert } + { (data?.is_changed_bytecode || isChangedBytecodeSocket) && ( + + Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky. + + ) } + { !data?.is_verified && data?.verified_twin_address_hash && !data?.minimal_proxy_address_hash && ( + + Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB + + All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with + + Verify & Publish + + page + + ) } + { data?.minimal_proxy_address_hash && ( + + Minimal Proxy Contract for + + . + + EIP-1167 + - minimal bytecode implementation that delegates all calls to a known address + + + ) } + + { data?.is_verified && ( + + { data.name && } + { data.compiler_version && } + { data.evm_version && } + { licenseLink && ( + + ) } + { typeof data.optimization_enabled === 'boolean' && + } + { data.optimization_runs !== null && + } + { data.verified_at && + } + { data.file_path && } + { config.UI.hasContractAuditReports && ( + } + isLoading={ isPlaceholderData } + /> + ) } + + ) } + + { constructorArgs && ( + + ) } + { data?.source_code && addressHash && ( + + ) } + { data?.compiler_settings ? ( + + ) : null } + { data?.abi && ( + + ) } + { data?.creation_bytecode && ( + + Contracts that self destruct in their constructors have no contract code published and cannot be verified. + Displaying the init data provided of the creating transaction. + + ) : null } + textareaMaxHeight="200px" + isLoading={ isPlaceholderData } + /> + ) } + { data?.deployed_bytecode && ( + + ) } + + + ); +}; + +export default ContractCode; diff --git a/ui/address/contract/ContractCodeIdes.tsx b/ui/address/contract/ContractCodeIdes.tsx new file mode 100644 index 0000000000..fc06317043 --- /dev/null +++ b/ui/address/contract/ContractCodeIdes.tsx @@ -0,0 +1,93 @@ +import { + Flex, + Button, + chakra, + PopoverTrigger, + PopoverBody, + PopoverContent, + Image, + Skeleton, + useDisclosure, + useColorModeValue, +} from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import Popover from 'ui/shared/chakra/Popover'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkExternal from 'ui/shared/links/LinkExternal'; + +interface Props { + className?: string; + hash: string; + isLoading?: string; +} + +const ContractCodeIde = ({ className, hash, isLoading }: Props) => { + const { isOpen, onToggle, onClose } = useDisclosure(); + const defaultIconColor = useColorModeValue('gray.600', 'gray.500'); + + const ideLinks = React.useMemo(() => { + return config.UI.ides.items + .map((ide) => { + const url = decodeURIComponent(ide.url.replace('{hash}', hash).replace('{domain}', config.app.host || '')); + const icon = 'icon_url' in ide ? + { : + ; + + return ( + + { icon } + { ide.title } + + ); + }); + }, [ defaultIconColor, hash ]); + + if (isLoading) { + return ; + } + + if (ideLinks.length === 0) { + return null; + } + + return ( + + + + + + + Redactors + + { ideLinks } + + + + + ); +}; + +export default React.memo(chakra(ContractCodeIde)); diff --git a/ui/address/contract/ContractExternalLibraries.tsx b/ui/address/contract/ContractExternalLibraries.tsx new file mode 100644 index 0000000000..83df015e2b --- /dev/null +++ b/ui/address/contract/ContractExternalLibraries.tsx @@ -0,0 +1,126 @@ +import { + Alert, + Box, + Button, + Flex, + Heading, + Modal, + ModalCloseButton, + ModalContent, + PopoverBody, + PopoverContent, + PopoverTrigger, + Skeleton, + StackDivider, + useDisclosure, + VStack, +} from '@chakra-ui/react'; +import React from 'react'; + +import type { SmartContractExternalLibrary } from 'types/api/contract'; + +import useIsMobile from 'lib/hooks/useIsMobile'; +import { apos } from 'lib/html-entities'; +import Popover from 'ui/shared/chakra/Popover'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + className?: string; + data: Array; + isLoading?: boolean; +} + +const Item = (data: SmartContractExternalLibrary) => { + return ( + + { data.name } + + + ); +}; + +const ContractExternalLibraries = ({ className, data, isLoading }: Props) => { + const { isOpen, onToggle, onClose } = useDisclosure(); + const isMobile = useIsMobile(); + + if (isLoading) { + return ; + } + + if (data.length === 0) { + return null; + } + + const button = ( + + ); + + const content = ( + <> + External libraries ({ data.length }) + + The linked library{ apos }s source code may not be the real one. + Check the source code at the library address (if any) if you want to be sure in case if there is any library linked + + } + spacing={ 2 } + mt={ 4 } + maxH={{ lg: '50vh' }} + overflowY="scroll" + > + { data.map((item) => ) } + + + ); + + if (isMobile) { + return ( + <> + { button } + + + + { content } + + + + ); + } + + return ( + + + { button } + + + + { content } + + + + ); +}; + +export default ContractExternalLibraries; diff --git a/ui/address/contract/ContractSecurityAudits.tsx b/ui/address/contract/ContractSecurityAudits.tsx new file mode 100644 index 0000000000..ce6e2fc9b3 --- /dev/null +++ b/ui/address/contract/ContractSecurityAudits.tsx @@ -0,0 +1,66 @@ +import { Box, Button, useDisclosure } from '@chakra-ui/react'; +import React from 'react'; + +import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; + +import useApiQuery from 'lib/api/useApiQuery'; +import dayjs from 'lib/date/dayjs'; +import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY'; +import FormModal from 'ui/shared/FormModal'; +import LinkExternal from 'ui/shared/links/LinkExternal'; + +import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm'; + +type Props = { + addressHash?: string; +} + +const ContractSecurityAudits = ({ addressHash }: Props) => { + const { data, isPlaceholderData } = useApiQuery('contract_security_audits', { + pathParams: { hash: addressHash }, + queryOptions: { + refetchOnMount: false, + placeholderData: { items: [] }, + enabled: Boolean(addressHash), + }, + }); + + const formTitle = 'Submit audit'; + + const modalProps = useDisclosure(); + + const renderForm = React.useCallback(() => { + return ; + }, [ addressHash, modalProps.onClose ]); + + return ( + <> + + { data?.items && data.items.length > 0 && ( + + + { data.items.map(item => ( + + { `${ item.audit_company_name }, ${ dayjs(item.audit_publish_date).format('MMM DD, YYYY') }` } + + )) } + + + ) } + + isOpen={ modalProps.isOpen } + onClose={ modalProps.onClose } + title={ formTitle } + renderForm={ renderForm } + /> + + ); +}; + +export default React.memo(ContractSecurityAudits); diff --git a/ui/address/contract/ContractSourceCode.tsx b/ui/address/contract/ContractSourceCode.tsx new file mode 100644 index 0000000000..e1ca2acd28 --- /dev/null +++ b/ui/address/contract/ContractSourceCode.tsx @@ -0,0 +1,175 @@ +import { Flex, Select, Skeleton, Text, Tooltip } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressImplementation } from 'types/api/addressParams'; +import type { SmartContract } from 'types/api/contract'; + +import { route } from 'nextjs-routes'; + +import useApiQuery from 'lib/api/useApiQuery'; +import * as stubs from 'stubs/contract'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; +import LinkInternal from 'ui/shared/links/LinkInternal'; +import CodeEditor from 'ui/shared/monaco/CodeEditor'; +import formatFilePath from 'ui/shared/monaco/utils/formatFilePath'; + +import ContractCodeIdes from './ContractCodeIdes'; +import ContractExternalLibraries from './ContractExternalLibraries'; + +function getEditorData(contractInfo: SmartContract | undefined) { + if (!contractInfo || !contractInfo.source_code) { + return undefined; + } + + const extension = (() => { + switch (contractInfo.language) { + case 'vyper': + return 'vy'; + case 'yul': + return 'yul'; + default: + return 'sol'; + } + })(); + + return [ + { file_path: formatFilePath(contractInfo.file_path || `index.${ extension }`), source_code: contractInfo.source_code }, + ...(contractInfo.additional_sources || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })), + ]; +} + +interface SourceContractOption { + address: string; + label: string; +} + +interface Props { + address: string; + implementations?: Array; +} + +export const ContractSourceCode = ({ address, implementations }: Props) => { + + const options: Array = React.useMemo(() => { + return [ + { label: 'Proxy', address }, + ...(implementations || []) + .filter((item) => item.name && item.address !== address) + .map(({ name, address }, item, array) => ({ address, label: array.length === 1 ? 'Implementation' : `Impl: ${ name }` })), + ]; + }, [ address, implementations ]); + + const [ sourceContract, setSourceContract ] = React.useState(options[0]); + + const contractQuery = useApiQuery('contract', { + pathParams: { hash: sourceContract.address }, + queryOptions: { + refetchOnMount: false, + placeholderData: stubs.CONTRACT_CODE_VERIFIED, + }, + }); + + const editorData = React.useMemo(() => { + return getEditorData(contractQuery.data); + }, [ contractQuery.data ]); + + const isLoading = contractQuery.isPlaceholderData; + + const handleSelectChange = React.useCallback((event: React.ChangeEvent) => { + const nextOption = options.find(({ address }) => address === event.target.value); + if (nextOption) { + setSourceContract(nextOption); + } + }, [ options ]); + + const heading = ( + + Contract source code + { contractQuery.data?.language && + ({ contractQuery.data.language }) } + + ); + + const select = options.length > 1 ? ( + + ) : null; + + const externalLibraries = contractQuery.data?.external_libraries ? + : + null; + + const diagramLink = contractQuery?.data?.can_be_visualized_via_sol2uml ? ( + + + + View UML diagram + + + + ) : null; + + const ides = ; + + const copyToClipboard = contractQuery.data && editorData?.length === 1 ? ( + + ) : + null; + + const content = (() => { + if (isLoading) { + return ; + } + + if (!editorData) { + return null; + } + + return ( + + ); + })(); + + return ( +
+ + { heading } + { select } + { externalLibraries } + { diagramLink } + { ides } + { copyToClipboard } + + { content } +
+ ); +}; + +export default React.memo(ContractSourceCode); diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..3db7025fd4 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_dark-color-mode_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..2108d45306 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_non-verified-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_non-verified-1.png new file mode 100644 index 0000000000..a6332dac40 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_non-verified-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_self-destructed-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_self-destructed-1.png new file mode 100644 index 0000000000..5545e8b31f Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_self-destructed-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-via-eth-bytecode-db-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-via-eth-bytecode-db-1.png new file mode 100644 index 0000000000..3582332da3 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-via-eth-bytecode-db-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-via-sourcify-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-via-sourcify-1.png new file mode 100644 index 0000000000..b797261a7a Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-via-sourcify-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png new file mode 100644 index 0000000000..241191634a Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-changed-byte-code-socket-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-1.png new file mode 100644 index 0000000000..bb3d1162ac Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-2.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-2.png new file mode 100644 index 0000000000..faed372ebc Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-3.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-3.png new file mode 100644 index 0000000000..7d9285311e Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_verified-with-multiple-sources-3.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png new file mode 100644 index 0000000000..8a0b09c4ea Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-has-audits-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png new file mode 100644 index 0000000000..1601567f2c Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-audits-feature-no-audits-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-certified-icon-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-certified-icon-mobile-1.png new file mode 100644 index 0000000000..9d405d4a33 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-certified-icon-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-proxy-address-alert-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-proxy-address-alert-mobile-1.png new file mode 100644 index 0000000000..f60ab9af44 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-proxy-address-alert-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-twin-address-alert-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-twin-address-alert-mobile-1.png new file mode 100644 index 0000000000..39196cf219 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_default_with-twin-address-alert-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..3f5ad28058 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_full-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-certified-icon-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-certified-icon-mobile-1.png new file mode 100644 index 0000000000..964b95edd1 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-certified-icon-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-proxy-address-alert-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-proxy-address-alert-mobile-1.png new file mode 100644 index 0000000000..b9c761a578 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-proxy-address-alert-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-twin-address-alert-mobile-1.png b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-twin-address-alert-mobile-1.png new file mode 100644 index 0000000000..2e9ff60d89 Binary files /dev/null and b/ui/address/contract/__screenshots__/ContractCode.pw.tsx_mobile_with-twin-address-alert-mobile-1.png differ diff --git a/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.pw.tsx b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.pw.tsx new file mode 100644 index 0000000000..a9053cad83 --- /dev/null +++ b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.pw.tsx @@ -0,0 +1,11 @@ +import noop from 'lodash/noop'; +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +import ContractSubmitAuditForm from './ContractSubmitAuditForm'; + +test('base view', async({ render }) => { + const component = await render(); + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.tsx b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.tsx new file mode 100644 index 0000000000..b03d4884cc --- /dev/null +++ b/ui/address/contract/contractSubmitAuditForm/ContractSubmitAuditForm.tsx @@ -0,0 +1,124 @@ +import { Button, VStack } from '@chakra-ui/react'; +import React from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; + +import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; + +import type { ResourceError } from 'lib/api/resources'; +import useApiFetch from 'lib/api/useApiFetch'; +import useToast from 'lib/hooks/useToast'; + +import AuditComment from './fields/AuditComment'; +import AuditCompanyName from './fields/AuditCompanyName'; +import AuditProjectName from './fields/AuditProjectName'; +import AuditProjectUrl from './fields/AuditProjectUrl'; +import AuditReportDate from './fields/AuditReportDate'; +import AuditReportUrl from './fields/AuditReportUrl'; +import AuditSubmitterEmail from './fields/AuditSubmitterEmail'; +import AuditSubmitterIsOwner from './fields/AuditSubmitterIsOwner'; +import AuditSubmitterName from './fields/AuditSubmitterName'; + +interface Props { + address?: string; + onSuccess: () => void; +} + +export type Inputs = { + submitter_name: string; + submitter_email: string; + is_project_owner: boolean; + project_name: string; + project_url: string; + audit_company_name: string; + audit_report_url: string; + audit_publish_date: string; + comment?: string; +} + +type AuditSubmissionErrors = { + errors: Record>; +} + +const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { + const containerRef = React.useRef(null); + + const apiFetch = useApiFetch(); + const toast = useToast(); + + const { handleSubmit, formState, control, setError } = useForm({ + mode: 'onTouched', + defaultValues: { is_project_owner: false }, + }); + + const onFormSubmit: SubmitHandler = React.useCallback(async(data) => { + try { + await apiFetch<'contract_security_audits', SmartContractSecurityAuditSubmission, AuditSubmissionErrors>('contract_security_audits', { + pathParams: { hash: address }, + fetchParams: { + method: 'POST', + body: data, + }, + }); + + toast({ + position: 'top-right', + title: 'Success', + description: 'Your audit report has been successfully submitted for review', + status: 'success', + variant: 'subtle', + isClosable: true, + }); + + onSuccess(); + + } catch (_error) { + const error = _error as ResourceError; + // add scroll to the error field + const errorMap = error?.payload?.errors; + if (errorMap && Object.keys(errorMap).length) { + (Object.keys(errorMap) as Array).forEach((errorField) => { + setError(errorField, { type: 'custom', message: errorMap[errorField].join(', ') }); + }); + } else { + toast({ + position: 'top-right', + title: 'Error', + description: (_error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.', + status: 'error', + variant: 'subtle', + isClosable: true, + }); + } + } + }, [ apiFetch, address, toast, setError, onSuccess ]); + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +export default React.memo(ContractSubmitAuditForm); diff --git a/ui/address/contract/contractSubmitAuditForm/__screenshots__/ContractSubmitAuditForm.pw.tsx_default_base-view-1.png b/ui/address/contract/contractSubmitAuditForm/__screenshots__/ContractSubmitAuditForm.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..b13f4662bb Binary files /dev/null and b/ui/address/contract/contractSubmitAuditForm/__screenshots__/ContractSubmitAuditForm.pw.tsx_default_base-view-1.png differ diff --git a/ui/address/contract/contractSubmitAuditForm/fields/AuditComment.tsx b/ui/address/contract/contractSubmitAuditForm/fields/AuditComment.tsx new file mode 100644 index 0000000000..918a496dea --- /dev/null +++ b/ui/address/contract/contractSubmitAuditForm/fields/AuditComment.tsx @@ -0,0 +1,40 @@ +import { FormControl, Textarea } from '@chakra-ui/react'; +import React from 'react'; +import type { Control, ControllerProps } from 'react-hook-form'; +import { Controller } from 'react-hook-form'; + +import InputPlaceholder from 'ui/shared/InputPlaceholder'; + +import type { Inputs } from '../ContractSubmitAuditForm'; + +interface Props { + control: Control; +} + +const AuditComment = ({ control }: Props) => { + const renderControl: ControllerProps['render'] = React.useCallback(({ field, fieldState }) => { + return ( + +