Commit 0de954b9 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into block-details-api

parents 925f7fdd 72ae4ea7
SENTRY_DSN=xxx
NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_ORG=block-scout
SENTRY_PROJECT=new-ui
SENTRY_AUTH_TOKEN=xxx
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_BLOCKSCOUT_VERSION=xxx
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true,"chainId":100,"currency":"xDAI"},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60","chainId":300,"currency":"xDAI"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets","chainId":200,"currency":"xDAI"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets","chainId":1,"currency":"ETH"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets","chainId":61,"currency":"ETC"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","chainId":99,"currency":"POA","nativeTokenAddress": "0x029a799563238d0e75e20be2f4bda0ea68d00172"},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets","chainId":30,"currency":"RBTC"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true,"currency":"xDAI"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets","chainId":77,"currency":"SPOA"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other","chainId":246529,"currency":"ATS"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other","chainId":22,"currency":"LYX"},{"name":"Astar","type":"astar","group":"other","chainId":22,"currency":"ASTR"}]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_SUPPORTED_NETWORKS=APP_NEXT_NEXT_PUBLIC_SUPPORTED_NETWORKS
NEXT_PUBLIC_BLOCKSCOUT_VERSION=APP_NEXT_NEXT_PUBLIC_BLOCKSCOUT_VERSION
NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK
NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=APP_NEXT_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE
NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME
NEXT_PUBLIC_NETWORK_SHORT_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_SHORT_NAME
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=APP_NEXT_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME
NEXT_PUBLIC_NETWORK_TYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_TYPE
NEXT_PUBLIC_NETWORK_SUBTYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_SUBTYPE
NEXT_PUBLIC_NETWORK_ID=APP_NEXT_NEXT_PUBLIC_NETWORK_ID
NEXT_PUBLIC_NETWORK_CURRENCY=APP_NEXT_NEXT_PUBLIC_NETWORK_CURRENCY
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=APP_NEXT_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=APP_NEXT_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED
NEXT_PUBLIC_FEATURED_NETWORKS=APP_NEXT_NEXT_PUBLIC_FEATURED_NETWORKS
NEXT_PUBLIC_APP_PROTOCOL=APP_NEXT_NEXT_PUBLIC_APP_PROTOCOL
NEXT_PUBLIC_APP_HOST=APP_NEXT_NEXT_PUBLIC_APP_HOST
NEXT_PUBLIC_APP_PORT=APP_NEXT_NEXT_PUBLIC_APP_PORT
NEXT_PUBLIC_API_ENDPOINT=APP_NEXT_NEXT_PUBLIC_API_ENDPOINT
NEXT_PUBLIC_API_BASE_PATH=APP_NEXT_NEXT_PUBLIC_API_BASE_PATH
......@@ -4,6 +4,7 @@ const RESTRICTED_MODULES = {
{ name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
],
};
module.exports = {
env: {
es6: true,
......@@ -202,6 +203,12 @@ module.exports = {
],
'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/config.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', {
......@@ -275,5 +282,12 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: [ 'configs/**/*.js', 'configs/**/*.ts', '*.config.ts' ],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
},
},
],
};
__snapshots__/** filter=lfs diff=lfs merge=lfs -text
name: Cleanup environments
on:
pull_request:
types:
- closed
- merged
workflow_dispatch:
jobs:
cleanup:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@master
with:
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
secrets: inherit
name: Run E2E tests k8s
on:
# push:
# pull_request:
workflow_dispatch
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
K8S_HOST: ${{ secrets.K8S_HOST }}
BASTION_HOST: ${{ secrets.BASTION_HOST }}
K8S_PORT: ${{ secrets.K8S_PORT }}
USERNAME: ${{ secrets.USERNAME }}
BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}}
jobs:
push_to_registry:
name: Push Docker image to registry
runs-on: ubuntu-latest
outputs:
shortSha: ${{ steps.output-step.outputs.short-sha }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# 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@v4
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: Add outputs
run: |
echo "::set-output name=short-sha::${{ env.SHORT_SHA }}"
id: output-step
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_and_tests:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
with:
valuesDir: deploy/values/e2e
appName: e2e-front
appNamespace: e2e-front-$GITHUB_SHA_SHORT
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
name: Linters
on: [pull_request]
on:
pull_request:
push:
branches:
- main
jobs:
eslint:
check_code:
name: Run code checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn lint:eslint
- name: Compile TypeScript
run: yarn lint:tsc
\ No newline at end of file
name: Playwright
on: [pull_request]
jobs:
test:
name: Run components visual tests
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.27.0-focal
steps:
- run: apt-get update && apt-get install git-lfs
- uses: actions/checkout@v3
with:
lfs: 'true'
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run your tests
run: HOME=/root yarn test-ct
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: playwright-report
path: playwright-report
\ No newline at end of file
name: Deploy review environment
on:
# push:
pull_request:
workflow_dispatch:
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
K8S_HOST: ${{ secrets.K8S_HOST }}
BASTION_HOST: ${{ secrets.BASTION_HOST }}
K8S_PORT: ${{ secrets.K8S_PORT }}
USERNAME: ${{ secrets.USERNAME }}
BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}}
jobs:
push_to_registry:
name: Push Docker image to registry
runs-on: ubuntu-latest
outputs:
shortSha: ${{ steps.output-step.outputs.short-sha }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# 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@v4
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: Add outputs
run: |
echo "::set-output name=short-sha::${{ env.SHORT_SHA }}"
id: output-step
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_frontend:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values/review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -12,8 +12,6 @@ env:
K8S_PORT: ${{ secrets.K8S_PORT }}
USERNAME: ${{ secrets.USERNAME }}
BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
push_to_registry:
......@@ -50,29 +48,21 @@ jobs:
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
name: Deploy frontend to k8s
build-args: |
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_main:
needs: push_to_registry
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set Kubernetes Context
uses: azure/k8s-set-context@v1
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Deploy to k8s
shell: bash
working-directory: charts
# port forwarding works only inside the step, consider refactoring as TS action
env:
NAMESPACE_NAME: bs-frontend
run: |
mkdir ~/.ssh
ssh-keyscan -H $BASTION_HOST >> ~/.ssh/known_hosts
eval `ssh-agent -s`
ssh-add - <<< "$BASTION_SSH_KEY"
sudo echo "127.0.0.1 $K8S_HOST" | sudo tee -a /etc/hosts
ssh -fN -v -L $K8S_LOCAL_PORT:$K8S_HOST:$K8S_PORT $USERNAME@$BASTION_HOST
helm upgrade --install -n $NAMESPACE_NAME $NAMESPACE_NAME ./ -f values-frontend.yaml --create-namespace
valuesDir: deploy/values/review
appNamespace: front-main
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:main
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -28,6 +28,7 @@ yarn-error.log*
# local env files
.env*.local
/configs/envs/.env.secrets
# vercel
.vercel
......@@ -39,3 +40,8 @@ yarn-error.log*
# Sentry
.sentryclirc
**.decrypted~**
/test-results/
/playwright-report/
/playwright/.cache/
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; }
git lfs post-checkout "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; }
git lfs post-commit "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; }
git lfs post-merge "$@"
#!/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 "$@"
......@@ -23,10 +23,30 @@
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "dev:poa_core",
"problemMatcher": [],
"label": "dev server: poa",
"detail": "start local dev server for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiMagenta",
"id": "server-process"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "typescript",
"label": "tsc build",
"detail": "run ts typechecking",
"detail": "compile typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
......@@ -42,6 +62,106 @@
"revealProblems": "onProblem",
},
"group": "build",
},
{
"type": "npm",
"script": "lint:eslint",
"problemMatcher": [],
"label": "eslint",
"detail": "run eslint",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiYellow",
"id": "zap"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "test-docker",
"problemMatcher": [],
"label": "test: playwright",
"detail": "run visual components tests",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"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": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiRed",
"id": "symbol-structure"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "start:docker:poa_core",
"problemMatcher": [],
"label": "docker: run poa",
"detail": "run docker container for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiRed",
"id": "browser"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "format-svg",
"problemMatcher": [],
"label": "format svg",
"detail": "format svg files with svgo",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiCyan",
"id": "combine"
},
"runOptions": {
"instanceLimit": 1
}
},
]
}
\ No newline at end of file
......@@ -8,18 +8,27 @@ WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
COPY .env.template .env.production
# 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
ARG SENTRY_DSN
ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_CSP_REPORT_URI
ARG SENTRY_AUTH_TOKEN
# pass commit sha to the app (uses by sentry.io as release version)
ARG GIT_COMMIT_SHA
ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
RUN yarn build
# Production image, copy all the files and run next
......@@ -37,11 +46,23 @@ COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
# Copy scripts and ENV templates file
COPY ./deploy/scripts/entrypoint.sh .
COPY ./deploy/scripts/replace_envs.sh .
COPY .env.template .
# 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
# Execute script for replace build ENV with run ones
RUN apk add --no-cache --upgrade bash
RUN ["chmod", "+x", "./entrypoint.sh"]
RUN ["chmod", "+x", "./replace_envs.sh"]
ENTRYPOINT ["./entrypoint.sh"]
USER nextjs
EXPOSE 3000
......
......@@ -9,47 +9,93 @@ Core technologies what used in the project are
- [Next.js](https://nextjs.org/) as application framework
- [Chakra](https://chakra-ui.com/) as component library; our theme customization could be found in `/theme` folder
- [css-modules](https://github.com/css-modules/css-modules) as lib for styling custom components
- [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
**Pre-requisites** You should have installed Node.js v16. The best way to manage your local Node.js version is [nvm](https://github.com/nvm-sh/nvm)
For local development please follow next steps:
- clone repo
- install dependencies with `yarn`
- create local env file `.env.local` according to `.env.example` snapshot (see list of used environment variables [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output
- clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secret values (see description [below](#environment-variables))
- to spin up local dev server
- for predefined networks configs (see full available list in `package.json`) you can just run `yarn dev:<app_name>`
- for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev`
- navigate to the host from logs output
## Components visual testing
We use [playwright experimental components testing](https://playwright.dev/docs/test-components) for visual (screenshots) CI check. Test renders a single component in headless browser in docker, generates screenshots and then compares this screenshot with a reference one.
To perform testing locally you need to install docker and run `yarn test-docker`
## Environment variables
### Variables list
The app instance could be customized by passing following variables to NodeJS environment.
The app instance could be customized by passing following variables to NodeJS environment at runtime.
**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted
### Network configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_SUPPORTED_NETWORKS | `Array<Network>` where `Network` can have following [properties](#network-configuration-properties) | Configuration of supported networks | `[{"name":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true,"chainId":99,"currency":"POA"}]` |
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` |
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
| NEXT_PUBLIC_NETWORK_TYPE | `string` | Network type (used as first part of the base path) | `xdai` |
| NEXT_PUBLIC_NETWORK_SUBTYPE | `string` | Network subtype (used as second part of the base path) | `mainnet` |
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
| NEXT_PUBLIC_NETWORK_CURRENCY | `string` | Network currency symbol | `xDAI` |
| NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS | `string` | Address of network's native token | `0x029a799563238d0e75e20be2f4bda0ea68d00172` |
| NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). If not provided, the network type will be used as its assets path part | `ethereum` |
| NEXT_PUBLIC_NETWORK_LOGO | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `https://www.fillmurray.com/240/40` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### UI configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#featured-network-configuration-properties) | Configuration of featured networks that will be shown in the network menu | `[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]` |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` *(optional)* | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` *(optional)* | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
### Network configuration properties
### App configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_APP_INSTANCE | `string` *(optional)* | Name of app instance | `wonderful_kepler` |
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` |
| NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` |
| NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` |
### API configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_API_ENDPOINT | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API endpoint base URL in this variable | `https://blockscout.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
### Featured network configuration properties
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `"Gnosis Chain"` |
| shortName | `string` | Used for SEO attributes (page title and description) | `"OoG"` |
| chainId | `number` | Id of the network. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `1` |
| currency | `string` | Network currency symbol. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `"xDAI"` |
| nativeTokenAddress | `string` | Address of network's native token | `"0x029a799563238d0e75e20be2f4bda0ea68d00172"` |
| type | `string` | Network type (used as first part of the base path) | `"xdai"` |
| subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `"mainnets"` |
| isAccountSupported | `boolean` *(optional)* | Set to true if network has account feature | `true` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `"https://www.fillmurray.com/60/60"` |
| logo | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `"https://www.fillmurray.com/240/40"` |
| assetsNamePath | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). The project already has some pre-defined mapping for popular network, which is match assetsNamePath against provided network type and sub-type. So typically you don't need to provide this variable, if you network is in the list or its type in config is conformed to a name in TrustWallet repo. If it is not the case, pass value here | `"ethereum"` |
| title | `string` | Displayed name of the network | `'Gnosis Chain'` |
| basePath | `string` | Network explorer main page url | `'/xdai/mainnet'` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `'https://www.fillmurray.com/60/60'` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### External services configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` |
\ No newline at end of file
# values-frontend.yaml
apiVersion: v1
appVersion: 0.0.1
version: 0.0.1
name: bs-frontend
description: '''
Helm chart for deploying blockscout frontend in K8S
Deploy command: `helm upgrade --install -n=<namespace> bs-frontend ./ -f values-<name>.yaml`
'''
{{- define "app_env" }}
{{- range $key, $value := .Values.environment }}
{{- $item := get $.Values.environment $key }}
{{- if or (kindIs "string" $item) (kindIs "int64" $item) (kindIs "bool" $item)}}
- name: {{ $key }}
value: {{ $value | quote }}
{{- else }}
- name: {{ $key }}
value: {{ pluck $.Values.global.env $item | first | default $item._default | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- define "node_env" }}
{{- range $key, $value := .Values.node_environment }}
{{- $item := get $.Values.node_environment $key }}
{{- if or (kindIs "string" $item) (kindIs "int64" $item) (kindIs "bool" $item)}}
- name: {{ $key }}
value: {{ $value | quote }}
{{- else }}
- name: {{ $key }}
value: {{ pluck $.Values.global.env $item | first | default $item._default | quote }}
{{- end }}
{{- end }}
{{- end }}
kind: Deployment
apiVersion: apps/v1
metadata:
name: {{ .Release.Name }}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "http-metrics"
spec:
replicas: {{ .Values.replicas.app }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
# serviceAccountName: vault-auth
imagePullSecrets:
- name: regcred
containers:
- name: {{ .Release.Name }}
image: {{ .Values.image }}
resources:
{{- with .Values.resources }}
limits:
memory: {{ pluck $.Values.global.env .limits.memory | first | default .limits.memory._default | quote }}
cpu: {{ pluck $.Values.global.env .limits.cpu | first | default .limits.cpu._default | quote }}
requests:
memory: {{ pluck $.Values.global.env .requests.memory | first | default .requests.memory._default | quote }}
cpu: {{ pluck $.Values.global.env .requests.cpu | first | default .requests.cpu._default | quote }}
{{- end }}
imagePullPolicy: Always
ports:
- containerPort: {{ .Values.docker.targetPort }}
env:
{{- include "app_env" . | indent 10 }}
# volumeMounts:
# - name: smweb-logs
# mountPath: /usr/local/sm-web-server/log
# readinessProbe:
# httpGet:
# path: /appversion
# port: {{ .Values.docker.port }}
# scheme: HTTP
# initialDelaySeconds: 60
# periodSeconds: 10
# livenessProbe:
# httpGet:
# path: /appversion
# port: {{ .Values.docker.port }}
# scheme: HTTP
# initialDelaySeconds: 100
# periodSeconds: 100
# volumes:
# - name: smweb-logs
# emptyDir: { }
# - name: config
# configMap:
# name: {{ .Release.Name }}-promtail-configmap
restartPolicy: Always
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
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: "off"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "15m"
nginx.ingress.kubernetes.io/proxy-send-timeout: "15m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "15m"
# cert-manager.io/cluster-issuer: vault
name: {{ .Release.Name }}-ingress
spec:
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-svc
port:
number: {{ .Values.docker.port }}
# tls:
# - hosts:
# - {{ .Values.ingress.host }}
# secretName: xcloud-cert-srv
kind: Service
apiVersion: v1
metadata:
name: {{ .Release.Name }}-svc
spec:
type: ClusterIP
ports:
- port: {{ .Values.docker.port }}
targetPort: {{ .Values.docker.targetPort }}
protocol: TCP
name: http
selector:
app: {{ .Release.Name }}
---
image: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
host: blockscout-frontend.aws-k8s.blockscout.com
resources:
limits:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
environment: {}
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS:
_default: [{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true, "chainId": 100},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60", "chainId": 300},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets", "chainId": 200},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets", "chainId": 1},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets", "chainId": 61},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true, "chainId": 99},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets", "chainId": 30},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets", "chainId": 77},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other", "chainId": 246529},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other", "chainId": 22}]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
global:
env: test
/* eslint-disable no-restricted-properties */
const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development';
const baseUrl = [
process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https',
'://',
process.env.NEXT_PUBLIC_VERCEL_URL || process.env.NEXT_PUBLIC_APP_HOST,
process.env.NEXT_PUBLIC_APP_PORT ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '',
].join('');
const apiUrl = [
process.env.NEXT_PUBLIC_API_ENDPOINT || 'https://blockscout.com',
process.env.NEXT_PUBLIC_API_BASE_PATH,
].filter(Boolean).join('');
const config = Object.freeze({
env,
isDev,
network: {
type: process.env.NEXT_PUBLIC_NETWORK_TYPE,
subtype: process.env.NEXT_PUBLIC_NETWORK_SUBTYPE,
logo: process.env.NEXT_PUBLIC_NETWORK_LOGO,
name: process.env.NEXT_PUBLIC_NETWORK_NAME,
id: process.env.NEXT_PUBLIC_NETWORK_ID,
shortName: process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME,
currency: process.env.NEXT_PUBLIC_NETWORK_CURRENCY,
assetsPathname: process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME,
nativeTokenAddress: process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS,
basePath: '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/'),
},
footerLinks: {
github: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK,
twitter: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK,
telegram: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK,
staking: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK,
},
featuredNetworks: process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"'),
blockScoutVersion: process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION,
isAccountSupported: process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?.replaceAll('\'', '"') === 'true',
marketplaceSubmitForm: process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM,
protocol: process.env.NEXT_PUBLIC_APP_PROTOCOL,
host: process.env.NEXT_PUBLIC_APP_HOST,
port: process.env.NEXT_PUBLIC_APP_PORT,
baseUrl,
apiUrl,
});
export default config;
# app config
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_INSTANCE=local
# nav and footer config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
# marketplace config
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
# api config
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
\ No newline at end of file
# nav and footer config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]
# current network config
NEXT_PUBLIC_NETWORK_NAME=POA
NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa
NEXT_PUBLIC_NETWORK_TYPE=poa
NEXT_PUBLIC_NETWORK_SUBTYPE=core
NEXT_PUBLIC_NETWORK_ID=99
NEXT_PUBLIC_NETWORK_CURRENCY=POA
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core
const getCspPolicy = require('../../lib/csp/getCspPolicy');
async function headers() {
return [
{
......@@ -14,10 +12,6 @@ async function headers() {
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Content-Security-Policy-Report-Only',
value: getCspPolicy(),
},
],
},
];
......
const BASE_PATH = require('../../lib/link/basePath.js');
const PATHS = require('../../lib/link/paths.js');
const oldUrls = [
{
oldPath: '/account/tag_address',
newPath: `${ PATHS.private_tags }?tab=address`,
},
{
oldPath: '/account/tag_transaction',
newPath: `${ PATHS.private_tags }?tab=tx`,
},
{
oldPath: '/pending-transactions',
newPath: `${ PATHS.txs }?tab=pending`,
},
{
oldPath: '/tx/:id/internal-transactions',
newPath: `${ PATHS.tx }?tab=internal`,
},
{
oldPath: '/tx/:id/logs',
newPath: `${ PATHS.tx }?tab=logs`,
},
{
oldPath: '/tx/:id/raw-trace',
newPath: `${ PATHS.tx }?tab=raw_trace`,
},
{
oldPath: '/tx/:id/state',
newPath: `${ PATHS.tx }?tab=state`,
},
{
oldPath: '/uncles',
newPath: `${ PATHS.blocks }?tab=uncles`,
},
{
oldPath: '/reorgs',
newPath: `${ PATHS.blocks }?tab=reorgs`,
},
{
oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }?tab=txs`,
},
];
async function redirects() {
const homePagePath = '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/');
return [
{
source: '/',
destination: homePagePath,
permanent: false,
},
...oldUrls.map(item => {
const source = BASE_PATH.replaceAll('[', ':').replaceAll(']', '') + item.oldPath;
const destination = item.newPath.replaceAll('[', ':').replaceAll(']', '');
return { source, destination, permanent: false };
}),
];
}
module.exports = redirects;
const parseNetworkConfig = require('../../lib/networks/parseNetworkConfig');
async function rewrites() {
// there can be networks without subtype
// routing in nextjs allows optional params only at the end of the path
// if there are paths with subtype and subsubtype, we will change the routing
// but so far we think we're ok with this hack
const networksFromConfig = parseNetworkConfig();
return networksFromConfig.filter(n => !n.subType).map(n => ({
source: `/${ n.type }/:slug*`,
destination: `/${ n.type }/mainnet/:slug*`,
}));
//
// UPDATE: as for now I hardcoded all networks without subtype
// because we cannot do proper dynamic rewrites in middleware using runtime ENVs
// see issue - https://github.com/vercel/next.js/discussions/35231
// it seems like it's solved but it's not actually
return [
{ source: '/astar/:slug*', destination: '/astar/mainnet/:slug*' },
{ source: '/shiden/:slug*', destination: '/shiden/mainnet/:slug*' },
];
}
module.exports = rewrites;
import type { NextjsOptions } from '@sentry/nextjs/types/utils/nextjsOptions';
const config: NextjsOptions = {
environment: process.env.VERCEL_ENV || process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
};
export default config;
import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
export const config: Sentry.BrowserOptions = {
environment: process.env.VERCEL_ENV || process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
};
export function configureScope(scope: Sentry.Scope) {
scope.setTag('app_instance', process.env.NEXT_PUBLIC_APP_INSTANCE);
}
[
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "easy-staking",
......@@ -21,7 +21,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "curve",
......@@ -41,7 +41,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "honwyswap",
......@@ -61,7 +61,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "sushi",
......@@ -81,7 +81,7 @@
},
{
"chainIds": [
100
"100"
],
"author": "xDaichain",
"id": "bao-finance",
......@@ -101,7 +101,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "component",
......@@ -121,7 +121,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "pooltogether",
......@@ -141,7 +141,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "swapr",
......@@ -161,7 +161,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "levinswap",
......@@ -181,7 +181,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "omen",
......@@ -201,7 +201,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "nifty-ink",
......@@ -221,7 +221,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "treasure-chess",
......@@ -241,7 +241,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "unique-one",
......@@ -261,7 +261,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "cold-truth-culture",
......@@ -281,7 +281,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "xdai-bridge",
......@@ -301,7 +301,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "omni-bridge",
......@@ -321,7 +321,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "gnosis-safe",
......@@ -341,7 +341,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "multisender",
......@@ -361,7 +361,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "disperse",
......@@ -381,7 +381,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "symmetric",
......
/* eslint-disable max-len */
export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'ok' as Transaction['status'],
block_num: 15006918,
confirmation_num: 283,
confirmation_duration: 30,
timestamp: 1662623567695,
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
type: 'Address',
alias: '',
},
address_to: {
hash: '0x35317007D203b8a86CA727ad44E473E40450E378',
type: 'Contract',
alias: '',
},
amount: {
value: 0.03,
value_usd: 35.5,
},
fee: {
value: 0.002395904453623692,
value_usd: 2.84,
},
gas_price: 0.000000017716513811,
gas_limit: 208420,
gas_used: 159319,
gas_fees: {
base: 13.538410068,
max: 20.27657523,
max_priority: 1.5,
},
burnt_fees: {
value: 0.002156925953623692,
value_usd: 2.55,
},
type: {
value: '2',
eip: 'EIP-1559',
},
nonce: 4,
position: 342,
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: { symbol: 'VIK', hash: '0xADFE00d92e5A16e773891F59780e6e54f40B532e', name: 'Viktor Coin' }, amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848f', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 },
],
txType: 'transaction' as TxType,
};
export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
import type { Transaction } from 'types/api/transaction';
import type { Transaction } from 'types/api/transaction';
import { tx } from './tx';
import type { TxType } from './tx';
export const txs = [
{
...tx,
method: 'Withdraw',
txType: 'transaction' as TxType,
errorText: '',
},
{
...tx,
status: 'error' as Transaction['status'],
errorText: 'Error: (Awaiting internal transactions for reason)',
txType: 'contract-call' as TxType,
method: 'CommitHash CommitHash CommitHash CommitHash',
amount: {
value: 0.04,
value_usd: 35.5,
},
fee: {
value: 0.002295904453623692,
value_usd: 2.84,
},
},
{
...tx,
status: null,
txType: 'token-transfer' as TxType,
method: 'Multicall',
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
alias: 'tkdkdkdkdkdkdkdkdkdkdkdkdkdkd.eth',
type: 'ENS name',
},
amount: {
value: 0.02,
value_usd: 35.5,
},
fee: {
value: 0.002495904453623692,
value_usd: 2.84,
},
errorText: '',
},
];
#!/bin/bash
./replace_envs.sh
echo "starting Nextjs"
exec "$@"
\ No newline at end of file
#!/bin/bash
# no verbose
set +x
# config
envFilename='.env.template'
nextFolder='./.next/'
# replacing build-stage ENVs with run-stage ENVs
# https://raphaelpralat.medium.com/system-environment-variables-in-next-js-with-docker-1f0754e04cde
function replace_envs {
# read all config file
while read line; do
# no comment or not empty
if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then
continue
fi
# split
configName="$(cut -d'=' -f1 <<<"$line")"
configValue="$(cut -d'=' -f2 <<<"$line")"
# get system env
envValue=$(env | grep "^$configName=" | grep -oe '[^=]*$');
# if config found
if [ -n "$configValue" ]; then
# replace all
echo "Replace: ${configValue} with: ${envValue}"
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#${envValue-''}#g"
fi
done < $envFilename
}
replace_envs
\ No newline at end of file
---
creation_rules:
- path_regex: ^(.+/)?secrets\.yaml$
pgp: >-
99E83B7490B1A9F51781E6055317CE0D5CE1230B
blockscout:
environment:
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD:
_default: ENC[AES256_GCM,data:aW52T8giY6Be3LMtiYJ7tJ0=,iv:vbHsPHzC/TI0wxwO1LgUuY7RWHUEpxP17b4NzcxH9Fc=,tag:kqqnsrIb+k8S+FUCAv2Y7Q==,type:str]
MAILSLURP_API_KEY:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID:
_default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:C6TAdc/RZ+qFOan+tWnTsfIDcYgxCNjuKOU=,iv:tEcHTumHxA9LreqLSwjcTTCP3igk/VQrJn/OWd+YQ4c=,tag:XOlWi3XHBZyENtLsv3afmw==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:lojxfDVJBi1Sc6KCL28tln6RJ5leg0jmaTbSHbBd1H4=,iv:l1Iq/YsZKkFuorQgKd+KzAIkqbXHv6I4eid+7LDnyOA=,tag:/c7GBciDCruz+dAOfxncww==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:9cWXw4j6LAg9qUzKTGevJE4U1leMV+i9DD26VDyFqlin4M6O1r/xzoWQtHfrHzxP1RRBuGPIjBqV/WCM1u9cVmhgrs2FQvzZkHk=,iv:Qrxdp4CfnSkCXMxhzMZvvpFSkglzdviq4UYGhffLDNk=,tag:RJhq0+B9Sa5D8k8cAhZW5A==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER:
_default: ENC[AES256_GCM,data:68YVwUmudMo8n+1FENmPunPwLFSCgg==,iv:oALjuhqudA8zfOdse5Hd4IaLvEtY4VJ7exwUGsEPWFA=,tag:8F+rS2HZSfqCCXY4MkdTXw==,type:str]
ACCOUNT_SENDGRID_TEMPLATE:
_default: ENC[AES256_GCM,data:teIknLFnYfGDD7drMUTvcoXBxR9rOjc8L8nOVtMYVOuzXQ==,iv:LoBlBjrWDf5/LsmBo58510evXBTxiIW1YekeLmB+Mow=,tag:dZ/YdphM5MxEygeFYlT0Nw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL:
_default: ENC[AES256_GCM,data:Y5Un1IwYz6luANlNDtbz6V9cdQ96sZFPbtS5MzDBPDFP8pBlQouhYeDBvB6irf2KiC3gYPHLea5lSWA=,iv:QH0A4zi/iWYka9n0SD2v+NXijNPyg0MPrGVawDzf5fU=,tag:wjUYXk943EXXF+++oNpLPw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY:
_default: ENC[AES256_GCM,data:WBxTPe+pQ2VzSPRmL3mzek0=,iv:0wYDv0BRmXrmz9EJg0cBclHpsPEQukO7tCnPAUlCKrs=,tag:3Zcyf/8kLU6ohUQ+97eONg==,type:str]
ACCOUNT_CLOAK_KEY:
_default: ENC[AES256_GCM,data:/+0GBBYW34fDHIAPpHCbNucu2FUH9VAI+wBV6dLHxvYk0whtXikiClentSk=,iv:ekOPgF0H/0/MG++cLi0gH+AzqWafkqFL9R7B8G8jPo4=,tag:QcI69GzicGVV96+EDOR81w==,type:str]
SECRET_KEY_BASE:
_default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str]
SECRET_KEY_GUARDIAN:
_default: ENC[AES256_GCM,data:vpsqZh2sBKUiG4LtTVZyi3qBUU2EMPIrAp9k4X4iSYNvJM7iDGjq/du3EmsQCccvlr54x3opyypHYsdA7gL2Cg==,iv:9q8ePtwr2CDieE2ZU3UyxJ1wtKzIOY1oJ7rZmXCcWWM=,tag:BFO+PztjAOfQHh9mpC/ZJA==,type:str]
DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:48q2fFA3s1HGSm7YB5QV8B0eiI2PZHPgOsUS84R8x+8GecrFTenifbcHD6AivFNKovLuClUy2aFuEtEvJDOfelY=,iv:5ZLl62Yj/AXwyg+pdBLUv3EyYXSTOiSCqGYOv3rBx94=,tag:FUiy0haST4mdrqe2VlvrLg==,type:str]
ACCOUNT_REDIS_URL:
_default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
_default: ENC[AES256_GCM,data:u9zFU9th8ShQ5HIaehYgMzOorx7Tfg7K,iv:ytd6IoU8PIMqbZ4W7kgkL5S4KV4MbvHfxMH2IthQnYE=,tag:0kgXuYSBZZp0LHG3m5EfYg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__SECRET_KEY:
_default: ENC[AES256_GCM,data:t78wGcNs4gqMav1TJaIyBUE4vxsaXloMSmPobix2zvpL2MfNl/6ERA==,iv:IdQot/B7QH9mJWFypTn9OIS+WOeq+B5KZYuYMkQ8+G4=,tag:xXX0WIIM5enPGaibiywtBg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__BUCKET:
_default: ENC[AES256_GCM,data:495QOb9qNhVugnOtaw==,iv:4h9S63+0P5qzeObiu5LJ3YKnFboSJ6WDE8tPnavySxg=,tag:ld3u6nYJH4qv3eN/pon5ow==,type:str]
postgres:
environment:
POSTGRES_PASSWORD:
_default: ENC[AES256_GCM,data:+DPLqOzpvlY=,iv:W6jbwxXI2NTb+kc7pGw0GPUpIPvgIfrNW9Uq5f64gS4=,tag:TV2bMILH/mYmUi0CxeYl2A==,type:str]
POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
geth:
files:
list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
init.sh: ENC[AES256_GCM,data:2qHHfwC63yJlxqaqmQQ69CxtcYZiEgdLdr/Hcxk8eKEou16bXL1NxpxfW8MWsfZyB4S3QNj05V6WSpZAw9ITG/GnXOKocZqKbZPmmuwQpLoekC37EsdUETb+tFvIunh9xRmgst4/ByXEyjC9pZk8SN0Qr9cGQl3VMzeGSkRndaE+qnlFxHRO5ELbDyaLMcyLWaQncescCSvIDpyOPNN7NwS3PYW+Nq3rvh4y+2FBoHUPUMIq46q94+ROnFk5TQsUI5/sVdw/5fuJklcTeqxuo4pPFLAgtkXkab0k2Og0eMI4It1xIrwpfJnxKwbjd0zljKo8tNzjpPZKHMfYchPX6uGH9fnwvXcH7nKUUdtSWrWa8R9a5AyO7SJ0hfzewMzvOWspihJW3JXFTvfesoG/cMMBDg3icsD/0HyxVBQP2riSZsou3hzDFAOVoNLAQTNRVXckSxk9zbx0aOy3DlWefJhIFj2ZmF4sliwxMe+Oy/So6PfiRLsP9v6/OTv6sriExtHdAx6xeA+dbYCO9ruW+5gNB2UUi3DQvpvejaxysJygaHvYCVDGAUKCxDAvQPrUQ/r5+UaE+GmqflDzhd9lhAptL/xL8tW8R+v9RZdMO/G9kxWMhKWiHH6QqgdYGOvanwCxAGtSvKhqf6fiWnNMr6wKG4qAFk6cMNUsLHtnC1qLYhZAfBa1/aK5cG0IbygZKurC+9G8x4SmOqUpW8h23XRsNd8peCRuSZ2uSGqss/1XvngfW0avzGiXMHW9,iv:ldcn5mGmfGmSmq30mUen6diWK6EAEnYF6NQTXXwY8yA=,tag:kSuu7AGzzmSEM5AzjIw5RQ==,type:str]
password.txt: ""
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:IVCGBLqP7IdmbDe9UbIFvJSBD7+g52chKzakELt2XuHDp9JvC4E+7xxp,iv:bDHp2llHAqhgI5N8swQALSDc6X3S0JCsXbJnEEDDJOc=,tag:Z1WsJXkqtq79WydIgUiDiA==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:6MLOmBMJoB+dYoW8L4JYslO3F5tSFzhrkI+6rGo1a51s9sXZa9c=,iv:126unfUWSieigaq4Zne8321tSYoNy3EHk/qwEodqgH8=,tag:6BlrNkck5yANO2rqECjkuQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-14T12:09:11Z"
mac: ENC[AES256_GCM,data:CnD+9hsC9ZyVhZPo+DXZfPH8svMuk50llaAm3JxgOlzhbJ4yp969WxLhZSORvj520b9geBPLZRU7ujLGiHKhrNzAK438LI2QttKQDt3WSbPwkIGDh/zuA201+gpT73awUNfMKCoHVjq4iQ6ty4KP/NCw1ZMcS/c1WVuRYE9RTl8=,iv:rl8eKiXwrBDjns2hiwJ6f28XyuhjH2soHeR1MBBu2Ig=,tag:vu5jGEPMkvmcl7m8huWl7g==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA1MXzg1c4SMLAQgAoRceoDDpqXEbiz6DaMX7YS7j5mcp+xVoemU9qY4ln7dT
XtwAWiRVr7mf1ZDI77bQunbyWAU3zM/lFDsrlNUMAUGZzoOOtIGSnjCqCYB5JGiP
ZfdjG88RAJx+WMIgWl66PW1ceru8We+KauQyG7bD5g59g3b5RadEhH3VER4cYJof
VI0+NPpAcGciRsV3vxGGf4q1ppM3Pz0AnXcC+HB+hxWa5DeAhZlavFO2zYBCzw/o
ypyLCoOcuEDSj9AY+EYnjyXIS72DXPA953/8QSaMSZ9JVKG5imtXslGEdj9PIQOp
0rPqjZCxzydQaPZ68jXMg2Ci4gZT4ZPle9fRFxNGdtJeAScCUk+5L70orvNmWwgD
3fFnMY6tMDd/qSqSRFEJ0Vm0M4MYSg5mgW9M64zGfw1bZLLjsIGMe2ZqQC/sZh1O
Mrk3/xd4md/Ko8BQcaZ6lCi0olz6KWTzmQhXgNTx9A==
=mNH9
-----END PGP MESSAGE-----
fp: 99E83B7490B1A9F51781E6055317CE0D5CE1230B
unencrypted_suffix: _unencrypted
version: 3.7.3
global:
env: e2e
# enable Blockscout deploy
blockscout:
enabled: true
image:
_default: blockscout/blockscout:latest
replicas:
app: 1
docker:
port: 80
targetPort: 4000
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
# probes
livenessProbe:
enabled: true
path: /
readinessProbe:
enabled: true
path: /
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "2"
requests:
memory:
_default: "1Gi"
cpu:
_default: "2"
# enable service to connect to RDS
rds:
enable: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
nodeSelector:
enabled: true
app: blockscout
# Blockscout environment variables
environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546
COIN:
_default: DAI
MIX_ENV:
_default: prod
ECTO_USE_SSL:
_default: 'false'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: true
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true'
postgres:
enabled: true
image: postgres:13.8
port: 5432
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "1"
requests:
memory:
_default: "1Gi"
cpu:
_default: "1"
environment:
POSTGRES_USER:
_default: 'postgres'
POSTGRES_HOST_AUTH_METHOD:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
image:
_default: ethereum/client-go:stable
replicas:
app: 1
portHttp: 8545
portWs: 8546
portAuth: 8551
command: '["sh","./root/init.sh"]'
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]'
environment: {}
persistence:
enabled: false
resources:
limits:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
jwt:
enabled: false
files:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
app: 1
docker:
port: 80
targetPort: 8043
metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
requests:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
app: blockscout
# probes
livenessProbe:
enabled: true
path: /health
readinessProbe:
enabled: true
path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__ADDR:
_default: 0.0.0.0:8043
# SMART_CONTRACT_VERIFIER__SOLIDITY__ENABLED:
# _default: 'true'
SMART_CONTRACT_VERIFIER__SOLIDITY__COMPILERS_DIR:
_default: /tmp/solidity-compilers
SMART_CONTRACT_VERIFIER__SOLIDITY__REFRESH_VERSIONS_SCHEDULE:
_default: 0 0 * * * * *
# It depends on the OS you are running the service on
# SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL:
# _default: https://solc-bin.ethereum.org/linux-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/macosx-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/windows-amd64/list.json
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__REGION:
_default: ""
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ENDPOINT:
_default: https://storage.googleapis.com
SMART_CONTRACT_VERIFIER__SOURCIFY__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__SOURCIFY__API_URL:
_default: https://sourcify.dev/server/
SMART_CONTRACT_VERIFIER__SOURCIFY__VERIFICATION_ATTEMPTS:
_default: 3
SMART_CONTRACT_VERIFIER__SOURCIFY__REQUEST_TIMEOUT:
_default: 10
SMART_CONTRACT_VERIFIER__METRICS__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__METRICS__ADDR:
_default: 0.0.0.0:6060
SMART_CONTRACT_VERIFIER__METRICS__ROUTE:
_default: /metrics
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_APP_PROTOCOL:
_default: http
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_INSTANCE:
_default: local
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
NEXT_PUBLIC_NETWORK_ID:
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: SPOA
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
---
creation_rules:
- path_regex: ^(.+/)?secrets\.yaml$
pgp: >-
99E83B7490B1A9F51781E6055317CE0D5CE1230B
blockscout:
environment:
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD:
_default: ENC[AES256_GCM,data:aW52T8giY6Be3LMtiYJ7tJ0=,iv:vbHsPHzC/TI0wxwO1LgUuY7RWHUEpxP17b4NzcxH9Fc=,tag:kqqnsrIb+k8S+FUCAv2Y7Q==,type:str]
MAILSLURP_API_KEY:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID:
_default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:C6TAdc/RZ+qFOan+tWnTsfIDcYgxCNjuKOU=,iv:tEcHTumHxA9LreqLSwjcTTCP3igk/VQrJn/OWd+YQ4c=,tag:XOlWi3XHBZyENtLsv3afmw==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:lojxfDVJBi1Sc6KCL28tln6RJ5leg0jmaTbSHbBd1H4=,iv:l1Iq/YsZKkFuorQgKd+KzAIkqbXHv6I4eid+7LDnyOA=,tag:/c7GBciDCruz+dAOfxncww==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:9cWXw4j6LAg9qUzKTGevJE4U1leMV+i9DD26VDyFqlin4M6O1r/xzoWQtHfrHzxP1RRBuGPIjBqV/WCM1u9cVmhgrs2FQvzZkHk=,iv:Qrxdp4CfnSkCXMxhzMZvvpFSkglzdviq4UYGhffLDNk=,tag:RJhq0+B9Sa5D8k8cAhZW5A==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER:
_default: ENC[AES256_GCM,data:68YVwUmudMo8n+1FENmPunPwLFSCgg==,iv:oALjuhqudA8zfOdse5Hd4IaLvEtY4VJ7exwUGsEPWFA=,tag:8F+rS2HZSfqCCXY4MkdTXw==,type:str]
ACCOUNT_SENDGRID_TEMPLATE:
_default: ENC[AES256_GCM,data:teIknLFnYfGDD7drMUTvcoXBxR9rOjc8L8nOVtMYVOuzXQ==,iv:LoBlBjrWDf5/LsmBo58510evXBTxiIW1YekeLmB+Mow=,tag:dZ/YdphM5MxEygeFYlT0Nw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL:
_default: ENC[AES256_GCM,data:Y5Un1IwYz6luANlNDtbz6V9cdQ96sZFPbtS5MzDBPDFP8pBlQouhYeDBvB6irf2KiC3gYPHLea5lSWA=,iv:QH0A4zi/iWYka9n0SD2v+NXijNPyg0MPrGVawDzf5fU=,tag:wjUYXk943EXXF+++oNpLPw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY:
_default: ENC[AES256_GCM,data:WBxTPe+pQ2VzSPRmL3mzek0=,iv:0wYDv0BRmXrmz9EJg0cBclHpsPEQukO7tCnPAUlCKrs=,tag:3Zcyf/8kLU6ohUQ+97eONg==,type:str]
ACCOUNT_CLOAK_KEY:
_default: ENC[AES256_GCM,data:/+0GBBYW34fDHIAPpHCbNucu2FUH9VAI+wBV6dLHxvYk0whtXikiClentSk=,iv:ekOPgF0H/0/MG++cLi0gH+AzqWafkqFL9R7B8G8jPo4=,tag:QcI69GzicGVV96+EDOR81w==,type:str]
SECRET_KEY_BASE:
_default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str]
SECRET_KEY_GUARDIAN:
_default: ENC[AES256_GCM,data:vpsqZh2sBKUiG4LtTVZyi3qBUU2EMPIrAp9k4X4iSYNvJM7iDGjq/du3EmsQCccvlr54x3opyypHYsdA7gL2Cg==,iv:9q8ePtwr2CDieE2ZU3UyxJ1wtKzIOY1oJ7rZmXCcWWM=,tag:BFO+PztjAOfQHh9mpC/ZJA==,type:str]
DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:48q2fFA3s1HGSm7YB5QV8B0eiI2PZHPgOsUS84R8x+8GecrFTenifbcHD6AivFNKovLuClUy2aFuEtEvJDOfelY=,iv:5ZLl62Yj/AXwyg+pdBLUv3EyYXSTOiSCqGYOv3rBx94=,tag:FUiy0haST4mdrqe2VlvrLg==,type:str]
ACCOUNT_REDIS_URL:
_default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
_default: ENC[AES256_GCM,data:u9zFU9th8ShQ5HIaehYgMzOorx7Tfg7K,iv:ytd6IoU8PIMqbZ4W7kgkL5S4KV4MbvHfxMH2IthQnYE=,tag:0kgXuYSBZZp0LHG3m5EfYg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__SECRET_KEY:
_default: ENC[AES256_GCM,data:t78wGcNs4gqMav1TJaIyBUE4vxsaXloMSmPobix2zvpL2MfNl/6ERA==,iv:IdQot/B7QH9mJWFypTn9OIS+WOeq+B5KZYuYMkQ8+G4=,tag:xXX0WIIM5enPGaibiywtBg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__BUCKET:
_default: ENC[AES256_GCM,data:495QOb9qNhVugnOtaw==,iv:4h9S63+0P5qzeObiu5LJ3YKnFboSJ6WDE8tPnavySxg=,tag:ld3u6nYJH4qv3eN/pon5ow==,type:str]
postgres:
environment:
POSTGRES_PASSWORD:
_default: ENC[AES256_GCM,data:+DPLqOzpvlY=,iv:W6jbwxXI2NTb+kc7pGw0GPUpIPvgIfrNW9Uq5f64gS4=,tag:TV2bMILH/mYmUi0CxeYl2A==,type:str]
POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
geth:
files:
list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
init.sh: ENC[AES256_GCM,data:2qHHfwC63yJlxqaqmQQ69CxtcYZiEgdLdr/Hcxk8eKEou16bXL1NxpxfW8MWsfZyB4S3QNj05V6WSpZAw9ITG/GnXOKocZqKbZPmmuwQpLoekC37EsdUETb+tFvIunh9xRmgst4/ByXEyjC9pZk8SN0Qr9cGQl3VMzeGSkRndaE+qnlFxHRO5ELbDyaLMcyLWaQncescCSvIDpyOPNN7NwS3PYW+Nq3rvh4y+2FBoHUPUMIq46q94+ROnFk5TQsUI5/sVdw/5fuJklcTeqxuo4pPFLAgtkXkab0k2Og0eMI4It1xIrwpfJnxKwbjd0zljKo8tNzjpPZKHMfYchPX6uGH9fnwvXcH7nKUUdtSWrWa8R9a5AyO7SJ0hfzewMzvOWspihJW3JXFTvfesoG/cMMBDg3icsD/0HyxVBQP2riSZsou3hzDFAOVoNLAQTNRVXckSxk9zbx0aOy3DlWefJhIFj2ZmF4sliwxMe+Oy/So6PfiRLsP9v6/OTv6sriExtHdAx6xeA+dbYCO9ruW+5gNB2UUi3DQvpvejaxysJygaHvYCVDGAUKCxDAvQPrUQ/r5+UaE+GmqflDzhd9lhAptL/xL8tW8R+v9RZdMO/G9kxWMhKWiHH6QqgdYGOvanwCxAGtSvKhqf6fiWnNMr6wKG4qAFk6cMNUsLHtnC1qLYhZAfBa1/aK5cG0IbygZKurC+9G8x4SmOqUpW8h23XRsNd8peCRuSZ2uSGqss/1XvngfW0avzGiXMHW9,iv:ldcn5mGmfGmSmq30mUen6diWK6EAEnYF6NQTXXwY8yA=,tag:kSuu7AGzzmSEM5AzjIw5RQ==,type:str]
password.txt: ""
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:KVzLMvMQc6f37c+OWu7vpAdDYwKI048XVY7EVijS2zz6jRRFacg=,iv:LeWSYZeaPdBsOxcGcca8L1Rp3ilsR+R13icX8Q/VUBA=,tag:Bj6NTTqb3R8oxAprZ+7CVQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-14T12:08:39Z"
mac: ENC[AES256_GCM,data:esHv2aUvW4lMmoD5yRWu4OJEpOMkCa7TOyPS0HDkQL25g4TOdE+AfVZBE5wLeL1rePaId4bHnX0sF2Tov0d8xhCH3mv6+Vvmgi+75Oqu8+logQ4LyZSI0yIcvmdGVHhaO6u3u1qwYXHrityIVmiXQdBck5oq67uyT+jtSh1pXpc=,iv:YNWhRxSY0WLRm+wbVURpfxU2K67MAB5dpSILSMy9oCE=,tag:TsCcsGmMG4id5ITR5LrDjg==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA1MXzg1c4SMLAQgAoRceoDDpqXEbiz6DaMX7YS7j5mcp+xVoemU9qY4ln7dT
XtwAWiRVr7mf1ZDI77bQunbyWAU3zM/lFDsrlNUMAUGZzoOOtIGSnjCqCYB5JGiP
ZfdjG88RAJx+WMIgWl66PW1ceru8We+KauQyG7bD5g59g3b5RadEhH3VER4cYJof
VI0+NPpAcGciRsV3vxGGf4q1ppM3Pz0AnXcC+HB+hxWa5DeAhZlavFO2zYBCzw/o
ypyLCoOcuEDSj9AY+EYnjyXIS72DXPA953/8QSaMSZ9JVKG5imtXslGEdj9PIQOp
0rPqjZCxzydQaPZ68jXMg2Ci4gZT4ZPle9fRFxNGdtJeAScCUk+5L70orvNmWwgD
3fFnMY6tMDd/qSqSRFEJ0Vm0M4MYSg5mgW9M64zGfw1bZLLjsIGMe2ZqQC/sZh1O
Mrk3/xd4md/Ko8BQcaZ6lCi0olz6KWTzmQhXgNTx9A==
=mNH9
-----END PGP MESSAGE-----
fp: 99E83B7490B1A9F51781E6055317CE0D5CE1230B
unencrypted_suffix: _unencrypted
version: 3.7.3
global:
env: e2e
# enable Blockscout deploy
blockscout:
enabled: true
image:
_default: blockscout/blockscout:latest
replicas:
app: 1
docker:
port: 80
targetPort: 4000
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
# probes
livenessProbe:
enabled: true
path: /
readinessProbe:
enabled: true
path: /
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "2"
requests:
memory:
_default: "1Gi"
cpu:
_default: "2"
# enable service to connect to RDS
rds:
enable: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
nodeSelector:
enabled: true
app: blockscout
# Blockscout environment variables
environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546
COIN:
_default: DAI
MIX_ENV:
_default: prod
ECTO_USE_SSL:
_default: 'false'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: true
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true'
postgres:
enabled: true
image: postgres:13.8
port: 5432
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "1"
requests:
memory:
_default: "1Gi"
cpu:
_default: "1"
environment:
POSTGRES_USER:
_default: 'postgres'
POSTGRES_HOST_AUTH_METHOD:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
image:
_default: ethereum/client-go:stable
replicas:
app: 1
portHttp: 8545
portWs: 8546
portAuth: 8551
command: '["sh","./root/init.sh"]'
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]'
environment: {}
persistence:
enabled: false
resources:
limits:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
jwt:
enabled: false
files:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
app: 1
docker:
port: 80
targetPort: 8043
metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
requests:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
app: blockscout
# probes
livenessProbe:
enabled: true
path: /health
readinessProbe:
enabled: true
path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__ADDR:
_default: 0.0.0.0:8043
# SMART_CONTRACT_VERIFIER__SOLIDITY__ENABLED:
# _default: 'true'
SMART_CONTRACT_VERIFIER__SOLIDITY__COMPILERS_DIR:
_default: /tmp/solidity-compilers
SMART_CONTRACT_VERIFIER__SOLIDITY__REFRESH_VERSIONS_SCHEDULE:
_default: 0 0 * * * * *
# It depends on the OS you are running the service on
# SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL:
# _default: https://solc-bin.ethereum.org/linux-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/macosx-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/windows-amd64/list.json
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__REGION:
_default: ""
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ENDPOINT:
_default: https://storage.googleapis.com
SMART_CONTRACT_VERIFIER__SOURCIFY__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__SOURCIFY__API_URL:
_default: https://sourcify.dev/server/
SMART_CONTRACT_VERIFIER__SOURCIFY__VERIFICATION_ATTEMPTS:
_default: 3
SMART_CONTRACT_VERIFIER__SOURCIFY__REQUEST_TIMEOUT:
_default: 10
SMART_CONTRACT_VERIFIER__METRICS__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__METRICS__ADDR:
_default: 0.0.0.0:6060
SMART_CONTRACT_VERIFIER__METRICS__ROUTE:
_default: /metrics
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_APP_PROTOCOL:
_default: http
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_INSTANCE:
_default: local
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
NEXT_PUBLIC_NETWORK_ID:
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: SPOA
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.868 1.55a.377.377 0 0 0-.378 0L3.188 6.363A.377.377 0 0 0 3 6.69v9.622c0 .135.072.26.188.327l8.302 4.811a.377.377 0 0 0 .378 0l8.302-4.811a.377.377 0 0 0 .188-.327V6.69a.377.377 0 0 0-.188-.327l-8.302-4.811ZM3.755 16.095V6.906l7.924-4.592 7.925 4.592v9.188l-7.925 4.592-7.924-4.592ZM8.66 6.972a.377.377 0 0 0-.754 0v6.354l-1.53-4.587a.377.377 0 0 0-.734.12v5.66a.377.377 0 0 0 .754 0v-3.335l1.529 4.586a.377.377 0 0 0 .735-.12V6.973Zm2.824-2.448a.377.377 0 0 1 .386-.003l2.262 1.32a.377.377 0 0 1-.38.652l-2.07-1.208-1.89 1.145v4.693h2.265a.377.377 0 0 1 0 .754H9.792v4.906a.377.377 0 0 1-.754 0V6.217c0-.132.069-.254.182-.323l2.264-1.37Zm3.195 2.06a.377.377 0 0 1 .515-.141l2.333 1.333a.377.377 0 1 1-.375.655l-.567-.324v7.544a.377.377 0 1 1-.755 0V7.676l-1.01-.578a.377.377 0 0 1-.141-.515Z" fill="currentColor"/>
</svg>
import appConfig from 'configs/app/config';
import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch';
......@@ -13,9 +14,9 @@ export default function fetchFactory(_req: NextApiRequest) {
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
};
const url = `https://blockscout.com${ path }`;
const url = new URL(path, appConfig.apiUrl);
return nodeFetch(url, {
return nodeFetch(url.toString(), {
headers,
...init,
});
......
import * as Sentry from '@sentry/nextjs';
// import * as Sentry from '@sentry/nextjs';
import type { NextApiRequest } from 'next';
import * as cookies from 'lib/cookies';
......@@ -7,9 +7,11 @@ export default function getUrlWithNetwork(_req: NextApiRequest, path: string) {
const networkType = _req.cookies[cookies.NAMES.NETWORK_TYPE];
const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE];
if (!networkType) {
Sentry.captureException(new Error('Incorrect network'), { extra: { networkType, networkSubType } });
}
// if (!networkType) {
// TODO setup sentry for NodeJS
// we probably do not need to if we will do api request from client directly
// Sentry.captureException(new Error('Incorrect network'), { extra: { networkType, networkSubType } });
// }
return `/${ networkType }${ networkSubType ? '/' + networkSubType : '' }/${ path }`;
}
import { withSentry } from '@sentry/nextjs';
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
......@@ -42,5 +41,5 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
res.status(500).json(responseError);
};
return withSentry(handler);
return handler;
}
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;
}
const getMarketplaceApps = require('../getMarketplaceApps');
const parseNetworkConfig = require('../networks/parseNetworkConfig');
import appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks';
import getMarketplaceApps from '../getMarketplaceApps';
const KEY_WORDS = {
BLOB: 'blob:',
......@@ -12,16 +15,18 @@ const KEY_WORDS = {
UNSAFE_EVAL: '\'unsafe-eval\'',
};
const MAIN_DOMAINS = [ '*.blockscout.com', 'blockscout.com' ];
const isDev = process.env.NODE_ENV === 'development';
const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
// eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssets() {
const icons = parseNetworkConfig()
const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string')
.map(({ icon }) => new URL(icon));
.map(({ icon }) => new URL(icon as string));
const logo = appConfig.network.logo ? new URL(appConfig.network.logo) : undefined;
return icons;
return logo ? icons.concat(logo) : icons;
}
function getMarketplaceAppsOrigins() {
......@@ -44,7 +49,7 @@ function makePolicyMap() {
KEY_WORDS.SELF,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason
isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// client error monitoring
'sentry.io', '*.sentry.io',
......@@ -55,7 +60,7 @@ function makePolicyMap() {
// next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
isDev ? KEY_WORDS.UNSAFE_EVAL : '',
appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS,
......@@ -110,11 +115,13 @@ function makePolicyMap() {
KEY_WORDS.NONE,
],
'frame-src': getMarketplaceAppsOrigins(),
...(REPORT_URI ? {
'report-uri': [
process.env.SENTRY_CSP_REPORT_URI,
REPORT_URI,
],
'frame-src': getMarketplaceAppsOrigins(),
} : {}),
};
}
......@@ -135,4 +142,4 @@ function getCspPolicy() {
return policyString;
}
module.exports = getCspPolicy;
export default getCspPolicy;
// should be CommonJS module since it used for next.config.js
const data = require('../data/marketplaceApps.json');
function getMarketplaceApps() {
return data;
}
module.exports = getMarketplaceApps;
import data from 'data/marketplaceApps.json';
export default function getMarketplaceApps() {
return data;
}
import BigNumber from 'bignumber.js';
import { WEI, GWEI } from 'lib/consts';
export default function getValueWithUnit(value: string | number, unit: 'wei' | 'gwei' | 'ether' = '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;
}
export default function hexToAddress(hex: string) {
const shortenHex = hex.slice(0, 66);
return shortenHex.slice(0, 2) + shortenHex.slice(26);
}
export default function hexToBytes(hex: string) {
const bytes = [];
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substring(c, c + 2), 16));
}
return bytes;
}
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);
}
import * as Sentry from '@sentry/react';
import { config, configureScope } from 'configs/sentry/react';
import React from 'react';
export default function useConfigSentry() {
React.useEffect(() => {
// gotta init sentry in browser
Sentry.init(config);
Sentry.configureScope(configureScope);
}, []);
}
import * as Sentry from '@sentry/nextjs';
import * as Sentry from '@sentry/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -29,6 +29,12 @@ export default function useFetch() {
return fetch(path, reqParams).then(response => {
if (!response.ok) {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
......@@ -36,12 +42,6 @@ export default function useFetch() {
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
......
import appConfig from 'configs/app/config';
import React, { useMemo } from 'react';
import marketplaceApps from 'data/marketplaceApps.json';
......@@ -12,27 +13,22 @@ import publicTagIcon from 'icons/publictags.svg';
import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
import useNetwork from './useNetwork';
export default function useNavItems() {
const selectedNetwork = useNetwork();
const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainIds.includes(selectedNetwork?.chainId)),
[ selectedNetwork?.chainId ])
marketplaceApps.filter(item => item.chainIds.includes(appConfig.network.id)),
[ ])
.length > 0;
const link = useLink();
const currentRoute = useCurrentRoute()();
return React.useMemo(() => {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') },
{ text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' } : null,
......@@ -44,7 +40,7 @@ export default function useNavItems() {
const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
{ text: 'Private tags', url: link('private_tags_address'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' },
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' },
......@@ -53,5 +49,5 @@ export default function useNavItems() {
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' };
return { mainNavItems, accountNavItems, profileItem };
}, [ isMarketplaceFilled, link, currentRoute ]);
}, [ isMarketplaceFilled, currentRoute ]);
}
import { useRouter } from 'next/router';
import findNetwork from 'lib/networks/findNetwork';
export default function useNetwork() {
const router = useRouter();
const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_sub_type === 'string' ? router.query.network_sub_type : undefined,
});
return selectedNetwork;
}
const BASE_PATH = '/[network_type]/[network_sub_type]';
module.exports = BASE_PATH;
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags_address', 'private_tags_tx', 'public_tags', 'api_keys', 'custom_abi' ];
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags', 'public_tags', 'api_keys', 'custom_abi' ];
import type { RouteName } from 'lib/link/routes';
export default function isAccountRoute(route: RouteName) {
......
import isBrowser from 'lib/isBrowser';
import findNetwork from 'lib/networks/findNetwork';
import appConfig from 'configs/app/config';
import { ROUTES } from './routes';
import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g;
export function link(routeName: RouteName, urlParams?: Record<string, Array<string> | string | undefined>, queryParams?: Record<string, string>): string {
export default function link(
routeName: RouteName,
urlParams?: Record<string, Array<string> | string | undefined>,
queryParams?: Record<string, string>,
): string {
const route = ROUTES[routeName];
if (!route) {
return '';
}
const network = findNetwork({
network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_type : '',
network_sub_type: typeof urlParams?.network_sub_type === 'string' ? urlParams?.network_sub_type : undefined,
});
const refinedUrlParams: typeof urlParams = {
network_type: appConfig.network.type,
network_sub_type: appConfig.network.subtype,
...urlParams,
};
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !network?.subType) {
if (paramName === 'network_sub_type' && !refinedUrlParams.network_sub_type) {
return '';
}
let paramValue = urlParams?.[paramName];
let paramValue = refinedUrlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dun't know how to manage it, fix me if you find an issue
// dunno know how to manage it, fix me if you find an issue
paramValue = paramValue.join(',');
}
return paramValue ? `/${ paramValue }` : '';
});
const url = new URL(path, isBrowser() ? window.location.origin : 'https://blockscout.com');
const url = new URL(path, appConfig.baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
......
const BASE_PATH = require('./basePath');
const paths = {
network_index: `${ BASE_PATH }`,
watchlist: `${ BASE_PATH }/account/watchlist`,
private_tags: `${ BASE_PATH }/account/tag_address`,
public_tags: `${ BASE_PATH }/account/public_tags_request`,
api_keys: `${ BASE_PATH }/account/api_key`,
custom_abi: `${ BASE_PATH }/account/custom_abi`,
profile: `${ BASE_PATH }/auth/profile`,
txs: `${ BASE_PATH }/txs`,
tx: `${ BASE_PATH }/tx/[id]`,
blocks: `${ BASE_PATH }/blocks`,
block: `${ BASE_PATH }/block/[id]`,
tokens: `${ BASE_PATH }/tokens`,
token_index: `${ BASE_PATH }/token/[hash]`,
token_instance_item: `${ BASE_PATH }/token/[hash]/instance/[id]`,
address_index: `${ BASE_PATH }/address/[id]`,
address_contract_verification: `${ BASE_PATH }/address/[id]/contract_verifications/new`,
apps: `${ BASE_PATH }/apps`,
app_index: `${ BASE_PATH }/apps/[id]`,
search_results: `${ BASE_PATH }/search-results`,
other: `${ BASE_PATH }/search-results`,
// no slash required, it is correct
auth: `${ BASE_PATH }auth/auth0`,
};
module.exports = paths;
import appConfig from 'configs/app/config';
import PATHS from './paths.js';
export interface Route {
pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
......@@ -5,128 +9,95 @@ export interface Route {
export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/[network_type]/[network_sub_type]';
export const ROUTES = {
// NETWORK MAIN PAGE
network_index: {
pattern: `${ BASE_PATH }`,
pattern: PATHS.network_index,
crossNetworkNavigation: true,
},
// ACCOUNT
watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
pattern: PATHS.watchlist,
},
private_tags_address: {
pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
},
private_tags_tx: {
pattern: `${ BASE_PATH }/account/tag_transaction`,
crossNetworkNavigation: true,
private_tags: {
pattern: PATHS.private_tags,
},
public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`,
crossNetworkNavigation: true,
pattern: PATHS.public_tags,
},
api_keys: {
pattern: `${ BASE_PATH }/account/api_key`,
crossNetworkNavigation: true,
pattern: PATHS.api_keys,
},
custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`,
crossNetworkNavigation: true,
pattern: PATHS.custom_abi,
},
profile: {
pattern: `${ BASE_PATH }/auth/profile`,
crossNetworkNavigation: true,
pattern: PATHS.profile,
},
// TRANSACTIONS
txs_validated: {
pattern: `${ BASE_PATH }/txs`,
crossNetworkNavigation: true,
},
txs_pending: {
pattern: `${ BASE_PATH }/pending-transactions`,
txs: {
pattern: PATHS.txs,
crossNetworkNavigation: true,
},
tx_index: {
pattern: `${ BASE_PATH }/tx/[id]`,
},
tx_internal: {
pattern: `${ BASE_PATH }/tx/[id]/internal-transactions`,
},
tx_logs: {
pattern: `${ BASE_PATH }/tx/[id]/logs`,
},
tx_raw_trace: {
pattern: `${ BASE_PATH }/tx/[id]/raw-trace`,
},
tx_state: {
pattern: `${ BASE_PATH }/tx/[id]/state`,
tx: {
pattern: PATHS.tx,
},
// BLOCKS
blocks: {
pattern: `${ BASE_PATH }/blocks`,
crossNetworkNavigation: true,
},
blocks_uncles: {
pattern: `${ BASE_PATH }/uncles`,
pattern: PATHS.blocks,
crossNetworkNavigation: true,
},
blocks_reorgs: {
pattern: `${ BASE_PATH }/reorgs`,
crossNetworkNavigation: true,
},
block_index: {
pattern: `${ BASE_PATH }/block/[id]`,
},
block_txs: {
pattern: `${ BASE_PATH }/block/[id]/transactions`,
block: {
pattern: PATHS.block,
},
// TOKENS
tokens: {
pattern: `${ BASE_PATH }/tokens`,
pattern: PATHS.tokens,
crossNetworkNavigation: true,
},
token_index: {
pattern: `${ BASE_PATH }/token/[id]`,
pattern: PATHS.token_index,
crossNetworkNavigation: true,
},
token_instance_item: {
pattern: PATHS.token_instance_item,
},
// ADDRESSES
address_index: {
pattern: `${ BASE_PATH }/address/[id]`,
pattern: PATHS.address_index,
crossNetworkNavigation: true,
},
address_contract_verification: {
pattern: PATHS.address_contract_verification,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: `${ BASE_PATH }/apps`,
pattern: PATHS.apps,
},
app_index: {
pattern: `${ BASE_PATH }/apps/[id]`,
pattern: PATHS.app_index,
},
// SEARCH
search_results: {
pattern: `${ BASE_PATH }/apps`,
pattern: PATHS.search_results,
},
// ??? what URL will be here
other: {
pattern: `${ BASE_PATH }/search-results`,
pattern: PATHS.other,
},
// AUTH
auth: {
// no slash required, it is correct
pattern: `${ BASE_PATH }auth/auth0`,
pattern: PATHS.auth,
},
};
......@@ -142,6 +113,6 @@ function checkRoutes(route: Record<string, Route>) {
return route;
}
if (process.env.NODE_ENV === 'development') {
if (appConfig.isDev) {
checkRoutes(ROUTES);
}
import { useRouter } from 'next/router';
import React from 'react';
import { link } from 'lib/link/link';
type LinkBuilderParams = Parameters<typeof link>;
export default function useLink() {
const router = useRouter();
const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type;
return React.useCallback((...args: LinkBuilderParams) => {
return link(args[0], { network_type: networkType, network_sub_type: networkSubType, ...args[1] }, args[2]);
}, [ networkType, networkSubType ]);
}
import type { Network } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
import parseNetworkConfig from './parseNetworkConfig';
// will change later when we agree how to host network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': gnosisIcon,
'xdai/optimism': optimismIcon,
'xdai/aox': arbitrumIcon,
'eth/mainnet': ethereumIcon,
'etc/mainnet': ethereumClassicIcon,
'poa/core': poaIcon,
'rsk/mainnet': rskIcon,
'xdai/testnet': arbitrumIcon,
'poa/sokol': poaSokolIcon,
'artis/sigma1': artisIcon,
};
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'eth/mainnet': require('icons/networks/logos/eth.svg'),
'etc/mainnet': require('icons/networks/logos/etc.svg'),
'poa/core': require('icons/networks/logos/poa.svg'),
'rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'poa/sokol': require('icons/networks/logos/sokol.svg'),
'artis/sigma1': require('icons/networks/logos/artis.svg'),
'lukso/l14': require('icons/networks/logos/lukso.svg'),
astar: require('icons/networks/logos/astar.svg'),
shiden: require('icons/networks/logos/shiden.svg'),
shibuya: require('icons/networks/logos/shibuya.svg'),
};
const NETWORKS: Array<Network> = (() => {
const networksFromConfig: Array<Network> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
logo: network.logo || LOGOS[network.type + (network.subType ? `/${ network.subType }` : '')],
icon: network.icon || ICONS[network.type + (network.subType ? `/${ network.subType }` : '')],
}));
})();
export default NETWORKS;
// for easy env creation
// const FOR_CONFIG = [
// {
// name: 'Gnosis Chain',
// type: 'xdai',
// subType: 'mainnet',
// group: 'mainnets',
// isAccountSupported: true,
// chainId: 100,
// currency: 'xDAI',
// },
// {
// name: 'Optimism on Gnosis Chain',
// shortName: 'OoG',
// type: 'xdai',
// subType: 'optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// chainId: 300,
// currency: 'xDAI',
// },
// {
// name: 'Arbitrum on xDai',
// type: 'xdai',
// subType: 'aox',
// group: 'mainnets',
// chainId: 200,
// currency: 'xDAI',
// },
// {
// name: 'Ethereum',
// shortName: 'ETH',
// type: 'eth',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 1,
// currency: 'ETH',
// },
// {
// name: 'Ethereum Classic',
// shortName: 'ETC',
// type: 'etc',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 61,
// currency: 'ETC',
// },
// {
// name: 'POA',
// shortName: 'POA',
// type: 'poa',
// subType: 'core',
// group: 'mainnets',
// chainId: 99,
// currency: 'POA',
// isAccountSupported: true,
// nativeTokenAddress: '0x029a799563238d0e75e20be2f4bda0ea68d00172',
// },
// {
// name: 'RSK',
// shortName: 'RBTC',
// type: 'rsk',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 30,
// currency: 'RBTC',
// },
// {
// name: 'Gnosis Chain Testnet',
// type: 'xdai',
// subType: 'testnet',
// group: 'testnets',
// isAccountSupported: true,
// currency: 'xDAI',
// },
// {
// name: 'POA Sokol',
// shortName: 'POA',
// type: 'poa',
// subType: 'sokol',
// group: 'testnets',
// chainId: 77,
// currency: 'SPOA',
// },
// {
// name: 'ARTIS Σ1',
// type: 'artis',
// subType: 'sigma1',
// group: 'other',
// chainId: 246529,
// currency: 'ATS',
// },
// {
// name: 'LUKSO L14',
// shortName: 'POA',
// type: 'lukso',
// subType: 'l14',
// group: 'other',
// chainId: 22,
// currency: 'LYX',
// },
// {
// name: 'Astar',
// type: 'astar',
// group: 'other',
// chainId: 22,
// currency: 'ASTR',
// },
// ];
import appConfig from 'configs/app/config';
import type { FeaturedNetwork } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': gnosisIcon,
'/xdai/optimism': optimismIcon,
'/xdai/aox': arbitrumIcon,
'/eth/mainnet': ethereumIcon,
'/etc/mainnet': ethereumClassicIcon,
'/poa/core': poaIcon,
'/rsk/mainnet': rskIcon,
'/xdai/testnet': arbitrumIcon,
'/poa/sokol': poaSokolIcon,
'/artis/sigma1': artisIcon,
};
// for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([
// {
// title: 'Gnosis Chain',
// basePath: '/xdai/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Optimism on Gnosis Chain',
// basePath: '/xdai/optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// },
// {
// title: 'Arbitrum on xDai',
// basePath: '/xdai/aox',
// group: 'mainnets',
// },
// {
// title: 'Ethereum',
// basePath: '/eth/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Ethereum Classic',
// basePath: '/etx/mainnet',
// group: 'mainnets',
// },
// {
// title: 'POA',
// basePath: '/poa/core',
// group: 'mainnets',
// },
// {
// title: 'RSK',
// basePath: '/rsk/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Gnosis Chain Testnet',
// basePath: '/xdai/testnet',
// group: 'testnets',
// },
// {
// title: 'POA Sokol',
// basePath: '/poa/sokol',
// group: 'testnets',
// },
// {
// title: 'ARTIS Σ1',
// basePath: '/artis/sigma1',
// group: 'other',
// },
// {
// title: 'LUKSO L14',
// basePath: '/lukso/l14',
// group: 'other',
// },
// {
// title: 'Astar',
// basePath: '/astar',
// group: 'other',
// },
// ]).replaceAll('"', '\'');
function parseNetworkConfig() {
try {
return JSON.parse(appConfig.featuredNetworks || '[]');
} catch (error) {
return [];
}
}
const featuredNetworks: Array<FeaturedNetwork> = (() => {
const networksFromConfig: Array<FeaturedNetwork> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
icon: network.icon || ICONS[network.basePath],
}));
})();
export default featuredNetworks;
import availableNetworks from 'lib/networks/availableNetworks';
interface Params {
network_type: string;
network_sub_type?: string;
}
export default function findNetwork(params: Params) {
return availableNetworks.find((network) =>
network.type === params.network_type &&
network.subType ? network.subType === params.network_sub_type : network.type === params.network_type,
);
}
import NETWORKS from './availableNetworks';
export default function getAvailablePaths() {
return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType || 'mainnet' } }));
}
import findNetwork from './findNetwork';
import appConfig from 'configs/app/config';
export default function getNetworkTitle({ network_type: type, network_sub_type: subType }: {network_type?: string; network_sub_type?: string}) {
const currentNetwork = findNetwork({ network_type: type || '', network_sub_type: subType });
if (currentNetwork) {
return currentNetwork.name + (currentNetwork.shortName ? ` (${ currentNetwork.shortName })` : '') + ' Explorer';
}
return '';
export default function getNetworkTitle() {
return appConfig.network.name + (appConfig.network.shortName ? ` (${ appConfig.network.shortName })` : '') + ' Explorer';
}
// should be CommonJS module since it used for next.config.js
function parseNetworkConfig() {
try {
return JSON.parse(process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS || '[]');
} catch (error) {
return [];
}
}
module.exports = parseNetworkConfig;
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import React from 'react';
import useNetwork from 'lib/hooks/useNetwork';
import isAccountRoute from 'lib/link/isAccountRoute';
import { link } from 'lib/link/link';
import link from 'lib/link/link';
import { ROUTES } from 'lib/link/routes';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import NETWORKS from 'lib/networks/availableNetworks';
import featuredNetworks from 'lib/networks/featuredNetworks';
export default function useNetworkNavigationItems() {
const selectedNetwork = useNetwork();
const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName];
const router = useRouter();
const isAccount = isAccountRoute(currentRouteName);
return React.useMemo(() => {
return NETWORKS.map((network) => {
const routeName = (() => {
if ('crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation) {
if ((isAccount && network.isAccountSupported) || !isAccount) {
return currentRouteName;
}
}
return 'network_index';
})();
const url = link(routeName, { ...router.query, network_type: network.type, network_sub_type: network.subType });
return featuredNetworks.map((network) => {
const routeName = 'crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation ? currentRouteName : 'network_index';
const [ , networkType, networkSubtype ] = network.basePath.split('/');
const url = link(routeName, { ...router.query, network_type: networkType, network_sub_type: networkSubtype });
return {
...network,
url: url,
isActive: selectedNetwork?.type === network.type && selectedNetwork?.subType === network?.subType,
isActive: appConfig.network.basePath === network.basePath,
};
});
}, [ currentRoute, currentRouteName, isAccount, router.query, selectedNetwork?.subType, selectedNetwork?.type ]);
}, [ currentRoute, currentRouteName, router.query ]);
}
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
......@@ -5,16 +5,14 @@ import React from 'react';
import type { PageParams } from './types';
import Block from 'ui/pages/Block';
import type { Props as BlockProps } from 'ui/pages/Block';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
tab: BlockProps['tab'];
}
const BlockNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
const BlockNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
......@@ -22,7 +20,7 @@ const BlockNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block tab={ tab }/>
<Block/>
</>
);
};
......
......@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
const networkTitle = getNetworkTitle();
return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '',
......
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
......@@ -2,26 +2,18 @@ import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from './types';
import Blocks from 'ui/pages/Blocks';
import type { Props as BlocksProps } from 'ui/pages/Blocks';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
tab: BlocksProps['tab'];
}
const BlocksNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
const { title } = getSeo(pageParams);
const BlocksNextPage: NextPage = () => {
const { title } = getSeo();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Blocks tab={ tab }/>
<Blocks/>
</>
);
};
......
import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
export default function getSeo() {
return {
title: params ? `${ networkTitle } - BlockScout` : '',
title: getNetworkTitle(),
};
}
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
return { paths: [], fallback: 'blocking' };
};
......@@ -4,17 +4,15 @@ import React from 'react';
import type { PageParams } from './types';
import type { Props as TransactionProps } from 'ui/pages/Transaction';
import Transaction from 'ui/pages/Transaction';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
tab: TransactionProps['tab'];
}
const TransactionNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
const TransactionNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
......@@ -22,7 +20,7 @@ const TransactionNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction tab={ tab }/>
<Transaction/>
</>
);
};
......
......@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
const networkTitle = getNetworkTitle();
return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '',
......
const { NextResponse } = require('next/server');
const { NAMES } = require('lib/cookies');
const { link } = require('lib/link/link');
const findNetwork = require('lib/networks/findNetwork').default;
export function middleware(req) {
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
const selectedNetwork = findNetwork(networkParams);
if (selectedNetwork) {
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (!apiToken) {
const authUrl = link('auth', networkParams);
return NextResponse.redirect(authUrl);
}
}
}
export const config = {
matcher: '/:network_type/:network_sub_type/account/:path*',
};
import appConfig from 'configs/app/config';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy';
import link from 'lib/link/link';
const cspPolicy = getCspPolicy();
export function middleware(req: NextRequest) {
const isPageRequest = req.headers.get('accept')?.includes('text/html');
if (!isPageRequest) {
return;
}
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
if (appConfig.network.type !== networkType && appConfig.network.subtype !== networkSubtype) {
const url = req.nextUrl.clone();
url.pathname = `/404`;
return NextResponse.rewrite(url);
}
// 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/');
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (isAccountRoute && !apiToken) {
const authUrl = link('auth', networkParams);
return NextResponse.redirect(authUrl);
}
const res = NextResponse.next();
res.headers.append('Content-Security-Policy-Report-Only', cspPolicy);
return res;
}
/**
* Configure which routes should pass through the Middleware.
* Exclude all `_next` urls.
*/
export const config = {
matcher: [ '/', '/:notunderscore((?!_next).+)' ],
};
const { withSentryConfig } = require('@sentry/nextjs');
const withReactSvg = require('next-react-svg');
const path = require('path');
const headers = require('./configs/nextjs/headers');
const redirects = require('./configs/nextjs/redirects');
const rewrites = require('./configs/nextjs/rewrites');
const moduleExports = {
......@@ -18,36 +18,14 @@ const moduleExports = {
return config;
},
async redirects() {
return [
{
source: '/',
destination: '/poa/core',
permanent: false,
},
];
},
headers,
// 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',
sentry: {
hideSourceMaps: true,
},
};
const sentryWebpackPluginOptions = {
// Additional config options for the Sentry Webpack plugin. Keep in mind that
// the following options are set automatically, and overriding them is not
// recommended:
// release, url, org, project, authToken, configFile, stripPrefix,
// urlPrefix, include, ignore
silent: true, // Suppresses all logs
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options.
deploy: {
env: process.env.VERCEL_ENV || process.env.NODE_ENV,
},
};
module.exports = withReactSvg(withSentryConfig(moduleExports, sentryWebpackPluginOptions));
module.exports = withReactSvg(moduleExports);
......@@ -9,11 +9,18 @@
},
"scripts": {
"dev": "next dev",
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev",
"build": "next build",
"build:vercel": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.secrets blockscout",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons"
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test-ct": "playwright test -c playwright-ct.config.ts",
"test-docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.27.0-focal ./run-tests.sh"
},
"dependencies": {
"@chakra-ui/react": "2.3.1",
......@@ -21,6 +28,8 @@
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@sentry/nextjs": "^7.12.1",
"@sentry/react": "^7.13.0",
"@sentry/tracing": "^7.13.0",
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"@types/react-scroll": "^1.8.4",
......@@ -42,10 +51,12 @@
"use-font-face-observer": "^1.2.1"
},
"devDependencies": {
"@playwright/experimental-ct-react": "^1.26.1",
"@types/node": "17.0.36",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"dotenv-cli": "^6.0.0",
"eslint": "8.16.0",
"eslint-config-next": "^12.3.0",
"eslint-plugin-es5": "^1.5.0",
......@@ -53,6 +64,7 @@
"eslint-plugin-regexp": "^1.7.0",
"husky": "^8.0.0",
"lint-staged": ">=10",
"playwright": "^1.26.1",
"svgo": "^2.8.0",
"typescript": "4.7.2"
},
......
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const ApiKeysPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
export default ApiKeysPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const CustomAbiPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
export default CustomAbiPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const PublicTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default PublicTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,17 +14,17 @@ type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<PrivateTags tab="private_tags_address"/>
<PrivateTags/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PrivateTags from 'ui/pages/PrivateTags';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
return (
<>
<Head><title>{ title }</title></Head>
<PrivateTags tab="private_tags_tx"/>
</>
);
};
export default TransactionTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const WatchListPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head>
......@@ -28,5 +28,5 @@ const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
export default WatchListPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,3 +17,6 @@ const AppsPage = () => {
};
export default AppsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -47,5 +47,5 @@ const AppPage: NextPage = () => {
export default AppPage;
export { getStaticPaths } from 'lib/next/apps/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -11,11 +11,11 @@ type Props = {
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<BlockNextPage tab="block_txs" pageParams={ pageParams }/>
<BlockNextPage pageParams={ pageParams }/>
);
};
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import BlockNextPage from 'lib/next/block/BlockNextPage';
type Props = {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<BlockNextPage tab="block_index" pageParams={ pageParams }/>
);
};
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -9,13 +9,13 @@ type Props = {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
const BlockPage: NextPage<Props> = () => {
return (
<BlocksNextPage tab="blocks" pageParams={ pageParams }/>
<BlocksNextPage/>
);
};
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const HomePage: NextPage = () => {
export default HomePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
return (
<>
<Head><title>{ title }</title></Head>
<Transactions tab="txs_pending"/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage';
type Props = {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<BlocksNextPage tab="blocks_reorgs" pageParams={ pageParams }/>
);
};
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -11,11 +11,11 @@ type Props = {
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<TransactionNextPage tab="tx_index" pageParams={ pageParams }/>
<TransactionNextPage pageParams={ pageParams }/>
);
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="tx_internal"/>;
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="tx_logs"/>;
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="tx_raw_trace"/>;
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="tx_state"/>;
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,17 +14,17 @@ type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<Transactions tab="txs_validated"/>
<Transactions/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage';
type Props = {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<BlocksNextPage tab="blocks_uncles" pageParams={ pageParams }/>
);
};
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -4,10 +4,13 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
queries: {
......
......@@ -17,6 +17,7 @@
*/
import * as Sentry from '@sentry/nextjs';
import sentryConfig from 'configs/sentry/nextjs';
import type { NextPageContext } from 'next';
import NextErrorComponent from 'next/error';
import React from 'react';
......@@ -34,6 +35,8 @@ const CustomErrorComponent = (props: { statusCode: number }) => {
};
CustomErrorComponent.getInitialProps = async(contextData: ContextOrProps) => {
Sentry.init(sentryConfig);
// In case this is running in a serverless function, await this in order to give Sentry
// time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(contextData);
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
const searchParamsStr = new URLSearchParams(searchParams).toString();
return `/v2/transactions/${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './',
testMatch: /.*\.pw\.tsx/,
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: Boolean(process.env.CI),
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
headless: true,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
};
export default config;
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from '../theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/playwright/index.ts"></script>
</body>
</html>
// Import styles, initialize component theme here.
// import '../src/common.css';
export {};
#!/bin/sh
yarn install
yarn test-ct
\ No newline at end of file
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.NODE_ENV;
Sentry.init({
environment: ENV,
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
defaults.url=https://sentry.io/
defaults.org=block-scout
defaults.project=new-ui
cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.VERCEL_ENV || process.env.NODE_ENV;
Sentry.init({
environment: ENV,
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
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;
......@@ -12,6 +12,7 @@ import Modal from './Modal';
import Popover from './Popover';
import Radio from './Radio';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Table from './Table';
import Tabs from './Tabs';
import Tag from './Tag';
......@@ -34,6 +35,7 @@ const components = {
Popover,
Radio,
Skeleton,
Spinner,
Tabs,
Table,
Tag,
......
import type { AddressParam } from './addressParams';
export interface TokenTransfer {
type: string;
export type ERC1155TotalPayload = {
value: string;
token_id: string;
}
export type TokenTransfer = (
{
token_type: 'ERC-20';
total: {
value: string;
};
} |
{
token_type: 'ERC-721';
total: {
token_id: string;
};
} |
{
token_type: 'ERC-1155';
total: ERC1155TotalPayload | Array<ERC1155TotalPayload>;
}
) & TokenTransferBase
interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
txHash: string;
from: AddressParam;
to: AddressParam;
token_address: string;
token_symbol: string;
token_type: string;
total: {
value: string;
};
exchange_rate: string;
}
......@@ -3,22 +3,27 @@ import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
raw: string;
decoded: string;
} | DecodedInput;
export interface Transaction {
hash: string;
result: string;
confirmations: number;
status: 'ok' | 'error' | null;
block: number;
timestamp: string;
block: number | null;
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: number;
value: string;
fee: Fee;
gas_price: number;
type: number;
gas_used: string;
gas_used: string | null;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
......@@ -27,10 +32,7 @@ export interface Transaction {
tx_burnt_fee: number | null;
nonce: number;
position: number;
revert_reason: {
raw: string;
decoded: string;
} | null;
revert_reason: TransactionRevertReason | null;
raw_input: string;
decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null;
......
......@@ -26,7 +26,7 @@ export type AppItemPreview = {
}
export type AppItemOverview = AppItemPreview & {
chainIds: Array<number>;
chainIds: Array<string>;
author: string;
url: string;
description: string;
......
......@@ -2,18 +2,9 @@ import type { FunctionComponent, SVGAttributes } from 'react';
export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export interface Network {
name: string;
chainId: number; // https://chainlist.org/
currency: string;
nativeTokenAddress: string;
shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string;
subType?: string;
group: 'mainnets' | 'testnets' | 'other';
export interface FeaturedNetwork {
title: string;
basePath: string;
group: NetworkGroup;
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
logo?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean;
assetsNamePath?: string;
}
export type Unit = 'wei' | 'gwei' | 'ether';
......@@ -8,7 +8,7 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
......@@ -39,8 +39,6 @@ const AppCard = ({ id,
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
const link = useLink();
return (
<LinkBox
_hover={{
......
......@@ -15,7 +15,7 @@ import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
......@@ -45,8 +45,6 @@ const AppModal = ({
categories,
} = marketplaceApps.find(app => app.id === id) as AppItemOverview;
const link = useLink();
const socialLinks = [
telegram ? {
icon: tgIcon,
......
import appConfig from 'configs/app/config';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import useNetwork from 'lib/hooks/useNetwork';
const favoriteAppsLocalStorageKey = 'favoriteApps';
......@@ -27,7 +27,6 @@ function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOv
}
export default function useMarketplaceApps() {
const selectedNetwork = useNetwork();
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
......@@ -80,18 +79,14 @@ export default function useMarketplaceApps() {
}, [ filterQuery, category, filterApps ]);
useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainIds.includes(selectedNetwork?.chainId))
.filter(item => item.chainIds.includes(appConfig.network.id))
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
}, [ ]);
return React.useMemo(() => ({
category,
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -14,9 +15,8 @@ import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import useNetwork from 'lib/hooks/useNetwork';
import { space } from 'lib/html-entities';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -30,9 +30,7 @@ import Utilization from 'ui/shared/Utilization';
const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const link = useLink();
const router = useRouter();
const network = useNetwork();
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, Block>(
......@@ -55,9 +53,9 @@ const BlockDetails = () => {
const increment = direction === 'next' ? +1 : -1;
const nextId = String(Number(router.query.id) + increment);
const url = link('block_index', { id: nextId });
const url = link('block', { id: nextId });
router.push(url, undefined);
}, [ link, router ]);
}, [ router ]);
if (isLoading) {
return <BlockDetailsSkeleton/>;
......@@ -106,7 +104,7 @@ const BlockDetails = () => {
title="Transactions"
hint="The number of transactions in the block."
>
<Link href={ link('block_txs', { id: router.query.id }) }>
<Link href={ link('block', { id: router.query.id }, { tab: 'transactions' }) }>
{ data.tx_count } transactions
</Link>
</DetailsInfoItem>
......@@ -124,12 +122,12 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Block reward"
hint={
`For each block, the miner is rewarded with a finite amount of ${ network?.currency || 'native token' }
`For each block, the miner is rewarded with a finite amount of ${ appConfig.network.currency || 'native token' }
on top of the fees paid for all transactions in the block.`
}
columnGap={ 1 }
>
<Text>{ totalReward.dividedBy(WEI).toFixed() } { network?.currency }</Text>
<Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
{ (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && (
<Text variant="secondary" whiteSpace="break-spaces">(
<Tooltip label="Static block reward">
......@@ -182,7 +180,7 @@ const BlockDetails = () => {
title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { network?.currency } </Text>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency } </Text>
<Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
</Text>
......@@ -191,12 +189,13 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Burnt fees"
hint={
`Amount of ${ network?.currency || 'native token' } burned from transactions included in the block.
`Amount of ${ appConfig.network.currency || 'native token' } burned from transactions included in the block.
Equals Block Base Fee per Gas * Gas Used.`
}
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { network?.currency }</Text>
<Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
{ !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
......@@ -213,7 +212,7 @@ const BlockDetails = () => {
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { network?.currency }
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</DetailsInfoItem>
) }
{ /* api doesn't support extra data yet */ }
......@@ -302,7 +301,7 @@ const BlockDetails = () => {
title={ type }
hint="Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees."
>
{ BigNumber(reward).dividedBy(WEI).toFixed() } { network?.currency }
{ BigNumber(reward).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</DetailsInfoItem>
)) }
</>
......
......@@ -3,7 +3,7 @@ import React from 'react';
import TxsContent from 'ui/txs/TxsContent';
const BlockTxs = () => {
return <TxsContent showDescription={ false } showSortButton={ false }/>;
return <TxsContent showDescription={ false } showSortButton={ false } txs={ [] }/>;
};
export default BlockTxs;
import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
......@@ -6,8 +7,7 @@ import type ArrayElement from 'types/utils/ArrayElement';
import type { blocks } from 'data/blocks';
import flameIcon from 'icons/flame.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
......@@ -20,8 +20,6 @@ interface Props {
const BlocksListItem = ({ data, isPending }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const link = useLink();
const network = useNetwork();
return (
<AccountListItemMobile rowGap={ 3 }>
......@@ -30,7 +28,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
{ isPending && <Spinner size="sm" color="blue.500" emptyColor={ spinnerEmptyColor }/> }
<Link
fontWeight={ 600 }
href={ link('block_index', { id: String(data.height) }) }
href={ link('block', { id: String(data.height) }) }
>
{ data.height }
</Link>
......@@ -58,7 +56,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
</Flex>
</Box>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { network?.currency }</Text>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text>
<Text variant="secondary">{ (data.reward.static + data.reward.tx_fee - data.burnt_fees).toLocaleString('en', { maximumFractionDigits: 5 }) }</Text>
</Flex>
<Flex>
......
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import { blocks } from 'data/blocks';
import useNetwork from 'lib/hooks/useNetwork';
import BlocksTableItem from 'ui/blocks/BlocksTableItem';
const BlocksTable = () => {
const network = useNetwork();
return (
<TableContainer width="100%" mt={ 8 }>
......@@ -18,8 +17,8 @@ const BlocksTable = () => {
<Th width="144px">Miner</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="40%">Gas used</Th>
<Th width="30%">Reward { network?.currency }</Th>
<Th width="30%">Burnt fees { network?.currency }</Th>
<Th width="30%">Reward { appConfig.network.currency }</Th>
<Th width="30%">Burnt fees { appConfig.network.currency }</Th>
</Tr>
</Thead>
<Tbody>
......
......@@ -6,7 +6,7 @@ import type ArrayElement from 'types/utils/ArrayElement';
import type { blocks } from 'data/blocks';
import flameIcon from 'icons/flame.svg';
import dayjs from 'lib/date/dayjs';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization';
......@@ -17,18 +17,14 @@ interface Props {
}
const BlocksTableItem = ({ data, isPending }: Props) => {
const link = useLink();
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Tr>
<Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm" color="blue.500" emptyColor={ spinnerEmptyColor }/> }
{ isPending && <Spinner size="sm"/> }
<Link
fontWeight={ 600 }
href={ link('block_index', { id: String(data.height) }) }
href={ link('block', { id: String(data.height) }) }
>
{ data.height }
</Link>
......
import { Box, Icon, Link } from '@chakra-ui/react';
import config from 'configs/app/config';
import React from 'react';
import PlusIcon from 'icons/plus.svg';
......@@ -48,13 +49,13 @@ const Apps = () => {
/>
) }
{ process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM && (
{ config.marketplaceSubmitForm && (
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
marginTop={{ base: 8, sm: 16 }}
href={ process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM }
href={ config.marketplaceSubmitForm }
isExternal
>
<Icon
......
......@@ -10,15 +10,11 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [
{ routeName: 'block_index', title: 'Details', component: <BlockDetails/> },
{ routeName: 'block_txs', title: 'Transactions', component: <BlockTxs/> },
{ id: 'index', title: 'Details', component: <BlockDetails/> },
{ id: 'txs', title: 'Transactions', component: <BlockTxs/> },
];
export interface Props {
tab: RoutedTab['routeName'];
}
const BlockPageContent = ({ tab }: Props) => {
const BlockPageContent = () => {
const router = useRouter();
if (!router.query.id) {
......@@ -30,7 +26,6 @@ const BlockPageContent = ({ tab }: Props) => {
<PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs
tabs={ TABS }
defaultActiveTab={ tab }
/>
</Page>
);
......
......@@ -8,22 +8,17 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [
{ routeName: 'blocks', title: 'All', component: <BlocksContent/> },
{ routeName: 'blocks_reorgs', title: 'Forked', component: <BlocksContent/> },
{ routeName: 'blocks_uncles', title: 'Uncles', component: <BlocksContent/> },
{ id: 'blocks', title: 'All', component: <BlocksContent/> },
{ id: 'reorgs', title: 'Forked', component: <BlocksContent/> },
{ id: 'uncles', title: 'Uncles', component: <BlocksContent/> },
];
export interface Props {
tab: RoutedTab['routeName'];
}
const BlocksPageContent = ({ tab }: Props) => {
const BlocksPageContent = () => {
return (
<Page>
<PageTitle text="Blocks"/>
<RoutedTabs
tabs={ TABS }
defaultActiveTab={ tab }
/>
</Page>
);
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import * as Sentry from '@sentry/react';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => {
const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
......@@ -19,8 +19,12 @@ const Home = () => {
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
setFormVisibility(Boolean(!token && appConfig.isAccountSupported));
}, []);
const checkSentry = React.useCallback(() => {
Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } });
}, []);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
......@@ -48,8 +52,9 @@ const Home = () => {
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageTitle text={
`Home Page for ${ selectedNetwork?.name } network`
`Home Page for ${ appConfig.network.name } network`
}/>
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
{ /* will be deleted when we move to new CI */ }
{ isFormVisible && (
<>
......
import { Box, Center, useColorMode } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import useNetwork from 'lib/hooks/useNetwork';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
......@@ -16,7 +16,6 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode();
const network = useNetwork();
const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false);
......@@ -31,15 +30,15 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
useEffect(() => {
if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: network?.chainId }, app.url);
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: Number(appConfig.network.id) }, app.url);
}
}, [ isFrameLoading, app, colorMode, network, ref ]);
}, [ isFrameLoading, app, colorMode, ref ]);
return (
<Page wrapChildren={ false }>
<Center
as="main"
h="100%"
h="100vh"
paddingTop={{ base: '138px', lg: 0 }}
>
{ (isFrameLoading) && (
......
......@@ -9,19 +9,15 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [
{ routeName: 'private_tags_address', title: 'Address', component: <PrivateAddressTags/> },
{ routeName: 'private_tags_tx', title: 'Transaction', component: <PrivateTransactionTags/> },
{ id: 'address', title: 'Address', component: <PrivateAddressTags/> },
{ id: 'tx', title: 'Transaction', component: <PrivateTransactionTags/> },
];
type Props = {
tab: RoutedTab['routeName'];
}
const PrivateTags = ({ tab }: Props) => {
const PrivateTags = () => {
return (
<Page>
<PageTitle text="Private tags"/>
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
<RoutedTabs tabs={ TABS }/>
</Page>
);
};
......
......@@ -4,7 +4,7 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import ExternalLink from 'ui/shared/ExternalLink';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -16,25 +16,19 @@ import TxRawTrace from 'ui/tx/TxRawTrace';
// import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [
{ routeName: 'tx_index', title: 'Details', component: <TxDetails/> },
{ routeName: 'tx_internal', title: 'Internal txn', component: <TxInternals/> },
{ routeName: 'tx_logs', title: 'Logs', component: <TxLogs/> },
{ id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready
// { routeName: 'tx_state', title: 'State', component: <TxState/> },
{ routeName: 'tx_raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
// { id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
];
export interface Props {
tab: RoutedTab['routeName'];
}
const TransactionPageContent = ({ tab }: Props) => {
const link = useLink();
const TransactionPageContent = () => {
return (
<Page>
{ /* TODO should be shown only when navigating from txs list */ }
<Link mb={ 6 } display="inline-flex" href={ link('txs_validated') }>
<Link mb={ 6 } display="inline-flex" href={ link('txs') }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
......@@ -56,7 +50,6 @@ const TransactionPageContent = ({ tab }: Props) => {
</Flex>
<RoutedTabs
tabs={ TABS }
defaultActiveTab={ tab }
/>
</Page>
);
......
......@@ -12,21 +12,17 @@ import TxsPending from 'ui/txs/TxsPending';
import TxsValidated from 'ui/txs/TxsValidated';
const TABS: Array<RoutedTab> = [
{ routeName: 'txs_validated', title: 'Validated', component: <TxsValidated/> },
{ routeName: 'txs_pending', title: 'Pending', component: <TxsPending/> },
{ id: 'validated', title: 'Validated', component: <TxsValidated/> },
{ id: 'pending', title: 'Pending', component: <TxsPending/> },
];
type Props = {
tab: RoutedTab['routeName'];
}
const Transactions = ({ tab }: Props) => {
const Transactions = () => {
return (
<Page>
<Box h="100%">
<PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
<RoutedTabs tabs={ TABS }/>
</Box>
</Page>
);
......
......@@ -2,44 +2,47 @@ import { Box, Text, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import { WEI, GWEI } from 'lib/consts';
import type { Unit } from 'types/unit';
import getValueWithUnit from 'lib/getValueWithUnit';
interface Props {
value: string;
unit?: 'wei' | 'gwei' | 'ether';
unit?: Unit;
currency?: string;
exchangeRate?: string;
exchangeRate?: string | null;
className?: string;
accuracy?: number;
accuracyUsd?: number;
}
const CurrencyValue = ({ value, currency = '', unit = 'wei', exchangeRate, className, accuracyUsd }: Props) => {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
unitBn = WEI;
break;
case 'gwei':
unitBn = GWEI;
break;
default:
unitBn = new BigNumber(1);
}
const CurrencyValue = ({ value, currency = '', unit, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
const valueCurr = getValueWithUnit(value, unit);
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
const valueBn = new BigNumber(value);
const valueCurr = valueBn.dividedBy(unitBn);
const exchangeRateBn = new BigNumber(exchangeRate || 0);
let usdContent;
if (exchangeRate !== undefined && exchangeRate !== null) {
const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn);
let usdResult: string;
if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
} else {
usdResult = usdBn.toFormat();
}
usdContent = (
<Text as="span" variant="secondary" fontWeight={ 400 }>(${ usdResult })</Text>
);
}
return (
<Box as="span" className={ className }>
<Text as="span">
{ valueCurr.toFixed() }{ currency ? ` ${ currency }` : '' }
<Box as="span" className={ className } display="inline-flex" rowGap={ 3 } columnGap={ 1 }>
<Text display="inline-block">
{ valueResult }{ currency ? ` ${ currency }` : '' }
</Text>
{ exchangeRate !== undefined && exchangeRate !== null &&
// TODO: mb need to implement rounding to the first significant digit
<Text as="span" variant="secondary" whiteSpace="pre" fontWeight={ 400 }> (${ accuracyUsd ? usdBn.toFixed(accuracyUsd) : usdBn.toFixed() })</Text>
}
{ usdContent }
</Box>
);
};
......
import { Box, Flex, Select, Textarea } from '@chakra-ui/react';
import React from 'react';
import hexToUtf8 from 'lib/hexToUtf8';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
type DataType = 'Hex' | 'UTF-8'
......@@ -26,7 +27,7 @@ const RawInputData = ({ hex }: Props) => {
<CopyToClipboard text={ hex }/>
</Flex>
<Textarea
value={ selectedDataType === 'Hex' ? hex : 'UTF-8 equivalent' }
value={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
w="100%"
maxH="220px"
mt={ 2 }
......@@ -38,4 +39,4 @@ const RawInputData = ({ hex }: Props) => {
);
};
export default RawInputData;
export default React.memo(RawInputData);
......@@ -7,12 +7,11 @@ import {
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router';
import React from 'react';
import React, { useEffect, useState } from 'react';
import type { RoutedTab } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import { link } from 'lib/link/link';
import RoutedTabsMenu from './RoutedTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
......@@ -26,29 +25,36 @@ const hiddenItemStyles: StyleProps = {
interface Props {
tabs: Array<RoutedTab>;
defaultActiveTab: RoutedTab['routeName'];
}
const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
const defaultIndex = tabs.findIndex(({ routeName }) => routeName === defaultActiveTab);
const isMobile = useIsMobile();
const RoutedTabs = ({ tabs }: Props) => {
const router = useRouter();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
useEffect(() => {
if (router.isReady) {
let tabIndex = 0;
if (router.query.tab) {
tabIndex = tabs.findIndex(({ id }) => id === router.query.tab);
if (tabIndex < 0) {
tabIndex = 0;
}
}
setActiveTabIndex(tabIndex);
}
}, [ tabs, router ]);
const [ activeTab ] = React.useState<number>(defaultIndex);
const isMobile = useIsMobile();
const { tabsCut, tabsList, tabsRefs, listRef } = useAdaptiveTabs(tabs, isMobile);
const router = useRouter();
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
if (nextTab.routeName) {
const newUrl = link(nextTab.routeName, router.query);
router.push(newUrl, undefined, { shallow: true });
}
router.query.tab = nextTab.id;
router.push(router);
}, [ tabs, router ]);
return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTab }>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex }>
<TabList
marginBottom={{ base: 6, lg: 12 }}
flexWrap="nowrap"
......@@ -68,14 +74,14 @@ const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
}}
>
{ tabsList.map((tab, index) => {
if (!tab.routeName) {
if (!tab.id) {
return (
<RoutedTabsMenu
key="menu"
tabs={ tabs }
activeTab={ tabs[activeTab] }
activeTab={ tabs[activeTabIndex] }
tabsCut={ tabsCut }
isActive={ activeTab >= tabsCut }
isActive={ activeTabIndex >= tabsCut }
styles={ tabsCut < tabs.length ?
// initially our cut is 0 and we don't want to show the menu button too
// but we want to keep it in the tabs row so it won't collapse
......@@ -91,7 +97,7 @@ const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
return (
<Tab
key={ tab.routeName }
key={ tab.id }
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
......@@ -102,7 +108,7 @@ const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
}) }
</TabList>
<TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.routeName }>{ tab.component }</TabPanel>) }
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
</TabPanels>
</Tabs>
);
......
......@@ -49,10 +49,10 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
<PopoverBody display="flex" flexDir="column">
{ tabs.slice(tabsCut).map((tab, index) => (
<Button
key={ tab.routeName }
key={ tab.id }
variant="ghost"
onClick={ handleItemClick }
isActive={ activeTab.routeName === tab.routeName }
isActive={ activeTab.id === tab.id }
justifyContent="left"
data-index={ index }
>
......
import type { RouteName } from 'lib/link/routes';
export interface RoutedTab {
// for simplicity we use routeName as an id
// if we migrate to non-Next.js router that should be revised
// id: string;
routeName: RouteName | null;
id: string;
title: string;
component: React.ReactNode;
}
export interface MenuButton {
routeName: null;
id: null;
title: string;
component: null;
}
......@@ -3,7 +3,7 @@ import type { MenuButton } from './types';
import { middot } from 'lib/html-entities';
export const menuButton: MenuButton = {
routeName: null,
id: null,
title: `${ middot }${ middot }${ middot }`,
component: null,
};
import { Image, chakra } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { Network } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
const EmptyElement = () => null;
const ASSETS_PATH_MAP: Record<string, string> = {
'xdai/mainnet': 'xdai',
'xdai/testnet': 'xdai',
'xdai/optimism': 'optimism',
'xdai/aox': 'arbitrum',
'eth/mainnet': 'ethereum',
'etc/mainnet': 'classic',
'poa/core': 'poa',
};
const getAssetsPath = (network: Network) => {
if (network.assetsNamePath) {
return network.assetsNamePath;
}
const key = [ network.type, network.subType ].filter(Boolean).join('/');
const nameFromMap = ASSETS_PATH_MAP[key];
return nameFromMap || network.type;
};
interface Props {
hash: string;
name: string;
name?: string;
className?: string;
}
const TokenLogo = ({ hash, name, className }: Props) => {
const network = useNetwork();
if (!network) {
return null;
}
const assetsPath = getAssetsPath(network);
const logoSrc = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${ assetsPath }/assets/${ hash }/logo.png`;
return <Image className={ className } src={ logoSrc } alt={ `${ name } logo` } fallback={ <EmptyElement/> }/>;
const logoSrc = `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
${ appConfig.network.assetsPathname || appConfig.network.type }
/assets/
${ hash }
/logo.png
`;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>;
};
export default React.memo(chakra(TokenLogo));
import { Center, Link, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
......@@ -12,9 +12,7 @@ interface Props {
}
const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
const link = useLink();
const url = link('token_index', { id: hash });
const url = link('token_index', { hash });
return (
<Center className={ className } columnGap={ 1 }>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from '../../playwright/RenderWithChakra';
import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } });
test('Utilization light', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('Utilization dark', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
......@@ -16,14 +16,13 @@ interface Props {
}
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight }: Props) => {
const link = useLink();
let url;
if (type === 'transaction') {
url = link('tx_index', { id: id || hash });
url = link('tx', { id: id || hash });
} else if (type === 'token') {
url = link('token_index', { id: id || hash });
url = link('token_index', { hash: id || hash });
} else if (type === 'block') {
url = link('block_index', { id: id || hash });
url = link('block', { id: id || hash });
} else {
url = link('address_index', { id: id || hash });
}
......
import { Box, VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import ghIcon from 'icons/social/git.svg';
......@@ -8,14 +9,13 @@ import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [
{ link: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK, icon: ghIcon, label: 'Github link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK, icon: twIcon, label: 'Twitter link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK, icon: tgIcon, label: 'Telegram link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link !== undefined);
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
{ link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link);
const BLOCKSCOUT_VERSION = process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT_VERSION }`;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
interface Props {
isCollapsed?: boolean;
......@@ -47,6 +47,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
{ SOCIAL_LINKS.length > 0 && (
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
......@@ -56,11 +57,13 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
);
}) }
</Stack>
) }
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mb={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
......
import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
......@@ -15,7 +15,6 @@ import NavLink from './NavLink';
const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isInBrowser = isBrowser();
const [ hasAccount, setHasAccount ] = React.useState(false);
......@@ -31,9 +30,9 @@ const NavigationDesktop = () => {
if (navBarCollapsedCookie === 'false') {
setCollapsedState(false);
}
setHasAccount(Boolean(selectedNetwork?.isAccountSupported && isAuth && isInBrowser));
setHasAccount(Boolean(appConfig.isAccountSupported && isAuth && isInBrowser));
}
}, [ isInBrowser, selectedNetwork?.isAccountSupported ]);
}, [ isInBrowser ]);
const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag);
......
import { Box, VStack } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink';
const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const hasAccount = appConfig.isAccountSupported && isAuth;
return (
<>
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import NextLink from 'next/link';
import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react';
import blockscoutLogo from 'icons/logo.svg';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
// predefined network logos
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'/eth/mainnet': require('icons/networks/logos/eth.svg'),
'/etc/mainnet': require('icons/networks/logos/etc.svg'),
'/poa/core': require('icons/networks/logos/poa.svg'),
'/rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'/xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'/poa/sokol': require('icons/networks/logos/sokol.svg'),
'/artis/sigma1': require('icons/networks/logos/artis.svg'),
'/lukso/l14': require('icons/networks/logos/lukso.svg'),
'/astar': require('icons/networks/logos/astar.svg'),
'/shiden': require('icons/networks/logos/shiden.svg'),
'/shibuya': require('icons/networks/logos/shibuya.svg'),
};
interface Props {
isCollapsed?: boolean;
onClick?: (event: React.SyntheticEvent) => void;
......@@ -15,10 +31,8 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white');
const link = useLink();
const href = link('network_index');
const network = useNetwork();
const logo = network?.logo;
const logo = appConfig.network.logo || LOGOS[appConfig.network.basePath];
const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' });
......@@ -29,7 +43,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<Image
h="20px"
src={ logo }
alt={ `${ network.type } ${ network.subType ? network.subType : '' } network icon` }
alt={ `${ appConfig.network.name } network icon` }
/>
);
} else if (typeof logo !== undefined) {
......
......@@ -3,18 +3,17 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import NETWORKS from 'lib/networks/availableNetworks';
import featuredNetworks from 'lib/networks/featuredNetworks';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const availableTabs = TABS.filter((tab) => NETWORKS.some(({ group }) => group === tab));
const availableTabs = TABS.filter((tab) => featuredNetworks.some(({ group }) => group === tab));
const NetworkMenuPopup = () => {
const selectedNetwork = useNetwork();
const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab);
return (
......@@ -35,7 +34,7 @@ const NetworkMenuPopup = () => {
.filter((network) => network.group === tab)
.map((network) => (
<NetworkMenuLink
key={ network.name }
key={ network.title }
{ ...network }
/>
)) }
......
......@@ -4,7 +4,6 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink';
......@@ -12,9 +11,9 @@ import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const NetworkMenuContentMobile = () => {
const selectedNetwork = useNetwork();
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTab(event.target.value as NetworkGroup);
......@@ -30,7 +29,7 @@ const NetworkMenuContentMobile = () => {
.filter(({ group }) => group === selectedTab)
.map((network) => (
<NetworkMenuLink
key={ network.name }
key={ network.title }
{ ...network }
isMobile
/>
......
......@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { Network } from 'types/networks';
import type { FeaturedNetwork } from 'types/networks';
import checkIcon from 'icons/check.svg';
import placeholderIcon from 'icons/networks/icons/placeholder.svg';
import useColors from './useColors';
interface Props extends Network {
interface Props extends FeaturedNetwork {
isActive: boolean;
isMobile?: boolean;
url: string;
}
const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: Props) => {
const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon });
const iconEl = typeof icon === 'string' ? (
<Image w="30px" h="30px" src={ icon } alt={ `${ type } ${ subType ? subType : '' } network icon` }/>
<Image w="30px" h="30px" src={ icon } alt={ `${ title } network icon` }/>
) : (
<Icon
as={ hasIcon ? icon : placeholderIcon }
......@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }:
fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' }
>
{ name }
{ title }
</Text>
{ isActive && (
<Icon
......
import type { ChangeEvent, FormEvent } from 'react';
import React from 'react';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import SearchBarDesktop from './SearchBarDesktop';
import SearchBarMobile from './SearchBarMobile';
const SearchBar = () => {
const [ value, setValue ] = React.useState('');
const link = useLink();
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
......@@ -18,7 +17,7 @@ const SearchBar = () => {
event.preventDefault();
const url = link('search_results', undefined, { q: value });
window.location.assign(url);
}, [ link, value ]);
}, [ value ]);
return (
<>
......
import { Flex, Link, Text, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props {
value: string;
tokenId: string;
hash: string;
symbol: string;
}
const NftTokenTransferSnippet = (props: Props) => {
const num = props.value === '1' ? '' : props.value;
const url = link('token_instance_item', { hash: props.hash, id: props.tokenId });
return (
<Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
<Text fontWeight={ 500 } as="span">For { num } token ID:</Text>
<Box display="inline-flex" alignItems="center">
<Icon as={ nftIcon } boxSize={ 6 } mr={ 1 }/>
<Link href={ url } fontWeight={ 600 }>{ props.tokenId }</Link>
</Box>
<TokenSnippet symbol={ props.symbol } hash={ props.hash } name="Foo"/>
</Flex>
);
};
export default React.memo(NftTokenTransferSnippet);
......@@ -8,23 +8,71 @@ import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet';
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer
type Props = TTokenTransfer;
const TokenTransfer = ({ from, to, total, exchange_rate: exchangeRate, ...token }: Props) => {
const TokenTransfer = (props: Props) => {
const isColumnLayout = props.token_type === 'ERC-1155' && Array.isArray(props.total);
const tokenSnippet = <TokenSnippet symbol={ props.token_symbol } hash={ props.token_address } name="Foo" ml={ 3 }/>;
const content = (() => {
switch (props.token_type) {
case 'ERC-20':
return (
<Flex>
<Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ props.total.value } unit="ether" exchangeRate={ props.exchange_rate } fontWeight={ 600 }/>
</Text>
{ tokenSnippet }
</Flex>
);
case 'ERC-721': {
return (
<Flex alignItems="center" flexWrap="wrap" columnGap={ 3 } rowGap={ 3 }>
<NftTokenTransferSnippet
tokenId={ props.total.token_id }
value="1"
hash={ props.token_address }
symbol={ props.token_symbol }
/>
);
}
case 'ERC-1155': {
const items = Array.isArray(props.total) ? props.total : [ props.total ];
return items.map((item) => (
<NftTokenTransferSnippet
key={ item.token_id }
tokenId={ item.token_id }
value={ item.value }
hash={ props.token_address }
symbol={ props.token_symbol }
/>
));
}
}
})();
return (
<Flex
alignItems={ isColumnLayout ? 'flex-start' : 'center' }
flexWrap="wrap"
columnGap={ 3 }
rowGap={ 3 }
flexDir={ isColumnLayout ? 'column' : 'row' }
>
<Flex alignItems="center">
<AddressLink fontWeight="500" hash={ from.hash } truncation="constant"/>
<AddressLink fontWeight="500" hash={ props.from.hash } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink fontWeight="500" hash={ to.hash } truncation="constant"/>
<AddressLink fontWeight="500" hash={ props.to.hash } truncation="constant"/>
</Flex>
<Flex flexDir="column" rowGap={ 5 }>
{ content }
</Flex>
<Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ total.value.replaceAll(',', '') } unit="ether" exchangeRate={ exchangeRate } fontWeight={ 600 }/>
</Text>
<TokenSnippet symbol={ token.token_symbol } hash={ token.token_address } name="Foo"/>
</Flex>
);
};
export default TokenTransfer;
export default React.memo(TokenTransfer);
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import TokenTransfer from 'ui/tx/TokenTransfer';
interface Props {
items: Array<TTokenTransfer>;
}
function getItemsNum(items: Array<TTokenTransfer>) {
const nonErc1155items = items.filter((item) => item.token_type !== 'ERC-1155').length;
const erc1155items = items
.filter((item) => item.token_type === 'ERC-1155')
.map((item) => {
if (Array.isArray(item.total)) {
return item.total.length;
}
return 1;
})
.reduce((sum, item) => sum + item, 0);
return nonErc1155items + erc1155items;
}
const TokenTransferList = ({ items }: Props) => {
const itemsNum = getItemsNum(items);
const hasScroll = itemsNum > 5;
const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600');
const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900');
return (
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
_after={ hasScroll ? {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: '20px',
height: '48px',
bgGradient: `linear(to-b, ${ gradientStartColor } 37.5%, ${ gradientEndColor } 77.5%)`,
} : undefined }
maxH={ hasScroll ? '200px' : 'auto' }
overflowY={ hasScroll ? 'scroll' : 'auto' }
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? 10 : 0 }
>
{ items.map((item, index) => <TokenTransfer key={ index } { ...item }/>) }
</Flex>
);
};
export default React.memo(TokenTransferList);
......@@ -111,6 +111,8 @@ const TxDecodedInputData = ({ data }: Props) => {
{ data.method_call }
</GridItem>
{ /* TABLE INSIDE OF BLOCK */ }
{ data.parameters.length > 0 && (
<>
<GridItem
pl={ PADDING }
pr={ GAP }
......@@ -150,6 +152,8 @@ const TxDecodedInputData = ({ data }: Props) => {
>
Data
</GridItem>
</>
) }
{ data.parameters.map(({ name, type, value, indexed }, index) => {
return (
<TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }>
......@@ -160,7 +164,7 @@ const TxDecodedInputData = ({ data }: Props) => {
</Address>
) : (
<Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
<Text>{ value }</Text>
<Text>{ String(value) }</Text>
<CopyToClipboard text={ value }/>
</Flex>
) }
......
import { Grid, GridItem, Text, Box, Icon, Link, Flex } from '@chakra-ui/react';
import { Grid, GridItem, Text, Box, Icon, Link, Spinner } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -14,7 +15,6 @@ import { WEI, WEI_IN_GWEI } from 'lib/consts';
// import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import useNetwork from 'lib/hooks/useNetwork';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -31,11 +31,18 @@ import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TokenTransfer from 'ui/tx/TokenTransfer';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TokenTransferList from 'ui/tx/TokenTransferList';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
const TOKEN_TRANSFERS = [
{ title: 'Tokens Transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens Minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens Burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens Created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
];
const TxDetails = () => {
const selectedNetwork = useNetwork();
const router = useRouter();
const fetch = useFetch();
......@@ -72,6 +79,7 @@ const TxDetails = () => {
hint="Unique character string (TxID) assigned to every verified transaction."
flexWrap="nowrap"
>
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
<Box overflow="hidden">
<HashStringShortenDynamic hash={ data.hash }/>
</Box>
......@@ -85,16 +93,29 @@ const TxDetails = () => {
>
<TxStatus status={ data.status } errorText={ data.status === 'error' ? data.result : undefined }/>
</DetailsInfoItem>
{ data.revert_reason && (
<DetailsInfoItem
title="Revert reason"
hint="The revert reason of the transaction."
>
<TxRevertReason { ...data.revert_reason }/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Block"
hint="Block number containing the transaction."
>
<Text>{ data.block }</Text>
<Text>{ data.block === null ? 'Pending' : data.block }</Text>
{ Boolean(data.confirmations) && (
<>
<TextSeparator color="gray.500"/>
<Text variant="secondary">
{ data.confirmations } Block confirmations
</Text>
</>
) }
</DetailsInfoItem>
{ data.timestamp && (
<DetailsInfoItem
title="Timestamp"
hint="Date & time of transaction inclusion, including length of time for confirmation."
......@@ -105,6 +126,7 @@ const TxDetails = () => {
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }<TextSeparator color="gray.500"/></Text>
<Text variant="secondary">{ getConfirmationDuration(data.confirmation_duration) }</Text>
</DetailsInfoItem>
) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem
title="From"
......@@ -140,44 +162,56 @@ const TxDetails = () => {
</Tooltip> */ }
{ /* <TokenSnippet symbol="UP" name="User Pay" hash="0xA17ed5dFc62D0a3E74D69a0503AE9FdA65d9f212" ml={ 3 }/> */ }
</DetailsInfoItem>
{ (data.token_transfers?.length || 0) > 0 && (
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => {
const items = data.token_transfers?.filter((token) => token.type === type) || [];
if (items.length === 0) {
return null;
}
return (
<DetailsInfoItem
title="Token transferred"
hint="List of token transferred in the transaction."
key={ type }
title={ title }
hint={ hint }
position="relative"
>
<Flex flexDirection="column" alignItems="flex-start" rowGap={ 5 } w="100%">
{ data.token_transfers?.map((item, index) => <TokenTransfer key={ index } { ...item }/>) }
</Flex>
<TokenTransferList items={ items }/>
</DetailsInfoItem>
) }
);
}) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem
title="Value"
hint="Value sent in the native token (and USD) if applicable."
>
<CurrencyValue value={ String(data.value) } currency={ selectedNetwork?.currency } exchangeRate={ data.exchange_rate }/>
<CurrencyValue value={ data.value } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transaction fee"
hint="Total transaction fee."
>
<CurrencyValue value={ String(data.fee.value) } currency={ selectedNetwork?.currency } exchangeRate={ data.exchange_rate }/>
<CurrencyValue
value={ data.fee.value }
currency={ appConfig.network.currency }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
>
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { selectedNetwork?.currency }</Text>
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
<Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas limit & usage by txn"
hint="Actual gas amount used by the transaction."
>
<Text>{ BigNumber(data.gas_used).toFormat() }</Text>
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<TextSeparator/>
<Text >{ BigNumber(data.gas_limit).toFormat() }</Text>
<Utilization ml={ 4 } value={ BigNumber(data.gas_used).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
<Utilization ml={ 4 } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
</DetailsInfoItem>
{ (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && (
<DetailsInfoItem
......@@ -189,18 +223,18 @@ const TxDetails = () => {
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
{ (data.max_fee_per_gas || data.max_priority_fee_per_gas) && <TextSeparator/> }
</Box>
) }
{ data.max_fee_per_gas && (
<Box>
<TextSeparator/>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.max_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
{ data.max_priority_fee_per_gas && <TextSeparator/> }
</Box>
) }
{ data.max_priority_fee_per_gas && (
<Box>
<TextSeparator/>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.max_priority_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
</Box>
......@@ -210,10 +244,15 @@ const TxDetails = () => {
{ data.tx_burnt_fee && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ selectedNetwork?.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
hint={ `Amount of ${ appConfig.network.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
>
<Icon as={ flameIcon } mr={ 1 } boxSize={ 5 } color="gray.500"/>
<CurrencyValue value={ String(data.tx_burnt_fee) } currency={ selectedNetwork?.currency } exchangeRate={ data.exchange_rate }/>
<CurrencyValue
value={ String(data.tx_burnt_fee) }
currency={ appConfig.network.currency }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem>
) }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
......@@ -237,23 +276,34 @@ const TxDetails = () => {
title="Other"
hint="Other data related to this transaction."
>
{ typeof data.type === 'number' && (
<Box>
{
[
typeof data.type === 'number' && (
<Box key="type">
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ data.type }</Text>
{ data.type === 2 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-1559)</Text> }
<TextSeparator/>
</Box>
) }
<Box>
),
<Box key="nonce">
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ data.nonce }</Text>
<TextSeparator/>
</Box>
<Box>
</Box>,
data.position !== null && (
<Box key="position">
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ data.position }</Text>
</Box>
),
]
.filter(Boolean)
.map((item, index) => (
<>
{ index !== 0 && <TextSeparator/> }
{ item }
</>
))
}
</DetailsInfoItem>
<DetailsInfoItem
title="Raw input"
......
import { Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TransactionRevertReason } from 'types/api/transaction';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
type Props = TransactionRevertReason;
const TxRevertReason = (props: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
if ('raw' in props) {
return (
<Grid
bgColor={ bgColor }
p={ 4 }
fontSize="sm"
borderRadius="md"
templateColumns="auto minmax(0, 1fr)"
rowGap={ 2 }
columnGap={ 4 }
whiteSpace="normal"
>
<GridItem fontWeight={ 500 }>Raw:</GridItem>
<GridItem>{ props.raw }</GridItem>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ props.decoded }</GridItem>
</Grid>
);
}
return <TxDecodedInputData data={ props }/>;
};
export default React.memo(TxRevertReason);
......@@ -3,15 +3,12 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
const selectedNetwork = useNetwork();
return (
<Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item } currency={ selectedNetwork?.currency }/>) }
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) }
</Box>
);
};
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
......@@ -11,9 +12,9 @@ import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currency?: string };
type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, currency, success, error }: Props) => {
const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return (
......@@ -34,7 +35,7 @@ const TxInternalsListItem = ({ type, from, to, value, currency, success, error }
</Address>
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { currency }</Text>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
{ /* no gas limit in api yet */ }
......
import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import arrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils';
......@@ -15,7 +15,6 @@ interface Props {
}
const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const selectedNetwork = useNetwork();
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
......@@ -30,7 +29,7 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { selectedNetwork?.currency }
Value { appConfig.network.currency }
</Link>
</Th>
{ /* no gas limit in api yet */ }
......
......@@ -26,7 +26,7 @@ const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) =
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Td>
<Td>
<Address>
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
......@@ -35,16 +35,16 @@ const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) =
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td>
<Address>
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
<Td isNumeric>
<Td isNumeric verticalAlign="middle">
{ value }
</Td>
{ /* no gas limit in api yet */ }
{ /* <Td isNumeric>
{ /* <Td isNumeric verticalAlign='middle'>
{ gasLimit.toLocaleString('en') }
</Td> */ }
</Tr>
......
import { Text, Grid, GridItem, Link, Tooltip, Button, Icon, useColorModeValue } from '@chakra-ui/react';
import { Text, Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link } from '@chakra-ui/react';
import React from 'react';
import type { Log } from 'types/api/log';
import searchIcon from 'icons/search.svg';
// import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -36,17 +38,26 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
pt: 0,
}}
>
{ !decoded && (
<GridItem colSpan={{ base: 1, lg: 2 }}>
<Alert status="warning" display="inline-table" whiteSpace="normal">
To see accurate decoded input data, the contract must be verified.{ space }
<Link href={ link('address_contract_verification', { id: address.hash }) }>Verify the contract here</Link>
</Alert>
</GridItem>
) }
<RowHeader>Address</RowHeader>
<GridItem display="flex" alignItems="center">
<Address>
<Address mr={{ base: 9, lg: 0 }}>
<AddressIcon hash={ address.hash }/>
<AddressLink hash={ address.hash } alias={ address.name } ml={ 2 }/>
</Address>
<Tooltip label="Find matches topic">
{ /* api doesn't have find topic feature yet */ }
{ /* <Tooltip label="Find matches topic">
<Link ml={ 2 } mr={{ base: 9, lg: 0 }} display="inline-flex">
<Icon as={ searchIcon } boxSize={ 5 }/>
</Link>
</Tooltip>
</Tooltip> */ }
<Tooltip label="Log index">
<Button variant="outline" colorScheme="gray" isActive ml="auto" size="sm" fontWeight={ 400 }>
{ index }
......@@ -63,7 +74,13 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
) }
<RowHeader>Topics</RowHeader>
<GridItem>
{ topics.filter(Boolean).map((item, index) => <TxLogTopic key={ index } hex={ item } index={ index }/>) }
{ topics.filter(Boolean).map((item, index) => (
<TxLogTopic
key={ index }
hex={ item }
index={ index }
/>
)) }
</GridItem>
<RowHeader>Data</RowHeader>
<GridItem p={ 4 } fontSize="sm" borderRadius="md" bgColor={ dataBgColor }>
......
import { Flex, Button, Select, Box } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react';
import hexToAddress from 'lib/hexToAddress';
import hexToUtf8 from 'lib/hexToUtf8';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
......@@ -8,36 +14,71 @@ interface Props {
index: number;
}
type DataType = 'Hex' | 'Dec'
const OPTIONS: Array<DataType> = [ 'Hex', 'Dec' ];
type DataType = 'hex' | 'text' | 'address' | 'number';
const VALUE_CONVERTERS: Record<DataType, (hex: string) => string> = {
hex: (hex) => hex,
text: hexToUtf8,
address: hexToAddress,
number: (hex) => BigInt(hex).toString(),
};
const OPTIONS: Array<DataType> = [ 'hex', 'address', 'text', 'number' ];
const TxLogTopic = ({ hex, index }: Props) => {
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('Hex');
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('hex');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedDataType(event.target.value as DataType);
}, []);
const value = VALUE_CONVERTERS[selectedDataType.toLowerCase() as Lowercase<DataType>](hex);
const content = (() => {
switch (selectedDataType) {
case 'hex':
case 'number':
case 'text': {
return (
<>
<Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ value }/>
</Box>
<CopyToClipboard text={ value }/>
</>
);
}
case 'address': {
return (
<Address>
<AddressLink hash={ value }/>
<CopyToClipboard text={ value }/>
</Address>
);
}
}
})();
return (
<Flex alignItems="center" px={{ base: 0, lg: 3 }} _notFirst={{ mt: 3 }} overflow="hidden" maxW="100%">
<Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }>
{ index }
</Button>
{ index !== 0 && (
<Select
size="sm"
borderRadius="base"
value={ selectedDataType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="75px"
mr={ 3 }
flexShrink={ 0 }
w="auto"
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ capitalize(option) }</option>) }
</Select>
<Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ hex }/>
</Box>
) }
{ content }
</Flex>
);
};
......
import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { data } from 'data/txState';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
......@@ -16,7 +16,6 @@ import TxStateStorageItem from './TxStateStorageItem';
type Props = ArrayElement<typeof data>;
const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props) => {
const selectedNetwork = useNetwork();
const hasStorageData = Boolean(storage?.length);
......@@ -65,7 +64,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
<Link>{ miner }</Link>
</Box>
<Box>
<Text as="span">Before { selectedNetwork?.currency } </Text>
<Text as="span">Before { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ before.balance }</Text>
</Box>
{ typeof before.nonce !== 'undefined' && (
......@@ -75,7 +74,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
</Box>
) }
<Box>
<Text as="span">After { selectedNetwork?.currency } </Text>
<Text as="span">After { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ after.balance }</Text>
</Box>
{ typeof after.nonce !== 'undefined' && (
......@@ -84,7 +83,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box>
) }
<Text>State difference { selectedNetwork?.currency }</Text>
<Text>State difference { appConfig.network.currency }</Text>
<Stat>
{ diff }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
......
......@@ -6,15 +6,13 @@ import {
Th,
TableContainer,
} from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import { data } from 'data/txState';
import useNetwork from 'lib/hooks/useNetwork';
import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm" w="auto">
......@@ -23,9 +21,9 @@ const TxStateTable = () => {
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency }` }</Th>
</Tr>
</Thead>
<Tbody>
......
import { Box, Heading, Text, Flex, Link, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { Transaction } from 'types/api/transaction';
import type { txs } from 'data/txs';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities';
import useLink from 'lib/link/useLink';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
const selectedNetwork = useNetwork();
const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => {
const sectionBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const sectionProps = {
borderBottom: '1px solid',
......@@ -27,48 +26,60 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
fontSize: 'sm',
};
const link = useLink();
return (
<>
<Heading as="h4" fontSize="18px" mb={ 6 }>Additional info </Heading>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
<Text>{ tx.fee.value }{ nbsp }{ selectedNetwork?.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
<CurrencyValue
value={ tx.fee.value }
currency={ appConfig.network.currency }
exchangeRate={ tx.exchange_rate }
accuracyUsd={ 2 }
/>
</Flex>
</Box>
{ tx.gas_used !== null && (
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas limit & usage by transaction</Text>
<Flex>
<Text>{ tx.gas_used.toLocaleString('en') }</Text>
<Text>{ BigNumber(tx.gas_used).toFormat() }</Text>
<TextSeparator/>
<Text>{ tx.gas_limit.toLocaleString('en') }</Text>
<Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/>
<Text>{ BigNumber(tx.gas_limit).toFormat() }</Text>
<Utilization ml={ 4 } value={ Number(BigNumber(tx.gas_used).dividedBy(BigNumber(tx.gas_limit)).toFixed(2)) }/>
</Flex>
</Box>
) }
{ (tx.base_fee_per_gas !== null || tx.max_fee_per_gas !== null || tx.max_priority_fee_per_gas !== null) && (
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas fees (Gwei)</Text>
{ tx.base_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.base_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
{ tx.max_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.max_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
{ tx.max_priority_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.max_priority_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
</Box>
) }
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Others</Text>
<Box>
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 } color="gray.500">({ tx.type.eip })</Text>
<Text fontWeight="600" as="span">{ tx.type }</Text>
{ tx.type === 2 && <Text fontWeight="400" as="span" ml={ 1 } color="gray.500">(EIP-1559)</Text> }
</Box>
<Box>
<Text as="span" fontWeight="500">Nonce: </Text>
......@@ -79,7 +90,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
</Box>
<Link fontSize="sm" href={ link('tx_index', { id: tx.hash }) }>More details</Link>
<Link fontSize="sm" href={ link('tx', { id: tx.hash }) }>More details</Link>
</>
);
};
......
import { Box, HStack, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import { txs } from 'data/txs';
import compareBns from 'lib/bigint/compareBns';
import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
......@@ -13,11 +14,12 @@ import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
showDescription?: boolean;
showSortButton?: boolean;
}
const TxsContent = ({ showSortButton = true, showDescription = true }: Props) => {
const TxsContent = ({ showSortButton = true, showDescription = true, txs }: Props) => {
const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
......@@ -50,21 +52,21 @@ const TxsContent = ({ showSortButton = true, showDescription = true }: Props) =>
useEffect(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.amount.value - tx2.amount.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.amount.value - tx1.amount.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value)));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.fee.value - tx2.fee.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value)));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.fee.value - tx1.fee.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value)));
break;
default:
setSortedTxs(txs);
}
}, [ sorting ]);
}, [ sorting, txs ]);
return (
<>
......@@ -93,7 +95,7 @@ const TxsContent = ({ showSortButton = true, showDescription = true }: Props) =>
placeholder="Search by addresses, hash, method..."
/>
</HStack>
<Show below="lg">{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Show>
<Show below="lg"><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Show above="lg"><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/>
......
......@@ -10,16 +10,16 @@ import {
Text,
useColorModeValue,
useDisclosure } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { Transaction } from 'types/api/transaction';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -28,21 +28,21 @@ import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const TxsListItem = ({ tx }: {tx: Transaction}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const selectedNetwork = useNetwork();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const link = useLink();
return (
<>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor: { borderColor } }}>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
</Flex>
......@@ -67,6 +67,7 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
{ /* TODO: we don't recieve method from api */ }
<Text
as="span"
variant="secondary"
......@@ -74,19 +75,22 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ tx.method }
{ /* { tx.method } */ }
CommitHash
</Text>
</Flex>
{ tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block_index', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
</Box>
) }
<Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_from.hash }/>
<AddressIcon hash={ tx.from.hash }/>
<AddressLink
hash={ tx.address_from.hash }
alias={ tx.address_from.alias }
hash={ tx.from.hash }
alias={ tx.from.name }
fontWeight="500"
ml={ 2 }
/>
......@@ -98,22 +102,22 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
color="gray.500"
/>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_to.hash }/>
<AddressIcon hash={ tx.to.hash }/>
<AddressLink
hash={ tx.address_to.hash }
alias={ tx.address_to.alias }
hash={ tx.to.hash }
alias={ tx.to.name }
fontWeight="500"
ml={ 2 }
/>
</Address>
</Flex>
<Box mt={ 2 }>
<Text as="span">Value { selectedNetwork?.currency } </Text>
<Text as="span" variant="secondary">{ tx.amount.value.toFixed(8) }</Text>
<Text as="span">Value { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text>
</Box>
<Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { selectedNetwork?.currency } </Text>
<Text as="span" variant="secondary">{ tx.fee.value.toFixed(8) }</Text>
<Text as="span">Fee { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text>
</Box>
</Box>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
......
import { Show, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
const TxsValidated = () => {
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ 'transactions_pending' ], async() => fetch('/api/transactions/?filter=pending'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile isPending/></Show>
<Show above="lg"><TxsSkeletonDesktop isPending/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
const TxsPending = () => {
return <TxsContent showDescription={ false }/>;
return <TxsContent txs={ data.items } showDescription={ false }/>;
};
export default TxsPending;
export default TxsValidated;
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = ({ isPending }: {isPending?: boolean}) => {
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
</>
);
};
export default TxsInternalsSkeletonDesktop;
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = ({ isPending }: {isPending?: boolean}) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
flexDirection="column"
paddingBottom={ 3 }
paddingTop={ 4 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
)) }
</Box>
</>
);
};
export default TxInternalsSkeletonMobile;
import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import type { txs as data } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork';
import TxsTableItem from './TxsTableItem';
type Props = {
txs: typeof data;
txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void;
sorting: Sort;
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="810px" size="xs">
......@@ -35,14 +33,14 @@ const TxsTable = ({ txs, sort, sorting }: Props) => {
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ selectedNetwork?.currency }` }
{ `Value ${ appConfig.network.currency }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ selectedNetwork?.currency }` }
{ `Fee ${ appConfig.network.currency }` }
</Link>
</Th>
</Tr>
......
......@@ -18,15 +18,15 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { Transaction } from 'types/api/transaction';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
......@@ -34,24 +34,22 @@ import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const link = useLink();
const TxsTableItem = ({ tx }: {tx: Transaction}) => {
const addressFrom = (
<Address>
<Tooltip label={ tx.address_from.type }>
<Box display="flex"><AddressIcon hash={ tx.address_from.hash }/></Box>
<Tooltip label={ tx.from.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.from.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_from.hash } alias={ tx.address_from.alias } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 }/>
</Address>
);
const addressTo = (
<Address>
<Tooltip label={ tx.address_to.type }>
<Box display="flex"> <AddressIcon hash={ tx.address_to.hash }/></Box>
<Tooltip label={ tx.to.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_to.hash } alias={ tx.address_to.alias } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 }/>
</Address>
);
......@@ -78,8 +76,10 @@ const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Td>
<Td>
<VStack alignItems="start">
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td>
......@@ -95,17 +95,26 @@ const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</VStack>
</Td>
<Td>
<TruncatedTextTooltip label={ tx.method }>
{ /* TODO: we don't recieve method from api */ }
{ /* <TruncatedTextTooltip label={ tx.method }>
<Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
>
{ tx.method }
</Tag>
</TruncatedTextTooltip> */ }
<TruncatedTextTooltip label="CommitHash">
<Tag
colorScheme="gray"
>
CommitHash
</Tag>
</TruncatedTextTooltip>
</Td>
<Td>
<Link href={ link('block_index', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td>
{ /* TODO: fix "show" problem */ }
<Show above="xl">
<Td>
{ addressFrom }
......@@ -134,10 +143,10 @@ const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Td>
</Show>
<Td isNumeric>
{ tx.amount.value.toFixed(8) }
<CurrencyValue value={ tx.value }/>
</Td>
<Td isNumeric>
{ tx.fee.value.toFixed(8) }
<CurrencyValue value={ tx.fee.value } accuracy={ 8 }/>
</Td>
</Tr>
);
......
import { Show, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
const TxsValidated = () => {
return <TxsContent/>;
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ 'transactions_validated' ], async() => fetch('/api/transactions/?filter=validated'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile/></Show>
<Show above="lg"><TxsSkeletonDesktop/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
return <TxsContent txs={ data.items }/>;
};
export default TxsValidated;
import { Grid, GridItem } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React, { useCallback } from 'react';
import { Controller } from 'react-hook-form';
import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form';
import useNetwork from 'lib/hooks/useNetwork';
import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ appConfig.network.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
type Props<Inputs extends FieldValues> = {
control: Control<Inputs>;
}
export default function AddressFormNotifications<Inputs extends FieldValues, Checkboxes extends Path<Inputs>>({ control }: Props<Inputs>) {
const selectedNetwork = useNetwork();
const NOTIFICATIONS_NAMES = React.useMemo(() => {
return [ selectedNetwork?.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
}, [ selectedNetwork?.currency ]);
// eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
<CheckboxInput<Inputs, Checkboxes> text={ text } field={ field }/>
......
import { HStack, VStack, Text, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { TWatchlistItem } from 'types/client/account';
import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities';
import AddressSnippet from 'ui/shared/AddressSnippet';
import TokenLogo from 'ui/shared/TokenLogo';
......@@ -14,7 +14,6 @@ const DECIMALS = 18;
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const selectedNetwork = useNetwork();
const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1);
const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A';
......@@ -24,8 +23,9 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
{ selectedNetwork && <TokenLogo hash={ selectedNetwork.nativeTokenAddress } name={ selectedNetwork.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ selectedNetwork?.currency } balance:${ nbsp }` + nativeBalance }</Text>
{ appConfig.network.nativeTokenAddress &&
<TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ appConfig.network.currency } balance:${ nbsp }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack>
{ item.tokens_count && (
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment