Commit 5e4c8864 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #168 from blockscout/fix-ci-cd

Fix ci cd
parents c256c241 25305586
SENTRY_DSN=xxx
NEXT_PUBLIC_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 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_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 = { ...@@ -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)' }, { name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
], ],
}; };
module.exports = { module.exports = {
env: { env: {
es6: true, es6: true,
...@@ -202,6 +203,12 @@ module.exports = { ...@@ -202,6 +203,12 @@ module.exports = {
], ],
'no-restricted-imports': [ 'error', RESTRICTED_MODULES ], '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-key': 'error',
'react/jsx-no-bind': [ 'error', { 'react/jsx-no-bind': [ 'error', {
...@@ -275,5 +282,12 @@ module.exports = { ...@@ -275,5 +282,12 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off', '@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 ],
},
},
], ],
}; };
name: Cleanup environments
on:
pull_request:
types:
- closed
jobs:
if_merged:
if: github.event.pull_request.merged == true
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@fix-e2e-tests
with:
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
secrets: inherit
name: Run E2E tests k8s
on:
push:
# pull_request:
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@fix-e2e-tests
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 name: Run code checks
on: [pull_request] on: [pull_request]
jobs: jobs:
eslint: Check code:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Run ESLint - name: Run ESLint
run: yarn lint:eslint run: yarn lint:eslint
\ No newline at end of file - name: Compile TypeScript
run: yarn lint:tsc
\ No newline at end of file
name: Deploy review environment
on:
# push:
pull_request:
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@fix-e2e-tests
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: ...@@ -12,8 +12,6 @@ env:
K8S_PORT: ${{ secrets.K8S_PORT }} K8S_PORT: ${{ secrets.K8S_PORT }}
USERNAME: ${{ secrets.USERNAME }} USERNAME: ${{ secrets.USERNAME }}
BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}} 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: jobs:
push_to_registry: push_to_registry:
...@@ -50,29 +48,17 @@ jobs: ...@@ -50,29 +48,17 @@ jobs:
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
deploy: build-args: |
name: Deploy frontend to k8s 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 needs: push_to_registry
runs-on: ubuntu-latest uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@deploy-smart-contract-verifier
steps: with:
- name: Check out the repo valuesDir: deploy/values
uses: actions/checkout@v3 appNamespace: frontend-main
- name: Set Kubernetes Context appName: frontend
uses: azure/k8s-set-context@v1 secrets: inherit
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
...@@ -40,6 +40,8 @@ yarn-error.log* ...@@ -40,6 +40,8 @@ yarn-error.log*
# Sentry # Sentry
.sentryclirc .sentryclirc
**.decrypted~**
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/playwright/.cache/ /playwright/.cache/
...@@ -8,18 +8,27 @@ WORKDIR /app ...@@ -8,18 +8,27 @@ WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile RUN yarn --frozen-lockfile
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM node:16-alpine AS builder FROM node:16-alpine AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
COPY .env.template .env.production
# Next.js collects completely anonymous telemetry data about general usage. # Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry # Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1 # 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 RUN yarn build
# Production image, copy all the files and run next # Production image, copy all the files and run next
...@@ -37,15 +46,27 @@ COPY --from=builder /app/next.config.js ./ ...@@ -37,15 +46,27 @@ COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json 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 # Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing # 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/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 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 USER nextjs
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
CMD ["node", "server.js"] CMD ["node", "server.js"]
\ No newline at end of file
...@@ -16,11 +16,16 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or ...@@ -16,11 +16,16 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or
----- -----
## Local Development ## 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: For local development please follow next steps:
- clone repo - clone repo
- install dependencies with `yarn` - install dependencies with `yarn`
- create local env file `.env.local` according to `.env.example` snapshot (see list of used environment variables [below](#environment-variables)) - clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secret values (see description [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output - 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 ## Components visual testing
...@@ -29,33 +34,49 @@ To perform testing locally you need to install docker and run `yarn test-docker` ...@@ -29,33 +34,49 @@ To perform testing locally you need to install docker and run `yarn test-docker`
## Environment variables ## Environment variables
### Variables list ### 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
| Variable | Type | Description | Default value | 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_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | `https://github.com/blockscout/blockscout` | | NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` | | NEXT_PUBLIC_NETWORK_TYPE | `string` | Network type (used as first part of the base path) | `xdai` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | `https://t.me/poa_network` | | NEXT_PUBLIC_NETWORK_SUBTYPE | `string` | Network subtype (used as second part of the base path) | `mainnet` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` | | NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
| 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_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` |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#network-configuration-properties) | Configuration of featured networks that will be shown in the app 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` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| 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` |
| 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` |
| 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>` |
### Network configuration properties ### Featured network configuration properties
| Property | Type | Description | Example value | Property | Type | Description | Example value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `"Gnosis Chain"` | | title | `string` | Displayed name of the network | `'Gnosis Chain'` |
| shortName | `string` | Used for SEO attributes (page title and description) | `"OoG"` | | basePath | `string` | Network explorer main page url | `'/xdai/mainnet'` |
| chainId | `number` | Id of the network. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `1` | | group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| currency | `string` | Network currency symbol. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `"xDAI"` | | 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'` |
| 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"` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>` *Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### Sentry.io setup
TBD
\ 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_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;
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
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_APP_INSTANCE=local
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
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
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'}]
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/poa/core
const getCspPolicy = require('../../lib/csp/getCspPolicy');
async function headers() { async function headers() {
return [ return [
{ {
...@@ -14,10 +12,6 @@ async function headers() { ...@@ -14,10 +12,6 @@ async function headers() {
key: 'X-Content-Type-Options', key: 'X-Content-Type-Options',
value: 'nosniff', value: 'nosniff',
}, },
{
key: 'Content-Security-Policy-Report-Only',
value: getCspPolicy(),
},
], ],
}, },
]; ];
......
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,
},
];
}
module.exports = redirects;
const parseNetworkConfig = require('../../lib/networks/parseNetworkConfig');
async function rewrites() { async function rewrites() {
// there can be networks without subtype // there can be networks without subtype
// routing in nextjs allows optional params only at the end of the path // 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 // if there are paths with subtype and subsubtype, we will change the routing
// but so far we think we're ok with this hack // but so far we think we're ok with this hack
const networksFromConfig = parseNetworkConfig(); //
return networksFromConfig.filter(n => !n.subType).map(n => ({ // UPDATE: as for now I hardcoded all networks without subtype
source: `/${ n.type }/:slug*`, // because we cannot do proper dynamic rewrites in middleware using runtime ENVs
destination: `/${ n.type }/mainnet/:slug*`, // 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; 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);
}
#!/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" ] && [ -n "$envValue" ]; 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
This diff is collapsed.
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:prerelease-5ca79e55
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: 3000
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_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: POA
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: core
NEXT_PUBLIC_NETWORK_ID:
_default: 99
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: POA
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: /
---
creation_rules:
- path_regex: ^(.+/)?secrets\.yaml$
pgp: >-
99E83B7490B1A9F51781E6055317CE0D5CE1230B
This diff is collapsed.
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:prerelease-5ca79e55
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: 3000
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: POA
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: core
NEXT_PUBLIC_NETWORK_ID:
_default: 99
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: POA
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: /
import appConfig from 'configs/app/config';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch'; import nodeFetch from 'node-fetch';
...@@ -13,9 +14,9 @@ export default function fetchFactory(_req: NextApiRequest) { ...@@ -13,9 +14,9 @@ export default function fetchFactory(_req: NextApiRequest) {
'content-type': 'application/json', 'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, 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, headers,
...init, ...init,
}); });
......
import * as Sentry from '@sentry/nextjs'; // import * as Sentry from '@sentry/nextjs';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
...@@ -7,9 +7,11 @@ export default function getUrlWithNetwork(_req: NextApiRequest, path: string) { ...@@ -7,9 +7,11 @@ export default function getUrlWithNetwork(_req: NextApiRequest, path: string) {
const networkType = _req.cookies[cookies.NAMES.NETWORK_TYPE]; const networkType = _req.cookies[cookies.NAMES.NETWORK_TYPE];
const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE]; const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE];
if (!networkType) { // if (!networkType) {
Sentry.captureException(new Error('Incorrect network'), { extra: { networkType, networkSubType } }); // 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 }`; return `/${ networkType }${ networkSubType ? '/' + networkSubType : '' }/${ path }`;
} }
import { withSentry } from '@sentry/nextjs';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch'; import fetchFactory from 'lib/api/fetch';
...@@ -42,5 +41,5 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string, ...@@ -42,5 +41,5 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
res.status(500).json(responseError); res.status(500).json(responseError);
}; };
return withSentry(handler); return handler;
} }
const getMarketplaceApps = require('../getMarketplaceApps'); import appConfig from 'configs/app/config';
const parseNetworkConfig = require('../networks/parseNetworkConfig');
import featuredNetworks from 'lib/networks/featuredNetworks';
import getMarketplaceApps from '../getMarketplaceApps';
const KEY_WORDS = { const KEY_WORDS = {
BLOB: 'blob:', BLOB: 'blob:',
...@@ -12,16 +15,18 @@ const KEY_WORDS = { ...@@ -12,16 +15,18 @@ const KEY_WORDS = {
UNSAFE_EVAL: '\'unsafe-eval\'', UNSAFE_EVAL: '\'unsafe-eval\'',
}; };
const MAIN_DOMAINS = [ '*.blockscout.com', 'blockscout.com' ]; const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
// eslint-disable-next-line no-restricted-properties
const isDev = process.env.NODE_ENV === 'development'; const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssets() { function getNetworksExternalAssets() {
const icons = parseNetworkConfig() const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string') .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() { function getMarketplaceAppsOrigins() {
...@@ -44,7 +49,7 @@ function makePolicyMap() { ...@@ -44,7 +49,7 @@ function makePolicyMap() {
KEY_WORDS.SELF, KEY_WORDS.SELF,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason // 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 // client error monitoring
'sentry.io', '*.sentry.io', 'sentry.io', '*.sentry.io',
...@@ -55,7 +60,7 @@ function makePolicyMap() { ...@@ -55,7 +60,7 @@ function makePolicyMap() {
// next.js generates and rebuilds source maps in dev using eval() // next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278 // https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
isDev ? KEY_WORDS.UNSAFE_EVAL : '', appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS, ...MAIN_DOMAINS,
...@@ -110,11 +115,13 @@ function makePolicyMap() { ...@@ -110,11 +115,13 @@ function makePolicyMap() {
KEY_WORDS.NONE, KEY_WORDS.NONE,
], ],
'report-uri': [
process.env.SENTRY_CSP_REPORT_URI,
],
'frame-src': getMarketplaceAppsOrigins(), 'frame-src': getMarketplaceAppsOrigins(),
...(REPORT_URI ? {
'report-uri': [
REPORT_URI,
],
} : {}),
}; };
} }
...@@ -135,4 +142,4 @@ function getCspPolicy() { ...@@ -135,4 +142,4 @@ function getCspPolicy() {
return policyString; 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 * 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 { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -29,6 +29,12 @@ export default function useFetch() { ...@@ -29,6 +29,12 @@ export default function useFetch() {
return fetch(path, reqParams).then(response => { return fetch(path, reqParams).then(response => {
if (!response.ok) { 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( return response.json().then(
(jsonError) => Promise.reject({ (jsonError) => Promise.reject({
error: jsonError as Error, error: jsonError as Error,
...@@ -36,12 +42,6 @@ export default function useFetch() { ...@@ -36,12 +42,6 @@ export default function useFetch() {
statusText: response.statusText, 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); return Promise.reject(error);
}, },
); );
......
import appConfig from 'configs/app/config';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import marketplaceApps from 'data/marketplaceApps.json'; import marketplaceApps from 'data/marketplaceApps.json';
...@@ -16,14 +17,10 @@ import useCurrentRoute from 'lib/link/useCurrentRoute'; ...@@ -16,14 +17,10 @@ import useCurrentRoute from 'lib/link/useCurrentRoute';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
import useNetwork from './useNetwork';
export default function useNavItems() { export default function useNavItems() {
const selectedNetwork = useNetwork();
const isMarketplaceFilled = useMemo(() => const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainIds.includes(selectedNetwork?.chainId)), marketplaceApps.filter(item => item.chainIds.includes(appConfig.network.id)),
[ selectedNetwork?.chainId ]) [ ])
.length > 0; .length > 0;
const link = useLink(); const link = useLink();
......
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;
}
import isBrowser from 'lib/isBrowser'; import appConfig from 'configs/app/config';
import findNetwork from 'lib/networks/findNetwork';
import { ROUTES } from './routes'; import { ROUTES } from './routes';
import type { RouteName } from './routes'; import type { RouteName } from './routes';
...@@ -12,27 +11,26 @@ export function link(routeName: RouteName, urlParams?: Record<string, Array<stri ...@@ -12,27 +11,26 @@ export function link(routeName: RouteName, urlParams?: Record<string, Array<stri
return ''; return '';
} }
const network = findNetwork({ // if we pass network type, we have to get subtype from params too
network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_type : '', // otherwise getting it from config since it is not cross-chain link
network_sub_type: typeof urlParams?.network_sub_type === 'string' ? urlParams?.network_sub_type : undefined, const networkSubType = typeof urlParams?.network_type === 'string' ? urlParams?.network_sub_type : appConfig.network.subtype;
});
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => { const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !network?.subType) { if (paramName === 'network_sub_type' && !networkSubType) {
return ''; return '';
} }
let paramValue = urlParams?.[paramName]; let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) { if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could // 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(','); paramValue = paramValue.join(',');
} }
return paramValue ? `/${ paramValue }` : ''; 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 ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value); url.searchParams.append(key, value);
......
...@@ -3,6 +3,8 @@ export interface Route { ...@@ -3,6 +3,8 @@ export interface Route {
crossNetworkNavigation?: boolean; // route will not change when switching networks crossNetworkNavigation?: boolean; // route will not change when switching networks
} }
import appConfig from 'configs/app/config';
export type RouteName = keyof typeof ROUTES; export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/[network_type]/[network_sub_type]'; const BASE_PATH = '/[network_type]/[network_sub_type]';
...@@ -17,27 +19,21 @@ export const ROUTES = { ...@@ -17,27 +19,21 @@ export const ROUTES = {
// ACCOUNT // ACCOUNT
watchlist: { watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`, pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
}, },
private_tags: { private_tags: {
pattern: `${ BASE_PATH }/account/tag_address`, pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
}, },
public_tags: { public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`, pattern: `${ BASE_PATH }/account/public_tags_request`,
crossNetworkNavigation: true,
}, },
api_keys: { api_keys: {
pattern: `${ BASE_PATH }/account/api_key`, pattern: `${ BASE_PATH }/account/api_key`,
crossNetworkNavigation: true,
}, },
custom_abi: { custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`, pattern: `${ BASE_PATH }/account/custom_abi`,
crossNetworkNavigation: true,
}, },
profile: { profile: {
pattern: `${ BASE_PATH }/auth/profile`, pattern: `${ BASE_PATH }/auth/profile`,
crossNetworkNavigation: true,
}, },
// TRANSACTIONS // TRANSACTIONS
...@@ -115,6 +111,6 @@ function checkRoutes(route: Record<string, Route>) { ...@@ -115,6 +111,6 @@ function checkRoutes(route: Record<string, Route>) {
return route; return route;
} }
if (process.env.NODE_ENV === 'development') { if (appConfig.isDev) {
checkRoutes(ROUTES); checkRoutes(ROUTES);
} }
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}) { export default function getNetworkTitle() {
const currentNetwork = findNetwork({ network_type: type || '', network_sub_type: subType }); return appConfig.network.name + (appConfig.network.shortName ? ` (${ appConfig.network.shortName })` : '') + ' Explorer';
if (currentNetwork) {
return currentNetwork.name + (currentNetwork.shortName ? ` (${ currentNetwork.shortName })` : '') + ' Explorer';
}
return '';
} }
// 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 { useRouter } from 'next/router';
import React from 'react'; 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 { ROUTES } from 'lib/link/routes';
import useCurrentRoute from 'lib/link/useCurrentRoute'; import useCurrentRoute from 'lib/link/useCurrentRoute';
import NETWORKS from 'lib/networks/availableNetworks'; import featuredNetworks from 'lib/networks/featuredNetworks';
export default function useNetworkNavigationItems() { export default function useNetworkNavigationItems() {
const selectedNetwork = useNetwork();
const currentRouteName = useCurrentRoute()(); const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName]; const currentRoute = ROUTES[currentRouteName];
const router = useRouter(); const router = useRouter();
const isAccount = isAccountRoute(currentRouteName);
return React.useMemo(() => { return React.useMemo(() => {
return NETWORKS.map((network) => { return featuredNetworks.map((network) => {
const routeName = 'crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation ? currentRouteName : 'network_index';
const routeName = (() => { const [ , networkType, networkSubtype ] = network.basePath.split('/');
if ('crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation) { const url = link(routeName, { ...router.query, network_type: networkType, network_sub_type: networkSubtype });
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 { return {
...network, ...network,
url: url, 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 };
};
...@@ -3,7 +3,7 @@ import type { PageParams } from './types'; ...@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {}); const networkTitle = getNetworkTitle();
return { return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '', title: params ? `Block ${ params.id } - ${ networkTitle }` : '',
......
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
...@@ -2,18 +2,12 @@ import type { NextPage } from 'next'; ...@@ -2,18 +2,12 @@ import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from './types';
import Blocks from 'ui/pages/Blocks'; import Blocks from 'ui/pages/Blocks';
import getSeo from './getSeo'; import getSeo from './getSeo';
type Props = { const BlocksNextPage: NextPage = () => {
pageParams: PageParams; const { title } = getSeo();
}
const BlocksNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title } = getSeo(pageParams);
return ( return (
<> <>
<Head> <Head>
......
import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo() {
const networkTitle = getNetworkTitle(params || {});
return { return {
title: params ? `${ networkTitle } - BlockScout` : '', title: getNetworkTitle(),
}; };
} }
import type { GetStaticPaths } from 'next'; import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => { export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true }; return { paths: [], fallback: 'blocking' };
}; };
...@@ -3,7 +3,7 @@ import type { PageParams } from './types'; ...@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {}); const networkTitle = getNetworkTitle();
return { return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '', 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 withReactSvg = require('next-react-svg');
const path = require('path'); const path = require('path');
const headers = require('./configs/nextjs/headers'); const headers = require('./configs/nextjs/headers');
const redirects = require('./configs/nextjs/redirects');
const rewrites = require('./configs/nextjs/rewrites'); const rewrites = require('./configs/nextjs/rewrites');
const moduleExports = { const moduleExports = {
...@@ -18,36 +18,14 @@ const moduleExports = { ...@@ -18,36 +18,14 @@ const moduleExports = {
return config; return config;
}, },
async redirects() { // NOTE: all config functions should be static and not depend on any environment variables
return [ // 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
source: '/', // https://github.com/blockscout/frontend/discussions/167
destination: '/poa/core',
permanent: false,
},
];
},
headers,
rewrites, rewrites,
redirects,
headers,
output: 'standalone', 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,9 +9,13 @@ ...@@ -9,9 +9,13 @@
}, },
"scripts": { "scripts": {
"dev": "next dev", "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 yarn dev",
"build": "next build", "build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start", "start": "next start",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.secrets blockscout",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx", "lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install", "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-ct": "playwright test -c playwright-ct.config.ts",
...@@ -23,6 +27,8 @@ ...@@ -23,6 +27,8 @@
"@emotion/react": "^11", "@emotion/react": "^11",
"@emotion/styled": "^11", "@emotion/styled": "^11",
"@sentry/nextjs": "^7.12.1", "@sentry/nextjs": "^7.12.1",
"@sentry/react": "^7.13.0",
"@sentry/tracing": "^7.13.0",
"@tanstack/react-query": "^4.0.10", "@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10", "@tanstack/react-query-devtools": "^4.0.10",
"@types/react-scroll": "^1.8.4", "@types/react-scroll": "^1.8.4",
...@@ -49,6 +55,7 @@ ...@@ -49,6 +55,7 @@
"@types/react": "18.0.9", "@types/react": "18.0.9",
"@types/react-dom": "18.0.5", "@types/react-dom": "18.0.5",
"@typescript-eslint/eslint-plugin": "^5.27.0", "@typescript-eslint/eslint-plugin": "^5.27.0",
"dotenv-cli": "^6.0.0",
"eslint": "8.16.0", "eslint": "8.16.0",
"eslint-config-next": "^12.3.0", "eslint-config-next": "^12.3.0",
"eslint-plugin-es5": "^1.5.0", "eslint-plugin-es5": "^1.5.0",
......
...@@ -14,8 +14,8 @@ type Props = { ...@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => { const ApiKeysPage: NextPage<Props> = () => {
const title = getNetworkTitle(pageParams || {}); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
...@@ -26,5 +26,5 @@ const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -26,5 +26,5 @@ const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
export default ApiKeysPage; export default ApiKeysPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -14,8 +14,8 @@ type Props = { ...@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => { const CustomAbiPage: NextPage<Props> = () => {
const title = getNetworkTitle(pageParams || {}); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
...@@ -26,5 +26,5 @@ const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -26,5 +26,5 @@ const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
export default CustomAbiPage; export default CustomAbiPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -14,8 +14,8 @@ type Props = { ...@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => { const PublicTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle(pageParams || {}); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
...@@ -26,5 +26,5 @@ const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -26,5 +26,5 @@ const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default PublicTagsPage; export default PublicTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -14,8 +14,8 @@ type Props = { ...@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => { const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle(pageParams || {}); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
...@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default AddressTagsPage; export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -14,8 +14,8 @@ type Props = { ...@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => { const WatchListPage: NextPage<Props> = () => {
const title = getNetworkTitle(pageParams || {}); const title = getNetworkTitle();
return ( return (
<> <>
<Head> <Head>
...@@ -28,5 +28,5 @@ const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -28,5 +28,5 @@ const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
export default WatchListPage; export default WatchListPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -17,3 +17,6 @@ const AppsPage = () => { ...@@ -17,3 +17,6 @@ const AppsPage = () => {
}; };
export default AppsPage; export default AppsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -47,5 +47,5 @@ const AppPage: NextPage = () => { ...@@ -47,5 +47,5 @@ const AppPage: NextPage = () => {
export default AppPage; export default AppPage;
export { getStaticPaths } from 'lib/next/apps/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -15,5 +15,5 @@ const MyProfilePage: NextPage = () => { ...@@ -15,5 +15,5 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage; export default MyProfilePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage; export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -9,13 +9,13 @@ type Props = { ...@@ -9,13 +9,13 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => { const BlockPage: NextPage<Props> = () => {
return ( return (
<BlocksNextPage pageParams={ pageParams }/> <BlocksNextPage/>
); );
}; };
export default BlockPage; export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -15,5 +15,5 @@ const HomePage: NextPage = () => { ...@@ -15,5 +15,5 @@ const HomePage: NextPage = () => {
export default HomePage; export default HomePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -17,5 +17,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -17,5 +17,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage; export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -14,8 +14,8 @@ type Props = { ...@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams; pageParams: PageParams;
} }
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => { const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle(pageParams || {}); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
...@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default AddressTagsPage; export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths'; export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -4,10 +4,13 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; ...@@ -4,10 +4,13 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme'; import theme from 'theme';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({ const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
*/ */
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import sentryConfig from 'configs/sentry/nextjs';
import type { NextPageContext } from 'next'; import type { NextPageContext } from 'next';
import NextErrorComponent from 'next/error'; import NextErrorComponent from 'next/error';
import React from 'react'; import React from 'react';
...@@ -34,6 +35,8 @@ const CustomErrorComponent = (props: { statusCode: number }) => { ...@@ -34,6 +35,8 @@ const CustomErrorComponent = (props: { statusCode: number }) => {
}; };
CustomErrorComponent.getInitialProps = async(contextData: ContextOrProps) => { CustomErrorComponent.getInitialProps = async(contextData: ContextOrProps) => {
Sentry.init(sentryConfig);
// In case this is running in a serverless function, await this in order to give Sentry // 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 // time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(contextData); await Sentry.captureUnderscoreErrorException(contextData);
......
// 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
});
...@@ -2,18 +2,9 @@ import type { FunctionComponent, SVGAttributes } from 'react'; ...@@ -2,18 +2,9 @@ import type { FunctionComponent, SVGAttributes } from 'react';
export type NetworkGroup = 'mainnets' | 'testnets' | 'other'; export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export interface Network { export interface FeaturedNetwork {
name: string; title: string;
chainId: number; // https://chainlist.org/ basePath: string;
currency: string; group: NetworkGroup;
nativeTokenAddress: string;
shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string;
subType?: string;
group: 'mainnets' | 'testnets' | 'other';
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string; icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
logo?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean;
assetsNamePath?: string;
} }
import appConfig from 'configs/app/config';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json'; import marketplaceApps from 'data/marketplaceApps.json';
import useNetwork from 'lib/hooks/useNetwork';
const favoriteAppsLocalStorageKey = 'favoriteApps'; const favoriteAppsLocalStorageKey = 'favoriteApps';
...@@ -27,7 +27,6 @@ function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOv ...@@ -27,7 +27,6 @@ function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOv
} }
export default function useMarketplaceApps() { export default function useMarketplaceApps() {
const selectedNetwork = useNetwork();
const [ isLoading, setIsLoading ] = useState(true); const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>(); const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]); const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
...@@ -80,18 +79,14 @@ export default function useMarketplaceApps() { ...@@ -80,18 +79,14 @@ export default function useMarketplaceApps() {
}, [ filterQuery, category, filterApps ]); }, [ filterQuery, category, filterApps ]);
useEffect(() => { useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ] 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)); .sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps); setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps); setDisplayedApps(defaultDisplayedApps);
setIsLoading(false); setIsLoading(false);
}, [ selectedNetwork ]); }, [ ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
category, category,
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react'; import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
...@@ -7,7 +8,6 @@ import { block } from 'data/block'; ...@@ -7,7 +8,6 @@ import { block } from 'data/block';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -23,7 +23,6 @@ const BlockDetails = () => { ...@@ -23,7 +23,6 @@ const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
const link = useLink(); const link = useLink();
const router = useRouter(); const router = useRouter();
const network = useNetwork();
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag); setIsExpanded((flag) => !flag);
...@@ -79,7 +78,7 @@ const BlockDetails = () => { ...@@ -79,7 +78,7 @@ const BlockDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Block reward" title="Block reward"
hint={ 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.` on top of the fees paid for all transactions in the block.`
} }
columnGap={ 1 } columnGap={ 1 }
...@@ -120,15 +119,16 @@ const BlockDetails = () => { ...@@ -120,15 +119,16 @@ const BlockDetails = () => {
title="Base fee per gas" title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion." hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
> >
<Text>{ (block.base_fee_per_gas / 10 ** 9).toLocaleString('en', { minimumFractionDigits: 18 }) } { network?.currency } </Text> <Text>{ (block.base_fee_per_gas / 10 ** 9).toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency } </Text>
<Text variant="secondary" whiteSpace="pre">{ space }({ block.base_fee_per_gas.toLocaleString('en', { minimumFractionDigits: 9 }) } Gwei)</Text> <Text variant="secondary" whiteSpace="pre">{ space }({ block.base_fee_per_gas.toLocaleString('en', { minimumFractionDigits: 9 }) } Gwei)</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint={ `Amount of ${ network?.currency || 'native token' } burned from transactions included in the block. Equals Block Base Fee per Gas * Gas Used.` } hint={ `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"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ block.burnt_fees.toLocaleString('en', { minimumFractionDigits: 18 }) } { network?.currency }</Text> <Text ml={ 1 }>{ block.burnt_fees.toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency }</Text>
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label="Burnt fees / Txn fees * 100%">
<Box> <Box>
<Utilization ml={ 4 } value={ block.burnt_fees / block.reward.tx_fee }/> <Utilization ml={ 4 } value={ block.burnt_fees / block.reward.tx_fee }/>
......
import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react'; import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type ArrayElement from 'types/utils/ArrayElement';
...@@ -6,7 +7,6 @@ import type ArrayElement from 'types/utils/ArrayElement'; ...@@ -6,7 +7,6 @@ import type ArrayElement from 'types/utils/ArrayElement';
import type { blocks } from 'data/blocks'; import type { blocks } from 'data/blocks';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -21,7 +21,6 @@ interface Props { ...@@ -21,7 +21,6 @@ interface Props {
const BlocksListItem = ({ data, isPending }: Props) => { const BlocksListItem = ({ data, isPending }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const link = useLink(); const link = useLink();
const network = useNetwork();
return ( return (
<AccountListItemMobile rowGap={ 3 }> <AccountListItemMobile rowGap={ 3 }>
...@@ -58,7 +57,7 @@ const BlocksListItem = ({ data, isPending }: Props) => { ...@@ -58,7 +57,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
</Flex> </Flex>
</Box> </Box>
<Flex columnGap={ 2 }> <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> <Text variant="secondary">{ (data.reward.static + data.reward.tx_fee - data.burnt_fees).toLocaleString('en', { maximumFractionDigits: 5 }) }</Text>
</Flex> </Flex>
<Flex> <Flex>
......
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react'; import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import { blocks } from 'data/blocks'; import { blocks } from 'data/blocks';
import useNetwork from 'lib/hooks/useNetwork';
import BlocksTableItem from 'ui/blocks/BlocksTableItem'; import BlocksTableItem from 'ui/blocks/BlocksTableItem';
const BlocksTable = () => { const BlocksTable = () => {
const network = useNetwork();
return ( return (
<TableContainer width="100%" mt={ 8 }> <TableContainer width="100%" mt={ 8 }>
...@@ -18,8 +17,8 @@ const BlocksTable = () => { ...@@ -18,8 +17,8 @@ const BlocksTable = () => {
<Th width="144px">Miner</Th> <Th width="144px">Miner</Th>
<Th width="64px" isNumeric>Txn</Th> <Th width="64px" isNumeric>Txn</Th>
<Th width="40%">Gas used</Th> <Th width="40%">Gas used</Th>
<Th width="30%">Reward { network?.currency }</Th> <Th width="30%">Reward { appConfig.network.currency }</Th>
<Th width="30%">Burnt fees { network?.currency }</Th> <Th width="30%">Burnt fees { appConfig.network.currency }</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link } from '@chakra-ui/react';
import config from 'configs/app/config';
import React from 'react'; import React from 'react';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
...@@ -48,13 +49,13 @@ const Apps = () => { ...@@ -48,13 +49,13 @@ const Apps = () => {
/> />
) } ) }
{ process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM && ( { config.marketplaceSubmitForm && (
<Link <Link
fontWeight="bold" fontWeight="bold"
display="inline-flex" display="inline-flex"
alignItems="baseline" alignItems="baseline"
marginTop={{ base: 8, sm: 16 }} marginTop={{ base: 8, sm: 16 }}
href={ process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM } href={ config.marketplaceSubmitForm }
isExternal isExternal
> >
<Icon <Icon
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react'; 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 { useRouter } from 'next/router';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => { const Home = () => {
const router = useRouter(); const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast(); const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false); const [ isFormVisible, setFormVisibility ] = React.useState(false);
...@@ -19,8 +19,12 @@ const Home = () => { ...@@ -19,8 +19,12 @@ const Home = () => {
React.useEffect(() => { React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN); const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported)); setFormVisibility(Boolean(!token && appConfig.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]); }, []);
const checkSentry = React.useCallback(() => {
Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } });
}, []);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => { const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value); setToken(event.target.value);
...@@ -48,8 +52,9 @@ const Home = () => { ...@@ -48,8 +52,9 @@ const Home = () => {
<Page> <Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px"> <VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageTitle text={ <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 */ } { /* will be deleted when we move to new CI */ }
{ isFormVisible && ( { isFormVisible && (
<> <>
......
import { Box, Center, useColorMode } from '@chakra-ui/react'; import { Box, Center, useColorMode } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import useNetwork from 'lib/hooks/useNetwork';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -16,7 +16,6 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -16,7 +16,6 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading); const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null); const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const network = useNetwork();
const handleIframeLoad = useCallback(() => { const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false); setIsFrameLoading(false);
...@@ -31,9 +30,9 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -31,9 +30,9 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
useEffect(() => { useEffect(() => {
if (app && !isFrameLoading) { if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: network?.chainId }, app.url); ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: appConfig.network.id }, app.url);
} }
}, [ isFrameLoading, app, colorMode, network, ref ]); }, [ isFrameLoading, app, colorMode, ref ]);
return ( return (
<Page wrapChildren={ false }> <Page wrapChildren={ false }>
......
import { Image, chakra } from '@chakra-ui/react'; import { Image, chakra } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import type { Network } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
const EmptyElement = () => null; 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 { interface Props {
hash: string; hash: string;
name: string; name?: string;
className?: string; className?: string;
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className }: Props) => {
const network = useNetwork(); const logoSrc = `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
if (!network) { ${ appConfig.network.assetsPathname || appConfig.network.type }
return null; /assets/
} ${ hash }
/logo.png
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 || 'token' } logo` } fallback={ <EmptyElement/> }/>;
return <Image className={ className } src={ logoSrc } alt={ `${ name } logo` } fallback={ <EmptyElement/> }/>;
}; };
export default React.memo(chakra(TokenLogo)); export default React.memo(chakra(TokenLogo));
import { Box, VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react'; import { Box, VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import ghIcon from 'icons/social/git.svg'; import ghIcon from 'icons/social/git.svg';
...@@ -8,14 +9,13 @@ import twIcon from 'icons/social/tweet.svg'; ...@@ -8,14 +9,13 @@ import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [ const SOCIAL_LINKS = [
{ link: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK, icon: ghIcon, label: 'Github link' }, { link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK, icon: twIcon, label: 'Twitter link' }, { link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK, icon: tgIcon, label: 'Telegram link' }, { link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK, icon: statsIcon, label: 'Staking analytic link' }, { link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link !== undefined); ].filter(({ link }) => link !== undefined);
const BLOCKSCOUT_VERSION = process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION; const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT_VERSION }`;
interface Props { interface Props {
isCollapsed?: boolean; isCollapsed?: boolean;
...@@ -60,7 +60,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -60,7 +60,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
<Text variant="secondary" mb={ 8 }> <Text variant="secondary" mb={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks. Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text> </Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text> <Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text>
</Box> </Box>
</VStack> </VStack>
); );
......
import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react'; import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
...@@ -15,7 +15,6 @@ import NavLink from './NavLink'; ...@@ -15,7 +15,6 @@ import NavLink from './NavLink';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isInBrowser = isBrowser(); const isInBrowser = isBrowser();
const [ hasAccount, setHasAccount ] = React.useState(false); const [ hasAccount, setHasAccount ] = React.useState(false);
...@@ -31,9 +30,9 @@ const NavigationDesktop = () => { ...@@ -31,9 +30,9 @@ const NavigationDesktop = () => {
if (navBarCollapsedCookie === 'false') { if (navBarCollapsedCookie === 'false') {
setCollapsedState(false); setCollapsedState(false);
} }
setHasAccount(Boolean(selectedNetwork?.isAccountSupported && isAuth && isInBrowser)); setHasAccount(Boolean(appConfig.isAccountSupported && isAuth && isInBrowser));
} }
}, [ isInBrowser, selectedNetwork?.isAccountSupported ]); }, [ isInBrowser ]);
const handleTogglerClick = React.useCallback(() => { const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag); setCollapsedState((flag) => !flag);
......
import { Box, VStack } from '@chakra-ui/react'; import { Box, VStack } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/snippets/navigation/NavFooter'; import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth; const hasAccount = appConfig.isAccountSupported && isAuth;
return ( return (
<> <>
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react'; import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react'; import type { FunctionComponent, SVGAttributes } from 'react';
import blockscoutLogo from 'icons/logo.svg'; import blockscoutLogo from 'icons/logo.svg';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; 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 { interface Props {
isCollapsed?: boolean; isCollapsed?: boolean;
onClick?: (event: React.SyntheticEvent) => void; onClick?: (event: React.SyntheticEvent) => void;
...@@ -17,8 +33,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => { ...@@ -17,8 +33,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white'); const logoColor = useColorModeValue('blue.600', 'white');
const link = useLink(); const link = useLink();
const href = link('network_index'); const href = link('network_index');
const network = useNetwork(); const logo = appConfig.network.logo || LOGOS[appConfig.network.basePath];
const logo = network?.logo;
const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' }); const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' });
...@@ -29,7 +44,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => { ...@@ -29,7 +44,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<Image <Image
h="20px" h="20px"
src={ logo } src={ logo }
alt={ `${ network.type } ${ network.subType ? network.subType : '' } network icon` } alt={ `${ appConfig.network.name } network icon` }
/> />
); );
} else if (typeof logo !== undefined) { } else if (typeof logo !== undefined) {
......
...@@ -3,18 +3,17 @@ import React from 'react'; ...@@ -3,18 +3,17 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks'; import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork'; import featuredNetworks from 'lib/networks/featuredNetworks';
import NETWORKS from 'lib/networks/availableNetworks';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems'; import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink'; import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ]; 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 NetworkMenuPopup = () => {
const selectedNetwork = useNetwork();
const items = useNetworkNavigationItems(); const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab); const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab);
return ( return (
...@@ -35,7 +34,7 @@ const NetworkMenuPopup = () => { ...@@ -35,7 +34,7 @@ const NetworkMenuPopup = () => {
.filter((network) => network.group === tab) .filter((network) => network.group === tab)
.map((network) => ( .map((network) => (
<NetworkMenuLink <NetworkMenuLink
key={ network.name } key={ network.title }
{ ...network } { ...network }
/> />
)) } )) }
......
...@@ -4,7 +4,6 @@ import React from 'react'; ...@@ -4,7 +4,6 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks'; import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems'; import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink'; import NetworkMenuLink from './NetworkMenuLink';
...@@ -12,9 +11,9 @@ import NetworkMenuLink from './NetworkMenuLink'; ...@@ -12,9 +11,9 @@ import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ]; const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const NetworkMenuContentMobile = () => { const NetworkMenuContentMobile = () => {
const selectedNetwork = useNetwork();
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const items = useNetworkNavigationItems(); 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>) => { const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTab(event.target.value as NetworkGroup); setSelectedTab(event.target.value as NetworkGroup);
...@@ -30,7 +29,7 @@ const NetworkMenuContentMobile = () => { ...@@ -30,7 +29,7 @@ const NetworkMenuContentMobile = () => {
.filter(({ group }) => group === selectedTab) .filter(({ group }) => group === selectedTab)
.map((network) => ( .map((network) => (
<NetworkMenuLink <NetworkMenuLink
key={ network.name } key={ network.title }
{ ...network } { ...network }
isMobile isMobile
/> />
......
...@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react'; ...@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { Network } from 'types/networks'; import type { FeaturedNetwork } from 'types/networks';
import checkIcon from 'icons/check.svg'; import checkIcon from 'icons/check.svg';
import placeholderIcon from 'icons/networks/icons/placeholder.svg'; import placeholderIcon from 'icons/networks/icons/placeholder.svg';
import useColors from './useColors'; import useColors from './useColors';
interface Props extends Network { interface Props extends FeaturedNetwork {
isActive: boolean; isActive: boolean;
isMobile?: boolean; isMobile?: boolean;
url: string; 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 hasIcon = Boolean(icon);
const colors = useColors({ hasIcon }); const colors = useColors({ hasIcon });
const iconEl = typeof icon === 'string' ? ( 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 <Icon
as={ hasIcon ? icon : placeholderIcon } as={ hasIcon ? icon : placeholderIcon }
...@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: ...@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }:
fontSize={ isMobile ? 'sm' : 'md' } fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' } lineHeight={ isMobile ? '20px' : '24px' }
> >
{ name } { title }
</Text> </Text>
{ isActive && ( { isActive && (
<Icon <Icon
......
import { Grid, GridItem, Text, Box, Icon, Link, Flex } from '@chakra-ui/react'; import { Grid, GridItem, Text, Box, Icon, Link, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
...@@ -14,7 +15,6 @@ import { WEI, WEI_IN_GWEI } from 'lib/consts'; ...@@ -14,7 +15,6 @@ import { WEI, WEI_IN_GWEI } from 'lib/consts';
// import successIcon from 'icons/status/success.svg'; // import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useNetwork from 'lib/hooks/useNetwork';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration'; import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
...@@ -35,7 +35,6 @@ import TokenTransfer from 'ui/tx/TokenTransfer'; ...@@ -35,7 +35,6 @@ import TokenTransfer from 'ui/tx/TokenTransfer';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData'; import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
const TxDetails = () => { const TxDetails = () => {
const selectedNetwork = useNetwork();
const router = useRouter(); const router = useRouter();
const fetch = useFetch(); const fetch = useFetch();
...@@ -154,19 +153,19 @@ const TxDetails = () => { ...@@ -154,19 +153,19 @@ const TxDetails = () => {
title="Value" title="Value"
hint="Value sent in the native token (and USD) if applicable." 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={ String(data.value) } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transaction fee" title="Transaction fee"
hint="Total transaction fee." hint="Total transaction fee."
> >
<CurrencyValue value={ String(data.fee.value) } currency={ selectedNetwork?.currency } exchangeRate={ data.exchange_rate }/> <CurrencyValue value={ String(data.fee.value) } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas price" 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." 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> <Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
...@@ -209,10 +208,10 @@ const TxDetails = () => { ...@@ -209,10 +208,10 @@ const TxDetails = () => {
{ data.tx_burnt_fee && ( { data.tx_burnt_fee && (
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" 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"/> <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 }/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<GridItem colSpan={{ base: undefined, lg: 2 }}> <GridItem colSpan={{ base: undefined, lg: 2 }}>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment