Commit 808184ca authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into client-api-calls

parents 29c7b39e 43fdf196
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
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_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},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60","logo":"https://www.fillmurray.com/240/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","chainId":99},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets","chainId":30},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"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_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx
......@@ -4,3 +4,21 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK
NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=APP_NEXT_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE
NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME
NEXT_PUBLIC_NETWORK_SHORT_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_SHORT_NAME
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=APP_NEXT_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME
NEXT_PUBLIC_NETWORK_TYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_TYPE
NEXT_PUBLIC_NETWORK_SUBTYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_SUBTYPE
NEXT_PUBLIC_NETWORK_ID=APP_NEXT_NEXT_PUBLIC_NETWORK_ID
NEXT_PUBLIC_NETWORK_CURRENCY=APP_NEXT_NEXT_PUBLIC_NETWORK_CURRENCY
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=APP_NEXT_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=APP_NEXT_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED
NEXT_PUBLIC_FEATURED_NETWORKS=APP_NEXT_NEXT_PUBLIC_FEATURED_NETWORKS
NEXT_PUBLIC_APP_PROTOCOL=APP_NEXT_NEXT_PUBLIC_APP_PROTOCOL
NEXT_PUBLIC_APP_HOST=APP_NEXT_NEXT_PUBLIC_APP_HOST
NEXT_PUBLIC_APP_PORT=APP_NEXT_NEXT_PUBLIC_APP_PORT
NEXT_PUBLIC_API_ENDPOINT=APP_NEXT_NEXT_PUBLIC_API_ENDPOINT
NEXT_PUBLIC_API_BASE_PATH=APP_NEXT_NEXT_PUBLIC_API_BASE_PATH
const RESTRICTED_MODULES = {
paths: [
{ name: 'dayjs', message: 'Please use lib/date/dayjs.ts' },
{ name: 'dayjs', message: 'Please use lib/date/dayjs.ts instead of directly importing dayjs' },
{ name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
],
};
module.exports = {
env: {
es6: true,
......@@ -193,7 +195,7 @@ module.exports = {
groups: [
'module',
'/types/',
[ '/^data/', '/^icons/', '/^lib/', '/^pages/', '/^theme/', '/^ui/' ],
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
......@@ -201,6 +203,12 @@ module.exports = {
],
'no-restricted-imports': [ 'error', RESTRICTED_MODULES ],
'no-restricted-properties': [ 2, {
object: 'process',
property: 'env',
// FIXME: restrict the rule only NEXT_PUBLIC variables
message: 'Please use configs/app/config.ts to import any NEXT_PUBLIC environment variables. For other properties please disable this rule for a while.',
} ],
'react/jsx-key': 'error',
'react/jsx-no-bind': [ 'error', {
......@@ -274,5 +282,12 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: [ 'configs/**/*.js', 'configs/**/*.ts', '*.config.ts' ],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
},
},
],
};
__snapshots__/** filter=lfs diff=lfs merge=lfs -text
......@@ -4,14 +4,12 @@ on:
pull_request:
types:
- closed
- merged
workflow_dispatch:
jobs:
if_merged:
if: github.event.pull_request.merged == true
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@deploy-smart-contract-verifier
cleanup:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@master
with:
valuesDir: values/frontend
appNamespace: frontend-$GITHUB_REF_NAME
appName: frontend
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
secrets: inherit
name: Publish Docker image for review environment
name: Run E2E tests k8s
on:
# push:
pull_request:
# pull_request:
workflow_dispatch
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
......@@ -63,13 +64,16 @@ jobs:
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_frontend:
deploy_and_tests:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
with:
valuesDir: deploy/values
appNamespace: frontend-$GITHUB_HEAD_REF
appName: frontend
frontendIngressHost: frontend-$GITHUB_HEAD_REF.aws-k8s.blockscout.com
valuesDir: deploy/values/e2e
appName: e2e-front
appNamespace: e2e-front-$GITHUB_SHA_SHORT
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
name: Linters
on: [pull_request]
on:
pull_request:
push:
branches:
- main
jobs:
eslint:
check_code:
name: Run code checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn lint:eslint
\ No newline at end of file
run: yarn lint:eslint
- name: Compile TypeScript
run: yarn lint:tsc
\ No newline at end of file
name: Playwright
on: [pull_request]
jobs:
test:
name: Run components visual tests
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.27.0-focal
steps:
- run: apt-get update && apt-get install git-lfs
- uses: actions/checkout@v3
with:
lfs: 'true'
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run your tests
run: HOME=/root yarn test-ct
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: playwright-report
path: playwright-report
\ No newline at end of file
name: Deploy review environment
on:
# push:
pull_request:
workflow_dispatch:
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
K8S_HOST: ${{ secrets.K8S_HOST }}
BASTION_HOST: ${{ secrets.BASTION_HOST }}
K8S_PORT: ${{ secrets.K8S_PORT }}
USERNAME: ${{ secrets.USERNAME }}
BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}}
jobs:
push_to_registry:
name: Push Docker image to registry
runs-on: ubuntu-latest
outputs:
shortSha: ${{ steps.output-step.outputs.short-sha }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/blockscout/frontend
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
- name: Add outputs
run: |
echo "::set-output name=short-sha::${{ env.SHORT_SHA }}"
id: output-step
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
deploy_frontend:
name: Deploy frontend app
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values/review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -49,16 +49,18 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
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 }}
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
deploy_frontend:
deploy_main:
name: Deploy frontend app
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@deploy-smart-contract-verifier
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values
appNamespace: frontend-main
appName: frontend
valuesDir: deploy/values/review
appNamespace: front-main
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:main
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -28,6 +28,7 @@ yarn-error.log*
# local env files
.env*.local
/configs/envs/.env.secrets
# vercel
.vercel
......@@ -41,3 +42,6 @@ yarn-error.log*
.sentryclirc
**.decrypted~**
/test-results/
/playwright-report/
/playwright/.cache/
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; }
git lfs post-checkout "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; }
git lfs post-commit "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; }
git lfs post-merge "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; }
git lfs pre-push "$@"
{
"recommendations": [
"formulahendry.auto-close-tag",
"formulahendry.auto-rename-tag",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
]
}
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": [],
"label": "dev server",
"detail": "start local dev server",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiGreen",
"id": "server-process"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "dev:poa_core",
"problemMatcher": [],
"label": "dev server: poa",
"detail": "start local dev server for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiMagenta",
"id": "server-process"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "typescript",
"label": "tsc build",
"detail": "compile typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"icon": {
"color": "terminal.ansiCyan",
"id": "symbol-type-parameter"
},
"presentation": {
"reveal": "never",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"group": "build",
},
{
"type": "npm",
"script": "lint:eslint:fix",
"problemMatcher": [],
"label": "eslint",
"detail": "run eslint",
"presentation": {
"reveal": "silent",
"panel": "new",
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiYellow",
"id": "zap"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "test-docker",
"problemMatcher": [],
"label": "test: playwright",
"detail": "run visual components tests",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "build:docker",
"problemMatcher": [],
"label": "docker: build",
"detail": "build docker image",
"presentation": {
"reveal": "always",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiRed",
"id": "symbol-structure"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "start:docker:poa_core",
"problemMatcher": [],
"label": "docker: run poa",
"detail": "run docker container for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiRed",
"id": "browser"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "npm",
"script": "format-svg",
"problemMatcher": [],
"label": "format svg",
"detail": "format svg files with svgo",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiCyan",
"id": "combine"
},
"runOptions": {
"instanceLimit": 1
}
},
]
}
\ No newline at end of file
......@@ -25,6 +25,10 @@ ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_CSP_REPORT_URI
ARG SENTRY_AUTH_TOKEN
# pass commit sha to the app (uses by sentry.io as release version)
ARG GIT_COMMIT_SHA
ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
RUN yarn build
# Production image, copy all the files and run next
......
......@@ -9,6 +9,7 @@ Core technologies what used in the project are
- [Next.js](https://nextjs.org/) as application framework
- [Chakra](https://chakra-ui.com/) as component library; our theme customization could be found in `/theme` folder
- [css-modules](https://github.com/css-modules/css-modules) as lib for styling custom components
- [playwright](https://playwright.dev/) as a tool for components visual testing
And of course our premier language is [Typescript](https://www.typescriptlang.org/)
......@@ -20,39 +21,81 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or
For local development please follow next steps:
- clone repo
- install dependencies with `yarn`
- clone `env.example` into local env file `env.local` (see explanation of all used environment variables [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output
- clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secret values (see description [below](#environment-variables))
- to spin up local dev server
- for predefined networks configs (see full available list in `package.json`) you can just run `yarn dev:<app_name>`
- for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev`
- navigate to the host from logs output
## Components visual testing
We use [playwright experimental components testing](https://playwright.dev/docs/test-components) for visual (screenshots) CI check. Test renders a single component in headless browser in docker, generates screenshots and then compares this screenshot with a reference one.
To perform testing locally you need to install docker and run `yarn test-docker`
## Environment variables
### Variables list
The app instance could be customized by passing following variables to NodeJS environment.
The app instance could be customized by passing following variables to NodeJS environment at runtime.
**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted
### Network configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` |
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
| NEXT_PUBLIC_NETWORK_TYPE | `string` | Network type (used as first part of the base path) | `xdai` |
| NEXT_PUBLIC_NETWORK_SUBTYPE | `string` | Network subtype (used as second part of the base path) | `mainnet` |
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
| NEXT_PUBLIC_NETWORK_CURRENCY | `string` | Network currency symbol | `xDAI` |
| NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS | `string` | Address of network's native token | `0x029a799563238d0e75e20be2f4bda0ea68d00172` |
| NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). If not provided, the network type will be used as its assets path part | `ethereum` |
| NEXT_PUBLIC_NETWORK_LOGO | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `https://www.fillmurray.com/240/40` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### UI configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#featured-network-configuration-properties) | Configuration of featured networks that will be shown in the network menu | `[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]` |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` *(optional)* | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` *(optional)* | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_SUPPORTED_NETWORKS | `Array<Network>` where `Network` can have following [properties](#network-configuration-properties) | Configuration of supported networks | `[{'name':'Gnosis Chain','type':'xdai','subType':'mainnet','group':'mainnets','isAccountSupported':true, 'chainId': 100,'icon':'https://www.fillmurray.com/60/60','logo':'https://www.fillmurray.com/240/40'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
### Network configuration properties
### App configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_APP_INSTANCE | `string` *(optional)* | Name of app instance | `wonderful_kepler` |
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` |
| NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` |
| NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` |
### API configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_API_ENDPOINT | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API endpoint base URL in this variable | `https://blockscout.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
### Featured network configuration properties
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `'Gnosis Chain'` |
| chainId | `number` | Id of the network. Could be seen there – [https://chainlist.org/](https://chainlist.org/) | `1` |
| 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"` |
| title | `string` | Displayed name of the network | `'Gnosis Chain'` |
| basePath | `string` | Network explorer main page url | `'/xdai/mainnet'` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| 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'` |
*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
### External services configuration
TBD
\ No newline at end of file
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` |
\ No newline at end of file
/* eslint-disable no-restricted-properties */
const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development';
const baseUrl = [
process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https',
'://',
process.env.NEXT_PUBLIC_VERCEL_URL || process.env.NEXT_PUBLIC_APP_HOST,
process.env.NEXT_PUBLIC_APP_PORT ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '',
].join('');
const 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,
api: {
endpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || 'https://blockscout.com',
basePath: process.env.NEXT_PUBLIC_API_BASE_PATH || '',
},
});
export default config;
# app config
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_INSTANCE=local
# nav and footer config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
# marketplace config
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
# api config
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
\ No newline at end of file
# nav and footer config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]
# current network config
NEXT_PUBLIC_NETWORK_NAME=POA
NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa
NEXT_PUBLIC_NETWORK_TYPE=poa
NEXT_PUBLIC_NETWORK_SUBTYPE=core
NEXT_PUBLIC_NETWORK_ID=99
NEXT_PUBLIC_NETWORK_CURRENCY=POA
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core
const BASE_PATH = require('../../lib/link/basePath.js');
const PATHS = require('../../lib/link/paths.js');
const oldUrls = [
{
oldPath: '/account/tag_transaction',
newPath: `${ PATHS.private_tags }?tab=tx`,
},
{
oldPath: '/pending-transactions',
newPath: `${ PATHS.txs }?tab=pending`,
},
{
oldPath: '/tx/:id/internal-transactions',
newPath: `${ PATHS.tx }?tab=internal`,
},
{
oldPath: '/tx/:id/logs',
newPath: `${ PATHS.tx }?tab=logs`,
},
{
oldPath: '/tx/:id/raw-trace',
newPath: `${ PATHS.tx }?tab=raw_trace`,
},
{
oldPath: '/tx/:id/state',
newPath: `${ PATHS.tx }?tab=state`,
},
{
oldPath: '/uncles',
newPath: `${ PATHS.blocks }?tab=uncles`,
},
{
oldPath: '/reorgs',
newPath: `${ PATHS.blocks }?tab=reorgs`,
},
{
oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }?tab=txs`,
},
];
async function redirects() {
const homePagePath = '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/');
return [
{
source: '/',
destination: '/poa/core',
destination: homePagePath,
permanent: false,
},
...oldUrls.map(item => {
const source = BASE_PATH.replaceAll('[', ':').replaceAll(']', '') + item.oldPath;
const destination = item.newPath.replaceAll('[', ':').replaceAll(']', '');
return { source, destination, permanent: false };
}),
];
}
......
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);
}
import type { AppItemOverview } from 'types/client/apps';
import { APP_CATEGORIES } from 'ui/apps/constants';
export const TEMPORARY_DEMO_APPS: Array<AppItemOverview> = [
{
author: 'xDaichain',
id: 'easy-staking',
title: 'Easy Staking',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'Accessible DeFi staking platform for STAKE holders on Ethereum',
site: 'https://easy-staking.xdaichain.com/',
// eslint-disable-next-line max-len
description: 'Easy Staking allows users to place STAKE into a contract and receive STAKE emissions on Ethereum. It provides an accessible staking mechanism for users and increases STAKE utility and DeFi composability. EasyStaking also:\n\n- Incentivizes liquidity providers on decentralized exchanges through unique reward mechanisms\n- Creates staking opportunities via hardware wallets and other Ethereum applications\n- Provides staking opportunities with no minimum STAKE requirements to participate\n- Limits total circulating supply',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'curve',
title: 'Curve',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
// eslint-disable-next-line max-len
shortDescription: 'Curve is an exchange liquidity pool on Ethereum designed for: extremely efficient stablecoin trading, low risk, supplemental fee income for liquidity providers, without an opportunity cost.',
site: 'https://xdai.curve.fi/',
// eslint-disable-next-line max-len
description: 'Curve is an exchange liquidity pool on Ethereum designed for: extremely efficient stablecoin trading, low risk, supplemental fee income for liquidity providers, without an opportunity cost.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'honwyswap',
title: 'HonwySwap',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
// eslint-disable-next-line max-len
shortDescription: 'Honeyswap is a decentralized exchange built on the Gnosis Chain, this enables users to experience fast and secure transactions with incredibly low fees. Multiple tokens are available with which you can swap and add liquidity.',
site: 'https://honeyswap.org/',
// eslint-disable-next-line max-len
description: 'Honeyswap is a decentralized exchange built on the Gnosis Chain, this enables users to experience fast and secure transactions with incredibly low fees. Multiple tokens are available with which you can swap and add liquidity.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'sushi',
title: 'Sushi',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'Swap, yield, lend, borrow, leverage, limit, launch all on one community-driven ecosystem',
site: 'https://app.sushi.com/',
description: 'Swap, yield, lend, borrow, leverage, limit, launch all on one community-driven ecosystem',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'bao-finance',
title: 'Bao Finance',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'Yield Farming for Synthetic Assets from LP tokens',
site: 'https://farms.baoswap.xyz/',
description: 'Yield Farming for Synthetic Assets from LP tokens',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'component',
title: 'Component',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
// eslint-disable-next-line max-len
shortDescription: 'During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.',
site: 'https://xdai.component.finance',
// eslint-disable-next-line max-len
description: 'During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'pooltogether',
title: 'PoolTogether',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
shortDescription: 'View, deposit and withdraw for all V3 Pools',
site: 'https://app.pooltogether.com/',
description: 'View, deposit and withdraw for all V3 Pools',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'swapr',
title: 'Swapr',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'A governance-enabled automated market maker with adjustable fees.',
site: 'https://swapr.eth.limo',
description: 'A governance-enabled automated market maker with adjustable fees.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'levinswap',
title: 'Levinswap',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
// eslint-disable-next-line max-len
shortDescription: 'AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.',
site: 'https://app.levinswap.org/',
description: 'AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'omen',
title: 'Omen',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
shortDescription: 'Decentralized prediction markets on Ethereum',
site: 'https://xdai.omen.eth.link/',
description: 'Decentralized prediction markets on Ethereum',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'nifty-ink',
title: 'Nifty Ink',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
shortDescription: 'NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum',
site: 'https://nifty.ink/explore',
description: 'NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'treasure-chess',
title: 'Treasure Chess',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'Every chess game is one-of-a-kind. Make yours a collectible.',
site: 'https://treasure.chess.com/',
description: 'Every chess game is one-of-a-kind. Make yours a collectible.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'unique-one',
title: 'Unique.One',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
// eslint-disable-next-line max-len
shortDescription: 'A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.',
site: 'https://www.unique.one/',
// eslint-disable-next-line max-len
description: 'A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'cold-truth-culture',
title: 'Cold Truth Culture',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
shortDescription: 'A Community That Empowers NFT Artists',
site: 'https://www.coldtruthculture.io/',
description: 'A Community That Empowers NFT Artists',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'xdai-bridge',
title: 'xDai Bridge',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
shortDescription: 'Token bridge between the Gnosis Chain and the Ethereum network',
site: 'https://bridge.gnosischain.com/',
description: 'Token bridge between the Gnosis Chain and the Ethereum network',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'omni-bridge',
title: 'OmniBridge',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.',
site: 'https://omni.gnosischain.com/bridge',
description: 'The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'gnosis-safe',
title: 'Gnosis Safe',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[0], APP_CATEGORIES[1] ],
shortDescription: 'Gnosis Safe is the most trusted platform to manage digital assets on Ethereum',
site: 'https://gnosis-safe.io/',
description: 'Gnosis Safe is the most trusted platform to manage digital assets on Ethereum',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'multisender',
title: 'Multisender',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
// eslint-disable-next-line max-len
shortDescription: 'Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.',
site: 'https://multisender.app/',
// eslint-disable-next-line max-len
description: 'Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'disperse',
title: 'Disperse',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
shortDescription: 'Distribute ether or tokens to multiple addresses',
site: 'https://disperse.app/',
description: 'Distribute ether or tokens to multiple addresses',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
{
author: 'xDaichain',
id: 'symmetric',
title: 'Symmetric',
logo: 'https://www.fillmurray.com/144/144',
categories: [ APP_CATEGORIES[1], APP_CATEGORIES[2] ],
// eslint-disable-next-line max-len
shortDescription: 'DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.',
site: 'https://symmetric.finance/',
// eslint-disable-next-line max-len
description: 'DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.',
url: 'https://blockscout-allowance-mainnet-stage.vercel.app/',
twitter: 'https://twitter.com/EasyStaking',
telegram: 'https://t.me/easystaking',
github: 'https://github.com/mikhin',
},
];
[
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "easy-staking",
"title": "Easy Staking Revoke.cash",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "Accessible DeFi staking platform for STAKE holders on Ethereum",
"site": "https://easy-staking.xdaichain.com/",
"description": "Easy Staking allows users to place STAKE into a contract and receive STAKE emissions on Ethereum. It provides an accessible staking mechanism for users and increases STAKE utility and DeFi composability. EasyStaking also:\n\n- Incentivizes liquidity providers on decentralized exchanges through unique reward mechanisms\n- Creates staking opportunities via hardware wallets and other Ethereum applications\n- Provides staking opportunities with no minimum STAKE requirements to participate\n- Limits total circulating supply",
"url": "https://revoke.cash",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "curve",
"title": "Curve Revoke.cash",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "Curve is an exchange liquidity pool on Ethereum designed for: extremely efficient stablecoin trading, low risk, supplemental fee income for liquidity providers, without an opportunity cost.",
"site": "https://xdai.curve.fi/",
"description": "Curve is an exchange liquidity pool on Ethereum designed for: extremely efficient stablecoin trading, low risk, supplemental fee income for liquidity providers, without an opportunity cost.",
"url": "https://revoke.cash",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "honwyswap",
"title": "HonwySwap Revoke.cash",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "Honeyswap is a decentralized exchange built on the Gnosis Chain, this enables users to experience fast and secure transactions with incredibly low fees. Multiple tokens are available with which you can swap and add liquidity.",
"site": "https://honeyswap.org/",
"description": "Honeyswap is a decentralized exchange built on the Gnosis Chain, this enables users to experience fast and secure transactions with incredibly low fees. Multiple tokens are available with which you can swap and add liquidity.",
"url": "https://revoke.cash",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "sushi",
"title": "Sushi Revoke.cash",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "Swap, yield, lend, borrow, leverage, limit, launch all on one community-driven ecosystem",
"site": "https://app.sushi.com/",
"description": "Swap, yield, lend, borrow, leverage, limit, launch all on one community-driven ecosystem",
"url": "https://revoke.cash",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"100"
],
"author": "xDaichain",
"id": "bao-finance",
"title": "Bao Finance",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "Yield Farming for Synthetic Assets from LP tokens",
"site": "https://farms.baoswap.xyz/",
"description": "Yield Farming for Synthetic Assets from LP tokens",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "component",
"title": "Component",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.",
"site": "https://xdai.component.finance",
"description": "During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "pooltogether",
"title": "PoolTogether",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "View, deposit and withdraw for all V3 Pools",
"site": "https://app.pooltogether.com/",
"description": "View, deposit and withdraw for all V3 Pools",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "swapr",
"title": "Swapr",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "A governance-enabled automated market maker with adjustable fees.",
"site": "https://swapr.eth.limo",
"description": "A governance-enabled automated market maker with adjustable fees.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "levinswap",
"title": "Levinswap",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.",
"site": "https://app.levinswap.org/",
"description": "AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "omen",
"title": "Omen",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "Decentralized prediction markets on Ethereum",
"site": "https://xdai.omen.eth.link/",
"description": "Decentralized prediction markets on Ethereum",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "nifty-ink",
"title": "Nifty Ink",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum",
"site": "https://nifty.ink/explore",
"description": "NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "treasure-chess",
"title": "Treasure Chess",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "Every chess game is one-of-a-kind. Make yours a collectible.",
"site": "https://treasure.chess.com/",
"description": "Every chess game is one-of-a-kind. Make yours a collectible.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "unique-one",
"title": "Unique.One",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.",
"site": "https://www.unique.one/",
"description": "A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "cold-truth-culture",
"title": "Cold Truth Culture",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "A Community That Empowers NFT Artists",
"site": "https://www.coldtruthculture.io/",
"description": "A Community That Empowers NFT Artists",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "xdai-bridge",
"title": "xDai Bridge",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "Token bridge between the Gnosis Chain and the Ethereum network",
"site": "https://bridge.gnosischain.com/",
"description": "Token bridge between the Gnosis Chain and the Ethereum network",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "omni-bridge",
"title": "OmniBridge",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.",
"site": "https://omni.gnosischain.com/bridge",
"description": "The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "gnosis-safe",
"title": "Gnosis Safe",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"defi",
"exchanges"
],
"shortDescription": "Gnosis Safe is the most trusted platform to manage digital assets on Ethereum",
"site": "https://gnosis-safe.io/",
"description": "Gnosis Safe is the most trusted platform to manage digital assets on Ethereum",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "multisender",
"title": "Multisender",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.",
"site": "https://multisender.app/",
"description": "Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"1"
],
"author": "xDaichain",
"id": "disperse",
"title": "Disperse",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "Distribute ether or tokens to multiple addresses",
"site": "https://disperse.app/",
"description": "Distribute ether or tokens to multiple addresses",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
},
{
"chainIds": [
"99"
],
"author": "xDaichain",
"id": "symmetric",
"title": "Symmetric",
"logo": "https://www.fillmurray.com/144/144",
"categories": [
"exchanges",
"finance"
],
"shortDescription": "DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.",
"site": "https://symmetric.finance/",
"description": "DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/",
"twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin"
}
]
/* eslint-disable max-len */
export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'success',
block_num: 15006918,
confirmation_num: 283,
confirmation_duration: 30,
timestamp: 1662623567695,
address_from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
address_to: '0x35317007D203b8a86CA727ad44E473E40450E378',
amount: {
value: 0.03,
value_usd: 35.5,
},
fee: {
value: 0.002395904453623692,
value_usd: 2.84,
},
gas_price: 0.000000017716513811,
gas_limit: 208420,
gas_used: 159319,
gas_fees: {
base: 13.538410068,
max: 20.27657523,
max_priority: 1.5,
},
burnt_fees: {
value: 0.002156925953623692,
value_usd: 2.55,
},
type: {
value: '2',
eip: 'EIP-1559',
},
nonce: 4,
position: 342,
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: 'USDT', amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: 'TOKE', amount: 76.1851851851846, usd: 194.05 },
],
};
export const data = [
{
id: 1,
type: 'call',
status: 'success' as const,
from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e',
value: 0.25207646303,
gasLimit: 369472,
},
{
id: 2,
type: 'delegate call',
status: 'error' as const,
from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
value: 0.5633333,
gasLimit: 340022,
},
{
id: 3,
type: 'static call',
status: 'success' as const,
from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
to: '0x35317007D203b8a86CA727ad44E473E40450E378',
value: 0.421152366,
gasLimit: 509333,
},
];
export const data = [
{
address: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
topics: [
{ hex: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' },
{ hex: '0x000000000000000000000000def171fe48cf0115b1d80b88dc8eab59176fee57' },
{ hex: '0x000000000000000000000000c465c0a16228ef6fe1bf29c04fdb04bb797fd537' },
],
data: '0x000000000000000000000000000000000000000000000000019faae14eb88000',
},
{
address: '0x73968b9a57c6e53d41345fd57a6e6ae27d6cdb2f',
topics: [
{ hex: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' },
{ hex: '0x000000000000000000000000c465c0a16228ef6fe1bf29c04fdb04bb797fd537' },
{ hex: '0x0000000000000000000000008453d9385af5f49edad9905345cd2411b5c5831b' },
],
data: '0x000000000000000000000000000000000000000000000013b6ee62022c95ced4',
},
];
declare module 'react-identicons'
declare module 'data/marketplaceApps.json' {
import type { AppItemOverview } from './types/client/apps';
const value: Array<AppItemOverview>;
export default value;
}
......@@ -23,10 +23,10 @@ function replace_envs {
envValue=$(env | grep "^$configName=" | grep -oe '[^=]*$');
# if config found
if [ -n "$configValue" ] && [ -n "$envValue" ]; then
if [ -n "$configValue" ]; then
# replace all
echo "Replace: ${configValue} with: ${envValue}"
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#$envValue#g"
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#${envValue-''}#g"
fi
done < $envFilename
}
......
blockscout:
environment:
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD:
_default: ENC[AES256_GCM,data:aW52T8giY6Be3LMtiYJ7tJ0=,iv:vbHsPHzC/TI0wxwO1LgUuY7RWHUEpxP17b4NzcxH9Fc=,tag:kqqnsrIb+k8S+FUCAv2Y7Q==,type:str]
MAILSLURP_API_KEY:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID:
_default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:C6TAdc/RZ+qFOan+tWnTsfIDcYgxCNjuKOU=,iv:tEcHTumHxA9LreqLSwjcTTCP3igk/VQrJn/OWd+YQ4c=,tag:XOlWi3XHBZyENtLsv3afmw==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:lojxfDVJBi1Sc6KCL28tln6RJ5leg0jmaTbSHbBd1H4=,iv:l1Iq/YsZKkFuorQgKd+KzAIkqbXHv6I4eid+7LDnyOA=,tag:/c7GBciDCruz+dAOfxncww==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:9cWXw4j6LAg9qUzKTGevJE4U1leMV+i9DD26VDyFqlin4M6O1r/xzoWQtHfrHzxP1RRBuGPIjBqV/WCM1u9cVmhgrs2FQvzZkHk=,iv:Qrxdp4CfnSkCXMxhzMZvvpFSkglzdviq4UYGhffLDNk=,tag:RJhq0+B9Sa5D8k8cAhZW5A==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER:
_default: ENC[AES256_GCM,data:68YVwUmudMo8n+1FENmPunPwLFSCgg==,iv:oALjuhqudA8zfOdse5Hd4IaLvEtY4VJ7exwUGsEPWFA=,tag:8F+rS2HZSfqCCXY4MkdTXw==,type:str]
ACCOUNT_SENDGRID_TEMPLATE:
_default: ENC[AES256_GCM,data:teIknLFnYfGDD7drMUTvcoXBxR9rOjc8L8nOVtMYVOuzXQ==,iv:LoBlBjrWDf5/LsmBo58510evXBTxiIW1YekeLmB+Mow=,tag:dZ/YdphM5MxEygeFYlT0Nw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL:
_default: ENC[AES256_GCM,data:Y5Un1IwYz6luANlNDtbz6V9cdQ96sZFPbtS5MzDBPDFP8pBlQouhYeDBvB6irf2KiC3gYPHLea5lSWA=,iv:QH0A4zi/iWYka9n0SD2v+NXijNPyg0MPrGVawDzf5fU=,tag:wjUYXk943EXXF+++oNpLPw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY:
_default: ENC[AES256_GCM,data:WBxTPe+pQ2VzSPRmL3mzek0=,iv:0wYDv0BRmXrmz9EJg0cBclHpsPEQukO7tCnPAUlCKrs=,tag:3Zcyf/8kLU6ohUQ+97eONg==,type:str]
ACCOUNT_CLOAK_KEY:
_default: ENC[AES256_GCM,data:/+0GBBYW34fDHIAPpHCbNucu2FUH9VAI+wBV6dLHxvYk0whtXikiClentSk=,iv:ekOPgF0H/0/MG++cLi0gH+AzqWafkqFL9R7B8G8jPo4=,tag:QcI69GzicGVV96+EDOR81w==,type:str]
SECRET_KEY_BASE:
_default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str]
SECRET_KEY_GUARDIAN:
_default: ENC[AES256_GCM,data:vpsqZh2sBKUiG4LtTVZyi3qBUU2EMPIrAp9k4X4iSYNvJM7iDGjq/du3EmsQCccvlr54x3opyypHYsdA7gL2Cg==,iv:9q8ePtwr2CDieE2ZU3UyxJ1wtKzIOY1oJ7rZmXCcWWM=,tag:BFO+PztjAOfQHh9mpC/ZJA==,type:str]
DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:48q2fFA3s1HGSm7YB5QV8B0eiI2PZHPgOsUS84R8x+8GecrFTenifbcHD6AivFNKovLuClUy2aFuEtEvJDOfelY=,iv:5ZLl62Yj/AXwyg+pdBLUv3EyYXSTOiSCqGYOv3rBx94=,tag:FUiy0haST4mdrqe2VlvrLg==,type:str]
ACCOUNT_REDIS_URL:
_default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
_default: ENC[AES256_GCM,data:u9zFU9th8ShQ5HIaehYgMzOorx7Tfg7K,iv:ytd6IoU8PIMqbZ4W7kgkL5S4KV4MbvHfxMH2IthQnYE=,tag:0kgXuYSBZZp0LHG3m5EfYg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__SECRET_KEY:
_default: ENC[AES256_GCM,data:t78wGcNs4gqMav1TJaIyBUE4vxsaXloMSmPobix2zvpL2MfNl/6ERA==,iv:IdQot/B7QH9mJWFypTn9OIS+WOeq+B5KZYuYMkQ8+G4=,tag:xXX0WIIM5enPGaibiywtBg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__BUCKET:
_default: ENC[AES256_GCM,data:495QOb9qNhVugnOtaw==,iv:4h9S63+0P5qzeObiu5LJ3YKnFboSJ6WDE8tPnavySxg=,tag:ld3u6nYJH4qv3eN/pon5ow==,type:str]
postgres:
environment:
POSTGRES_PASSWORD:
_default: ENC[AES256_GCM,data:+DPLqOzpvlY=,iv:W6jbwxXI2NTb+kc7pGw0GPUpIPvgIfrNW9Uq5f64gS4=,tag:TV2bMILH/mYmUi0CxeYl2A==,type:str]
POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
geth:
files:
list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
init.sh: ENC[AES256_GCM,data:2qHHfwC63yJlxqaqmQQ69CxtcYZiEgdLdr/Hcxk8eKEou16bXL1NxpxfW8MWsfZyB4S3QNj05V6WSpZAw9ITG/GnXOKocZqKbZPmmuwQpLoekC37EsdUETb+tFvIunh9xRmgst4/ByXEyjC9pZk8SN0Qr9cGQl3VMzeGSkRndaE+qnlFxHRO5ELbDyaLMcyLWaQncescCSvIDpyOPNN7NwS3PYW+Nq3rvh4y+2FBoHUPUMIq46q94+ROnFk5TQsUI5/sVdw/5fuJklcTeqxuo4pPFLAgtkXkab0k2Og0eMI4It1xIrwpfJnxKwbjd0zljKo8tNzjpPZKHMfYchPX6uGH9fnwvXcH7nKUUdtSWrWa8R9a5AyO7SJ0hfzewMzvOWspihJW3JXFTvfesoG/cMMBDg3icsD/0HyxVBQP2riSZsou3hzDFAOVoNLAQTNRVXckSxk9zbx0aOy3DlWefJhIFj2ZmF4sliwxMe+Oy/So6PfiRLsP9v6/OTv6sriExtHdAx6xeA+dbYCO9ruW+5gNB2UUi3DQvpvejaxysJygaHvYCVDGAUKCxDAvQPrUQ/r5+UaE+GmqflDzhd9lhAptL/xL8tW8R+v9RZdMO/G9kxWMhKWiHH6QqgdYGOvanwCxAGtSvKhqf6fiWnNMr6wKG4qAFk6cMNUsLHtnC1qLYhZAfBa1/aK5cG0IbygZKurC+9G8x4SmOqUpW8h23XRsNd8peCRuSZ2uSGqss/1XvngfW0avzGiXMHW9,iv:ldcn5mGmfGmSmq30mUen6diWK6EAEnYF6NQTXXwY8yA=,tag:kSuu7AGzzmSEM5AzjIw5RQ==,type:str]
password.txt: ""
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:IVCGBLqP7IdmbDe9UbIFvJSBD7+g52chKzakELt2XuHDp9JvC4E+7xxp,iv:bDHp2llHAqhgI5N8swQALSDc6X3S0JCsXbJnEEDDJOc=,tag:Z1WsJXkqtq79WydIgUiDiA==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:6MLOmBMJoB+dYoW8L4JYslO3F5tSFzhrkI+6rGo1a51s9sXZa9c=,iv:126unfUWSieigaq4Zne8321tSYoNy3EHk/qwEodqgH8=,tag:6BlrNkck5yANO2rqECjkuQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-14T12:09:11Z"
mac: ENC[AES256_GCM,data:CnD+9hsC9ZyVhZPo+DXZfPH8svMuk50llaAm3JxgOlzhbJ4yp969WxLhZSORvj520b9geBPLZRU7ujLGiHKhrNzAK438LI2QttKQDt3WSbPwkIGDh/zuA201+gpT73awUNfMKCoHVjq4iQ6ty4KP/NCw1ZMcS/c1WVuRYE9RTl8=,iv:rl8eKiXwrBDjns2hiwJ6f28XyuhjH2soHeR1MBBu2Ig=,tag:vu5jGEPMkvmcl7m8huWl7g==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA1MXzg1c4SMLAQgAoRceoDDpqXEbiz6DaMX7YS7j5mcp+xVoemU9qY4ln7dT
XtwAWiRVr7mf1ZDI77bQunbyWAU3zM/lFDsrlNUMAUGZzoOOtIGSnjCqCYB5JGiP
ZfdjG88RAJx+WMIgWl66PW1ceru8We+KauQyG7bD5g59g3b5RadEhH3VER4cYJof
VI0+NPpAcGciRsV3vxGGf4q1ppM3Pz0AnXcC+HB+hxWa5DeAhZlavFO2zYBCzw/o
ypyLCoOcuEDSj9AY+EYnjyXIS72DXPA953/8QSaMSZ9JVKG5imtXslGEdj9PIQOp
0rPqjZCxzydQaPZ68jXMg2Ci4gZT4ZPle9fRFxNGdtJeAScCUk+5L70orvNmWwgD
3fFnMY6tMDd/qSqSRFEJ0Vm0M4MYSg5mgW9M64zGfw1bZLLjsIGMe2ZqQC/sZh1O
Mrk3/xd4md/Ko8BQcaZ6lCi0olz6KWTzmQhXgNTx9A==
=mNH9
-----END PGP MESSAGE-----
fp: 99E83B7490B1A9F51781E6055317CE0D5CE1230B
unencrypted_suffix: _unencrypted
version: 3.7.3
global:
env: e2e
# enable Blockscout deploy
blockscout:
enabled: true
image:
_default: blockscout/blockscout:latest
replicas:
app: 1
docker:
port: 80
targetPort: 4000
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
# probes
livenessProbe:
enabled: true
path: /
readinessProbe:
enabled: true
path: /
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "2"
requests:
memory:
_default: "1Gi"
cpu:
_default: "2"
# enable service to connect to RDS
rds:
enable: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
nodeSelector:
enabled: true
app: blockscout
# Blockscout environment variables
environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546
COIN:
_default: DAI
MIX_ENV:
_default: prod
ECTO_USE_SSL:
_default: 'false'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: true
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true'
postgres:
enabled: true
image: postgres:13.8
port: 5432
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "1"
requests:
memory:
_default: "1Gi"
cpu:
_default: "1"
environment:
POSTGRES_USER:
_default: 'postgres'
POSTGRES_HOST_AUTH_METHOD:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
image:
_default: ethereum/client-go:stable
replicas:
app: 1
portHttp: 8545
portWs: 8546
portAuth: 8551
command: '["sh","./root/init.sh"]'
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]'
environment: {}
persistence:
enabled: false
resources:
limits:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
jwt:
enabled: false
files:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
app: 1
docker:
port: 80
targetPort: 8043
metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
requests:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
app: blockscout
# probes
livenessProbe:
enabled: true
path: /health
readinessProbe:
enabled: true
path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__ADDR:
_default: 0.0.0.0:8043
# SMART_CONTRACT_VERIFIER__SOLIDITY__ENABLED:
# _default: 'true'
SMART_CONTRACT_VERIFIER__SOLIDITY__COMPILERS_DIR:
_default: /tmp/solidity-compilers
SMART_CONTRACT_VERIFIER__SOLIDITY__REFRESH_VERSIONS_SCHEDULE:
_default: 0 0 * * * * *
# It depends on the OS you are running the service on
# SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL:
# _default: https://solc-bin.ethereum.org/linux-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/macosx-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/windows-amd64/list.json
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__REGION:
_default: ""
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ENDPOINT:
_default: https://storage.googleapis.com
SMART_CONTRACT_VERIFIER__SOURCIFY__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__SOURCIFY__API_URL:
_default: https://sourcify.dev/server/
SMART_CONTRACT_VERIFIER__SOURCIFY__VERIFICATION_ATTEMPTS:
_default: 3
SMART_CONTRACT_VERIFIER__SOURCIFY__REQUEST_TIMEOUT:
_default: 10
SMART_CONTRACT_VERIFIER__METRICS__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__METRICS__ADDR:
_default: 0.0.0.0:6060
SMART_CONTRACT_VERIFIER__METRICS__ROUTE:
_default: /metrics
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_APP_PROTOCOL:
_default: http
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_INSTANCE:
_default: local
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
NEXT_PUBLIC_NETWORK_ID:
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: SPOA
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
---
creation_rules:
- path_regex: ^(.+/)?secrets\.yaml$
pgp: >-
99E83B7490B1A9F51781E6055317CE0D5CE1230B
blockscout:
environment:
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD:
_default: ENC[AES256_GCM,data:aW52T8giY6Be3LMtiYJ7tJ0=,iv:vbHsPHzC/TI0wxwO1LgUuY7RWHUEpxP17b4NzcxH9Fc=,tag:kqqnsrIb+k8S+FUCAv2Y7Q==,type:str]
MAILSLURP_API_KEY:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID:
_default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:C6TAdc/RZ+qFOan+tWnTsfIDcYgxCNjuKOU=,iv:tEcHTumHxA9LreqLSwjcTTCP3igk/VQrJn/OWd+YQ4c=,tag:XOlWi3XHBZyENtLsv3afmw==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:lojxfDVJBi1Sc6KCL28tln6RJ5leg0jmaTbSHbBd1H4=,iv:l1Iq/YsZKkFuorQgKd+KzAIkqbXHv6I4eid+7LDnyOA=,tag:/c7GBciDCruz+dAOfxncww==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:9cWXw4j6LAg9qUzKTGevJE4U1leMV+i9DD26VDyFqlin4M6O1r/xzoWQtHfrHzxP1RRBuGPIjBqV/WCM1u9cVmhgrs2FQvzZkHk=,iv:Qrxdp4CfnSkCXMxhzMZvvpFSkglzdviq4UYGhffLDNk=,tag:RJhq0+B9Sa5D8k8cAhZW5A==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER:
_default: ENC[AES256_GCM,data:68YVwUmudMo8n+1FENmPunPwLFSCgg==,iv:oALjuhqudA8zfOdse5Hd4IaLvEtY4VJ7exwUGsEPWFA=,tag:8F+rS2HZSfqCCXY4MkdTXw==,type:str]
ACCOUNT_SENDGRID_TEMPLATE:
_default: ENC[AES256_GCM,data:teIknLFnYfGDD7drMUTvcoXBxR9rOjc8L8nOVtMYVOuzXQ==,iv:LoBlBjrWDf5/LsmBo58510evXBTxiIW1YekeLmB+Mow=,tag:dZ/YdphM5MxEygeFYlT0Nw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL:
_default: ENC[AES256_GCM,data:Y5Un1IwYz6luANlNDtbz6V9cdQ96sZFPbtS5MzDBPDFP8pBlQouhYeDBvB6irf2KiC3gYPHLea5lSWA=,iv:QH0A4zi/iWYka9n0SD2v+NXijNPyg0MPrGVawDzf5fU=,tag:wjUYXk943EXXF+++oNpLPw==,type:str]
ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY:
_default: ENC[AES256_GCM,data:WBxTPe+pQ2VzSPRmL3mzek0=,iv:0wYDv0BRmXrmz9EJg0cBclHpsPEQukO7tCnPAUlCKrs=,tag:3Zcyf/8kLU6ohUQ+97eONg==,type:str]
ACCOUNT_CLOAK_KEY:
_default: ENC[AES256_GCM,data:/+0GBBYW34fDHIAPpHCbNucu2FUH9VAI+wBV6dLHxvYk0whtXikiClentSk=,iv:ekOPgF0H/0/MG++cLi0gH+AzqWafkqFL9R7B8G8jPo4=,tag:QcI69GzicGVV96+EDOR81w==,type:str]
SECRET_KEY_BASE:
_default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str]
SECRET_KEY_GUARDIAN:
_default: ENC[AES256_GCM,data:vpsqZh2sBKUiG4LtTVZyi3qBUU2EMPIrAp9k4X4iSYNvJM7iDGjq/du3EmsQCccvlr54x3opyypHYsdA7gL2Cg==,iv:9q8ePtwr2CDieE2ZU3UyxJ1wtKzIOY1oJ7rZmXCcWWM=,tag:BFO+PztjAOfQHh9mpC/ZJA==,type:str]
DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:48q2fFA3s1HGSm7YB5QV8B0eiI2PZHPgOsUS84R8x+8GecrFTenifbcHD6AivFNKovLuClUy2aFuEtEvJDOfelY=,iv:5ZLl62Yj/AXwyg+pdBLUv3EyYXSTOiSCqGYOv3rBx94=,tag:FUiy0haST4mdrqe2VlvrLg==,type:str]
ACCOUNT_REDIS_URL:
_default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
_default: ENC[AES256_GCM,data:u9zFU9th8ShQ5HIaehYgMzOorx7Tfg7K,iv:ytd6IoU8PIMqbZ4W7kgkL5S4KV4MbvHfxMH2IthQnYE=,tag:0kgXuYSBZZp0LHG3m5EfYg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__SECRET_KEY:
_default: ENC[AES256_GCM,data:t78wGcNs4gqMav1TJaIyBUE4vxsaXloMSmPobix2zvpL2MfNl/6ERA==,iv:IdQot/B7QH9mJWFypTn9OIS+WOeq+B5KZYuYMkQ8+G4=,tag:xXX0WIIM5enPGaibiywtBg==,type:str]
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__BUCKET:
_default: ENC[AES256_GCM,data:495QOb9qNhVugnOtaw==,iv:4h9S63+0P5qzeObiu5LJ3YKnFboSJ6WDE8tPnavySxg=,tag:ld3u6nYJH4qv3eN/pon5ow==,type:str]
postgres:
environment:
POSTGRES_PASSWORD:
_default: ENC[AES256_GCM,data:+DPLqOzpvlY=,iv:W6jbwxXI2NTb+kc7pGw0GPUpIPvgIfrNW9Uq5f64gS4=,tag:TV2bMILH/mYmUi0CxeYl2A==,type:str]
POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
geth:
files:
list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
init.sh: ENC[AES256_GCM,data:2qHHfwC63yJlxqaqmQQ69CxtcYZiEgdLdr/Hcxk8eKEou16bXL1NxpxfW8MWsfZyB4S3QNj05V6WSpZAw9ITG/GnXOKocZqKbZPmmuwQpLoekC37EsdUETb+tFvIunh9xRmgst4/ByXEyjC9pZk8SN0Qr9cGQl3VMzeGSkRndaE+qnlFxHRO5ELbDyaLMcyLWaQncescCSvIDpyOPNN7NwS3PYW+Nq3rvh4y+2FBoHUPUMIq46q94+ROnFk5TQsUI5/sVdw/5fuJklcTeqxuo4pPFLAgtkXkab0k2Og0eMI4It1xIrwpfJnxKwbjd0zljKo8tNzjpPZKHMfYchPX6uGH9fnwvXcH7nKUUdtSWrWa8R9a5AyO7SJ0hfzewMzvOWspihJW3JXFTvfesoG/cMMBDg3icsD/0HyxVBQP2riSZsou3hzDFAOVoNLAQTNRVXckSxk9zbx0aOy3DlWefJhIFj2ZmF4sliwxMe+Oy/So6PfiRLsP9v6/OTv6sriExtHdAx6xeA+dbYCO9ruW+5gNB2UUi3DQvpvejaxysJygaHvYCVDGAUKCxDAvQPrUQ/r5+UaE+GmqflDzhd9lhAptL/xL8tW8R+v9RZdMO/G9kxWMhKWiHH6QqgdYGOvanwCxAGtSvKhqf6fiWnNMr6wKG4qAFk6cMNUsLHtnC1qLYhZAfBa1/aK5cG0IbygZKurC+9G8x4SmOqUpW8h23XRsNd8peCRuSZ2uSGqss/1XvngfW0avzGiXMHW9,iv:ldcn5mGmfGmSmq30mUen6diWK6EAEnYF6NQTXXwY8yA=,tag:kSuu7AGzzmSEM5AzjIw5RQ==,type:str]
password.txt: ""
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:KVzLMvMQc6f37c+OWu7vpAdDYwKI048XVY7EVijS2zz6jRRFacg=,iv:LeWSYZeaPdBsOxcGcca8L1Rp3ilsR+R13icX8Q/VUBA=,tag:Bj6NTTqb3R8oxAprZ+7CVQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-14T12:08:39Z"
mac: ENC[AES256_GCM,data:esHv2aUvW4lMmoD5yRWu4OJEpOMkCa7TOyPS0HDkQL25g4TOdE+AfVZBE5wLeL1rePaId4bHnX0sF2Tov0d8xhCH3mv6+Vvmgi+75Oqu8+logQ4LyZSI0yIcvmdGVHhaO6u3u1qwYXHrityIVmiXQdBck5oq67uyT+jtSh1pXpc=,iv:YNWhRxSY0WLRm+wbVURpfxU2K67MAB5dpSILSMy9oCE=,tag:TsCcsGmMG4id5ITR5LrDjg==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA1MXzg1c4SMLAQgAoRceoDDpqXEbiz6DaMX7YS7j5mcp+xVoemU9qY4ln7dT
XtwAWiRVr7mf1ZDI77bQunbyWAU3zM/lFDsrlNUMAUGZzoOOtIGSnjCqCYB5JGiP
ZfdjG88RAJx+WMIgWl66PW1ceru8We+KauQyG7bD5g59g3b5RadEhH3VER4cYJof
VI0+NPpAcGciRsV3vxGGf4q1ppM3Pz0AnXcC+HB+hxWa5DeAhZlavFO2zYBCzw/o
ypyLCoOcuEDSj9AY+EYnjyXIS72DXPA953/8QSaMSZ9JVKG5imtXslGEdj9PIQOp
0rPqjZCxzydQaPZ68jXMg2Ci4gZT4ZPle9fRFxNGdtJeAScCUk+5L70orvNmWwgD
3fFnMY6tMDd/qSqSRFEJ0Vm0M4MYSg5mgW9M64zGfw1bZLLjsIGMe2ZqQC/sZh1O
Mrk3/xd4md/Ko8BQcaZ6lCi0olz6KWTzmQhXgNTx9A==
=mNH9
-----END PGP MESSAGE-----
fp: 99E83B7490B1A9F51781E6055317CE0D5CE1230B
unencrypted_suffix: _unencrypted
version: 3.7.3
global:
env: e2e
# enable Blockscout deploy
blockscout:
enabled: true
image:
_default: blockscout/blockscout:latest
replicas:
app: 1
docker:
port: 80
targetPort: 4000
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
# probes
livenessProbe:
enabled: true
path: /
readinessProbe:
enabled: true
path: /
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "2"
requests:
memory:
_default: "1Gi"
cpu:
_default: "2"
# enable service to connect to RDS
rds:
enable: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
nodeSelector:
enabled: true
app: blockscout
# Blockscout environment variables
environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546
COIN:
_default: DAI
MIX_ENV:
_default: prod
ECTO_USE_SSL:
_default: 'false'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: true
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true'
postgres:
enabled: true
image: postgres:13.8
port: 5432
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "1"
requests:
memory:
_default: "1Gi"
cpu:
_default: "1"
environment:
POSTGRES_USER:
_default: 'postgres'
POSTGRES_HOST_AUTH_METHOD:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
image:
_default: ethereum/client-go:stable
replicas:
app: 1
portHttp: 8545
portWs: 8546
portAuth: 8551
command: '["sh","./root/init.sh"]'
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]'
environment: {}
persistence:
enabled: false
resources:
limits:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
jwt:
enabled: false
files:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
app: 1
docker:
port: 80
targetPort: 8043
metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
requests:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
app: blockscout
# probes
livenessProbe:
enabled: true
path: /health
readinessProbe:
enabled: true
path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__ADDR:
_default: 0.0.0.0:8043
# SMART_CONTRACT_VERIFIER__SOLIDITY__ENABLED:
# _default: 'true'
SMART_CONTRACT_VERIFIER__SOLIDITY__COMPILERS_DIR:
_default: /tmp/solidity-compilers
SMART_CONTRACT_VERIFIER__SOLIDITY__REFRESH_VERSIONS_SCHEDULE:
_default: 0 0 * * * * *
# It depends on the OS you are running the service on
# SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL:
# _default: https://solc-bin.ethereum.org/linux-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/macosx-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/windows-amd64/list.json
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__REGION:
_default: ""
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ENDPOINT:
_default: https://storage.googleapis.com
SMART_CONTRACT_VERIFIER__SOURCIFY__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__SOURCIFY__API_URL:
_default: https://sourcify.dev/server/
SMART_CONTRACT_VERIFIER__SOURCIFY__VERIFICATION_ATTEMPTS:
_default: 3
SMART_CONTRACT_VERIFIER__SOURCIFY__REQUEST_TIMEOUT:
_default: 10
SMART_CONTRACT_VERIFIER__METRICS__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__METRICS__ADDR:
_default: 0.0.0.0:6060
SMART_CONTRACT_VERIFIER__METRICS__ROUTE:
_default: /metrics
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "0.3Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_INSTANCE:
_default: review
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: sokol
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
NEXT_PUBLIC_NETWORK_ID:
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: SPOA
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
frontend:
environment: null
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-09-15T15:33:05Z"
mac: ENC[AES256_GCM,data:Cs7TXLsmURtuCJtdkfnt1zc+4dJ/RpbY/EpaQ5e/je/L8dtv4Clms4fDfhYZ40cKda6QyoCGSiUxA9zkKgCy+7jhaC2Aqfmf3mBqFz4SmrRoStAVneaGUFEIT0BVIB0ORGVmppfla0qsES1bLW2/7lxHzswhYLNO8v2jfQ916B4=,iv:3s8f7KVuFfwZtA24SpTV+qC73YUdJXDKI/UV2kpA2Es=,tag:/ctNygsaqJKw/x3x4KF1rQ==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA1MXzg1c4SMLAQgAoRceoDDpqXEbiz6DaMX7YS7j5mcp+xVoemU9qY4ln7dT
XtwAWiRVr7mf1ZDI77bQunbyWAU3zM/lFDsrlNUMAUGZzoOOtIGSnjCqCYB5JGiP
ZfdjG88RAJx+WMIgWl66PW1ceru8We+KauQyG7bD5g59g3b5RadEhH3VER4cYJof
VI0+NPpAcGciRsV3vxGGf4q1ppM3Pz0AnXcC+HB+hxWa5DeAhZlavFO2zYBCzw/o
ypyLCoOcuEDSj9AY+EYnjyXIS72DXPA953/8QSaMSZ9JVKG5imtXslGEdj9PIQOp
0rPqjZCxzydQaPZ68jXMg2Ci4gZT4ZPle9fRFxNGdtJeAScCUk+5L70orvNmWwgD
3fFnMY6tMDd/qSqSRFEJ0Vm0M4MYSg5mgW9M64zGfw1bZLLjsIGMe2ZqQC/sZh1O
Mrk3/xd4md/Ko8BQcaZ6lCi0olz6KWTzmQhXgNTx9A==
=mNH9
-----END PGP MESSAGE-----
fp: 99E83B7490B1A9F51781E6055317CE0D5CE1230B
unencrypted_suffix: _unencrypted
version: 3.7.3
global:
env: testnet
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
host:
_default: bs-frontend.aws-k8s.blockscout.com
resources:
limits:
memory:
_default: 0.3Gi
cpu:
_default: "0.2"
requests:
memory:
_default: 0.3Gi
cpu:
_default: "0.2"
# node label
nodeSelector:
app: blockscout
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},{"name":"Optimism on Gnosis Chain","type":"xdai","subType":"optimism","group":"mainnets"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","type":"lukso","subType":"l14","group":"other"}]'
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.535 11.293a1 1 0 0 0 0 1.414l3.536 3.536a1 1 0 1 1-1.414 1.414l-4.95-4.95a1 1 0 0 1 0-1.414l4.95-4.95a1 1 0 1 1 1.414 1.414l-3.536 3.536Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m11.003 6.276-5.267 5.267a.667.667 0 0 1-.943-.943l5.266-5.267H5.67A.667.667 0 1 1 5.67 4h6.667v6.667a.667.667 0 0 1-1.333 0V6.276Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.774 13.088a.936.936 0 0 0 0 1.325l2.814 2.812a.935.935 0 0 0 1.324 0l2.813-2.812A.935.935 0 1 0 8.4 13.088l-1.213 1.21v-9.95a.954.954 0 0 0-.938-.937.954.954 0 0 0-.937.938v9.95L4.1 13.088a.937.937 0 0 0-1.327 0Zm10.039 2.538a.937.937 0 1 0 1.875 0V5.702l1.213 1.21a.935.935 0 1 0 1.324-1.324l-2.812-2.813a.936.936 0 0 0-1.325 0l-2.812 2.813A.935.935 0 1 0 11.6 6.912l1.213-1.21v9.924Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#link_svg__a)">
<path d="M5.547 6.073c1.303-1.303 3.469-1.303 4.772 0 1.171 1.172 1.345 3.04.382 4.387l-.026.038a.75.75 0 0 1-1.221-.872l.026-.038a1.89 1.89 0 0 0-2.874-2.435l-2.63 2.632c-.738.717-.738 1.934 0 2.672a1.887 1.887 0 0 0 2.433.202l.038-.047a.772.772 0 0 1 1.045.194.75.75 0 0 1-.173 1.048l-.038.026a3.389 3.389 0 0 1-4.366-5.154l2.632-2.653Zm6.914 5.833A3.388 3.388 0 0 1 7.307 7.54l.026-.038c.22-.335.689-.415 1.045-.173.338.22.417.689.176 1.045l-.026.038c-.537.731-.452 1.781.202 2.435a1.893 1.893 0 0 0 2.671 0l2.63-2.632c.738-.738.738-1.955 0-2.672a1.887 1.887 0 0 0-2.433-.202l-.037.026c-.338.242-.806.143-1.046-.174a.749.749 0 0 1 .174-1.046l.037-.026a3.389 3.389 0 0 1 4.367 5.153l-2.632 2.632Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="link_svg__a">
<path fill="#fff" transform="translate(1.504 3)" d="M0 0h15v12H0z"/>
</clipPath>
</defs>
</svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.868 1.55a.377.377 0 0 0-.378 0L3.188 6.363A.377.377 0 0 0 3 6.69v9.622c0 .135.072.26.188.327l8.302 4.811a.377.377 0 0 0 .378 0l8.302-4.811a.377.377 0 0 0 .188-.327V6.69a.377.377 0 0 0-.188-.327l-8.302-4.811ZM3.755 16.095V6.906l7.924-4.592 7.925 4.592v9.188l-7.925 4.592-7.924-4.592ZM8.66 6.972a.377.377 0 0 0-.754 0v6.354l-1.53-4.587a.377.377 0 0 0-.734.12v5.66a.377.377 0 0 0 .754 0v-3.335l1.529 4.586a.377.377 0 0 0 .735-.12V6.973Zm2.824-2.448a.377.377 0 0 1 .386-.003l2.262 1.32a.377.377 0 0 1-.38.652l-2.07-1.208-1.89 1.145v4.693h2.265a.377.377 0 0 1 0 .754H9.792v4.906a.377.377 0 0 1-.754 0V6.217c0-.132.069-.254.182-.323l2.264-1.37Zm3.195 2.06a.377.377 0 0 1 .515-.141l2.333 1.333a.377.377 0 1 1-.375.655l-.567-.324v7.544a.377.377 0 1 1-.755 0V7.676l-1.01-.578a.377.377 0 0 1-.141-.515Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m15.026 13.848 2.98 2.978a.834.834 0 1 1-1.18 1.18l-2.978-2.98a7.467 7.467 0 0 1-4.681 1.64c-4.14 0-7.5-3.36-7.5-7.5 0-4.14 3.36-7.5 7.5-7.5 4.14 0 7.5 3.36 7.5 7.5a7.466 7.466 0 0 1-1.641 4.681Zm-1.672-.619A5.814 5.814 0 0 0 15 9.167a5.832 5.832 0 0 0-5.833-5.834 5.831 5.831 0 0 0-5.834 5.834A5.832 5.832 0 0 0 9.167 15a5.814 5.814 0 0 0 4.062-1.646l.125-.125Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.76 17.333a.603.603 0 0 1-.294-.075L9 14.798l-4.467 2.46a.606.606 0 0 1-.663-.051.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.854-5.21-3.616-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.301c.09-.08.2-.131.316-.149l4.994-.76 2.234-4.74a.65.65 0 0 1 .232-.269.61.61 0 0 1 .666 0c.1.065.18.158.233.269l2.233 4.74 4.994.76c.117.018.226.07.316.149a.66.66 0 0 1 .193.3.69.69 0 0 1-.16.678l-3.614 3.69.853 5.21a.692.692 0 0 1-.14.537.634.634 0 0 1-.216.173.605.605 0 0 1-.265.061Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z" fill="#4A5568"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z"/>
</svg>
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5" fill="#D9DBE0"/>
<circle cx="5" cy="5" r="2.5" fill="#707886"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m4.7 14.167-.592.591a.833.833 0 0 0 0 1.175.833.833 0 0 0 1.175 0l.592-.591A.833.833 0 0 0 4.7 14.167ZM4.166 10a.833.833 0 0 0-.833-.833H2.5a.833.833 0 1 0 0 1.666h.833A.833.833 0 0 0 4.166 10ZM10 4.167a.833.833 0 0 0 .833-.834V2.5a.833.833 0 1 0-1.667 0v.833a.833.833 0 0 0 .834.834ZM4.7 5.875a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.592-.592a.833.833 0 0 0-1.175 1.175l.592.592Zm10 .242a.833.833 0 0 0 .583-.242l.592-.592A.832.832 0 1 0 14.7 4.108l-.534.592a.833.833 0 0 0 0 1.175.833.833 0 0 0 .55.242H14.7Zm2.8 3.05h-.834a.833.833 0 0 0 0 1.666h.834a.833.833 0 1 0 0-1.666ZM10 15.833a.833.833 0 0 0-.834.834v.833a.833.833 0 1 0 1.667 0v-.833a.833.833 0 0 0-.833-.834Zm5.3-1.666a.833.833 0 0 0-1.134 1.133l.592.592a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.633-.55ZM10 5.417A4.583 4.583 0 1 0 14.583 10 4.592 4.592 0 0 0 10 5.417Zm0 7.5a2.917 2.917 0 1 1 0-5.833 2.917 2.917 0 0 1 0 5.833Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="url(#toke_svg__a)" d="M0 0h20v20H0z"/>
<defs>
<pattern id="toke_svg__a" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#toke_svg__b" transform="scale(.03125)"/>
</pattern>
<image id="toke_svg__b" width="32" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAMbmlDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnluSkJAQIICAlNCbIFIDSAmhBZBeBBshCSSUGBOCir0sKrh2EQEbuiqi2FZA7NiVRbH3xYKKsi7qYkPlTUhA133le+f75t4/Z878p9yZ3HsAoH/gSaV5qDYA+ZICWUJ4MHN0WjqT9BQQAQ3QwVBgwePLpey4uGgAZeD+d3l3AyDK+1VnJdc/5/+r6AqEcj4AyFiIMwVyfj7ExwHAq/hSWQEARKXeanKBVIlnQ6wngwFCvEqJs1V4uxJnqvDhfpukBA7ElwHQoPJ4smwAtO5BPbOQnw15tD5D7CoRiCUA0IdBHMAX8QQQK2Mflp8/UYnLIbaH9lKIYTyAlfkdZ/bf+DMH+Xm87EGsyqtfNELEcmkeb+r/WZr/Lfl5igEftnBQRbKIBGX+sIa3cidGKTEV4i5JZkysstYQfxALVHUHAKWIFBHJKnvUhC/nwPoBA4hdBbyQKIhNIA6T5MVEq/WZWeIwLsRwt6BTxAXcJIgNIV4olIcmqm02yiYmqH2h9VkyDlutP8eT9ftV+nqgyE1mq/nfiIRcNT+mVSRKSoWYArF1oTglBmItiF3kuYlRapuRRSJOzICNTJGgjN8a4gShJDxYxY8VZsnCEtT2JfnygXyxjSIxN0aN9xWIkiJU9cFO8Xn98cNcsMtCCTt5gEcoHx09kItAGBKqyh17LpQkJ6p5PkgLghNUa3GKNC9ObY9bCvPClXpLiD3khYnqtXhKAdycKn48S1oQl6SKEy/K4UXGqeLBl4FowAEhgAkUcGSCiSAHiFu7GrrgL9VMGOABGcgGQuCs1gysSO2fkcBrIigCf0AkBPLBdcH9s0JQCPVfBrWqqzPI6p8t7F+RC55CnA+iQB78rehfJRn0lgKeQI34H955cPBhvHlwKOf/vX5A+03DhppotUYx4JFJH7AkhhJDiBHEMKIDbowH4H54NLwGweGGs3CfgTy+2ROeEtoIjwjXCe2E2xPEc2U/RDkKtEP+MHUtMr+vBW4LOT3xYNwfskNm3AA3Bs64B/TDxgOhZ0+o5ajjVlaF+QP33zL47mmo7ciuZJQ8hBxEtv9xpZajlucgi7LW39dHFWvmYL05gzM/+ud8V30BvEf9aIktxPZjZ7ET2HnsMNYAmNgxrBFrwY4o8eDuetK/uwa8JfTHkwt5xP/wN/BklZWUu9a6drp+Vs0VCKcUKA8eZ6J0qkycLSpgsuHbQcjkSvguw5hurm5uACjfNaq/r7fx/e8QxKDlm27e7wD4H+vr6zv0TRd5DIC93vD4H/yms2cBoKMJwLmDfIWsUKXDlRcC/Jegw5NmBMyAFbCH+bgBL+AHgkAoiASxIAmkgfEwehHc5zIwGUwHc0AxKAXLwGpQATaAzWA72AX2gQZwGJwAZ8BFcBlcB3fh7ukAL0E3eAd6EQQhITSEgRgh5ogN4oS4ISwkAAlFopEEJA3JQLIRCaJApiPzkFJkBVKBbEJqkL3IQeQEch5pQ24jD5FO5A3yCcVQKqqHmqK26HCUhbLRKDQJHYdmo5PQInQ+ugQtR6vRnWg9egK9iF5H29GXaA8GME3MALPAnDEWxsFisXQsC5NhM7ESrAyrxuqwJvicr2LtWBf2ESfiDJyJO8MdHIEn43x8Ej4TX4xX4NvxevwUfhV/iHfjXwk0ggnBieBL4BJGE7IJkwnFhDLCVsIBwml4ljoI74hEogHRjugNz2IaMYc4jbiYuI64m3ic2EZ8TOwhkUhGJCeSPymWxCMVkIpJa0k7ScdIV0gdpA8amhrmGm4aYRrpGhKNuRplGjs0jmpc0Xim0UvWJtuQfcmxZAF5KnkpeQu5iXyJ3EHupehQ7Cj+lCRKDmUOpZxSRzlNuUd5q6mpaanpoxmvKdacrVmuuUfznOZDzY9UXaojlUMdS1VQl1C3UY9Tb1Pf0mg0W1oQLZ1WQFtCq6GdpD2gfdBiaLlocbUEWrO0KrXqta5ovaKT6TZ0Nn08vYheRt9Pv0Tv0iZr22pztHnaM7UrtQ9q39Tu0WHojNCJ1cnXWayzQ+e8znNdkq6tbqiuQHe+7mbdk7qPGRjDisFh8BnzGFsYpxkdekQ9Oz2uXo5eqd4uvVa9bn1dfQ/9FP0p+pX6R/TbDTADWwOuQZ7BUoN9BjcMPg0xHcIeIhyyaEjdkCtD3hsONQwyFBqWGO42vG74yYhpFGqUa7TcqMHovjFu7GgcbzzZeL3xaeOuoXpD/Ybyh5YM3Tf0jglq4miSYDLNZLNJi0mPqZlpuKnUdK3pSdMuMwOzILMcs1VmR806zRnmAeZi81Xmx8xfMPWZbGYes5x5itltYWIRYaGw2GTRatFraWeZbDnXcrflfSuKFcsqy2qVVbNVt7W59Sjr6da11ndsyDYsG5HNGpuzNu9t7WxTbRfYNtg+tzO049oV2dXa3bOn2QfaT7Kvtr/mQHRgOeQ6rHO47Ig6ejqKHCsdLzmhTl5OYqd1Tm3DCMN8hkmGVQ+76Ux1ZjsXOtc6P3QxcIl2mevS4PJquPXw9OHLh58d/tXV0zXPdYvr3RG6IyJHzB3RNOKNm6Mb363S7Zo7zT3MfZZ7o/trDycPocd6j1ueDM9Rngs8mz2/eHl7ybzqvDq9rb0zvKu8b7L0WHGsxaxzPgSfYJ9ZPod9Pvp6+Rb47vP908/ZL9dvh9/zkXYjhSO3jHzsb+nP89/k3x7ADMgI2BjQHmgRyAusDnwUZBUkCNoa9IztwM5h72S/CnYNlgUfCH7P8eXM4BwPwULCQ0pCWkN1Q5NDK0IfhFmGZYfVhnWHe4ZPCz8eQYiIilgecZNryuVza7jdkd6RMyJPRVGjEqMqoh5FO0bLoptGoaMiR60cdS/GJkYS0xALYrmxK2Pvx9nFTYo7FE+Mj4uvjH+aMCJhesLZREbihMQdie+SgpOWJt1Ntk9WJDen0FPGptSkvE8NSV2R2j56+OgZoy+mGaeJ0xrTSekp6VvTe8aEjlk9pmOs59jisTfG2Y2bMu78eOPxeeOPTKBP4E3Yn0HISM3YkfGZF8ur5vVkcjOrMrv5HP4a/ktBkGCVoFPoL1whfJbln7Ui63m2f/bK7E5RoKhM1CXmiCvEr3MicjbkvM+Nzd2W25eXmrc7XyM/I/+gRFeSKzk10WzilIltUidpsbR9ku+k1ZO6ZVGyrXJEPk7eWKAHP+pbFPaKnxQPCwMKKws/TE6ZvH+KzhTJlJapjlMXTX1WFFb0yzR8Gn9a83SL6XOmP5zBnrFpJjIzc2bzLKtZ82d1zA6fvX0OZU7unN/mus5dMfeveanzmuabzp89//FP4T/VFmsVy4pvLvBbsGEhvlC8sHWR+6K1i76WCEoulLqWlpV+XsxffOHnET+X/9y3JGtJ61KvpeuXEZdJlt1YHrh8+wqdFUUrHq8ctbJ+FXNVyaq/Vk9Yfb7Mo2zDGsoaxZr28ujyxrXWa5et/VwhqrheGVy5u8qkalHV+3WCdVfWB62v22C6oXTDp43ijbc2hW+qr7atLttM3Fy4+emWlC1nf2H9UrPVeGvp1i/bJNvatydsP1XjXVOzw2TH0lq0VlHbuXPszsu7QnY11jnXbdptsLt0D9ij2PNib8beG/ui9jXvZ+2v+9Xm16oDjAMl9Uj91PruBlFDe2NaY9vByIPNTX5NBw65HNp22OJw5RH9I0uPUo7OP9p3rOhYz3Hp8a4T2SceN09ovnty9Mlrp+JPtZ6OOn3uTNiZk2fZZ4+d8z93+Lzv+YMXWBcaLnpdrG/xbDnwm+dvB1q9WusveV9qvOxzualtZNvRK4FXTlwNuXrmGvfaxesx19tuJN+4dXPszfZbglvPb+fdfn2n8E7v3dn3CPdK7mvfL3tg8qD6d4ffd7d7tR95GPKw5VHio7uP+Y9fPpE/+dwx/yntadkz82c1z92eH+4M67z8YsyLjpfSl71dxX/o/FH1yv7Vr38G/dnSPbq747Xsdd+bxW+N3m77y+Ov5p64ngfv8t/1vi/5YPRh+0fWx7OfUj896538mfS5/IvDl6avUV/v9eX39Ul5Ml7/pwAGB5qVBcCbbQDQ0gBgwL6NMkbVC/YLoupf+xH4T1jVL/aLFwB18Ps9vgt+3dwEYM8W2H5BfjrsVeNoACT5ANTdfXCoRZ7l7qbiosI+hfCgr+8t7NlIKwH4sqyvr7e6r+/LZhgs7B2PS1Q9qFKIsGfYyP2SmZ8J/o2o+tPvcvzxDpQReIAf7/8C3Y6Q551eUKMAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAACCgAwAEAAAAAQAAACAAAAAAj05AyQAAAMtJREFUSA1jlJCQYKAlYKKl4SCzRy0gGMKjQTQaRJAQaG9vV1ZWJhgaWBUQlYp0dXX5+fmx6icoSJQFBE3Bo2DoW8CC6buYmBhubm5kcU5OTnt7e1NTU2RBTPbXr1+XLFmCJo7FAm1tbV5eXmR1169fx0xFmpqa79+/f/HiBVzlhw8f4Gw4g5HsCmfy5Mn37t3r7++Hm4WVMfQjmeY+wBLJWIMSU/DkyZPIMYypACJCfiTjMhFNnOZBNGoBWohjckeDCDNM0ESGfhABAMrMIr5XxwgBAAAAAElFTkSuQmCC"/>
</defs>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="url(#usdt_svg__a)" d="M0 0h20v20H0z"/>
<defs>
<pattern id="usdt_svg__a" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#usdt_svg__b" transform="scale(.03125)"/>
</pattern>
<image id="usdt_svg__b" width="32" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAMbmlDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnluSkJAQIICAlNCbIFIDSAmhBZBeBBshCSSUGBOCir0sKrh2EQEbuiqi2FZA7NiVRbH3xYKKsi7qYkPlTUhA133le+f75t4/Z878p9yZ3HsAoH/gSaV5qDYA+ZICWUJ4MHN0WjqT9BQQAQ3QwVBgwePLpey4uGgAZeD+d3l3AyDK+1VnJdc/5/+r6AqEcj4AyFiIMwVyfj7ExwHAq/hSWQEARKXeanKBVIlnQ6wngwFCvEqJs1V4uxJnqvDhfpukBA7ElwHQoPJ4smwAtO5BPbOQnw15tD5D7CoRiCUA0IdBHMAX8QQQK2Mflp8/UYnLIbaH9lKIYTyAlfkdZ/bf+DMH+Xm87EGsyqtfNELEcmkeb+r/WZr/Lfl5igEftnBQRbKIBGX+sIa3cidGKTEV4i5JZkysstYQfxALVHUHAKWIFBHJKnvUhC/nwPoBA4hdBbyQKIhNIA6T5MVEq/WZWeIwLsRwt6BTxAXcJIgNIV4olIcmqm02yiYmqH2h9VkyDlutP8eT9ftV+nqgyE1mq/nfiIRcNT+mVSRKSoWYArF1oTglBmItiF3kuYlRapuRRSJOzICNTJGgjN8a4gShJDxYxY8VZsnCEtT2JfnygXyxjSIxN0aN9xWIkiJU9cFO8Xn98cNcsMtCCTt5gEcoHx09kItAGBKqyh17LpQkJ6p5PkgLghNUa3GKNC9ObY9bCvPClXpLiD3khYnqtXhKAdycKn48S1oQl6SKEy/K4UXGqeLBl4FowAEhgAkUcGSCiSAHiFu7GrrgL9VMGOABGcgGQuCs1gysSO2fkcBrIigCf0AkBPLBdcH9s0JQCPVfBrWqqzPI6p8t7F+RC55CnA+iQB78rehfJRn0lgKeQI34H955cPBhvHlwKOf/vX5A+03DhppotUYx4JFJH7AkhhJDiBHEMKIDbowH4H54NLwGweGGs3CfgTy+2ROeEtoIjwjXCe2E2xPEc2U/RDkKtEP+MHUtMr+vBW4LOT3xYNwfskNm3AA3Bs64B/TDxgOhZ0+o5ajjVlaF+QP33zL47mmo7ciuZJQ8hBxEtv9xpZajlucgi7LW39dHFWvmYL05gzM/+ud8V30BvEf9aIktxPZjZ7ET2HnsMNYAmNgxrBFrwY4o8eDuetK/uwa8JfTHkwt5xP/wN/BklZWUu9a6drp+Vs0VCKcUKA8eZ6J0qkycLSpgsuHbQcjkSvguw5hurm5uACjfNaq/r7fx/e8QxKDlm27e7wD4H+vr6zv0TRd5DIC93vD4H/yms2cBoKMJwLmDfIWsUKXDlRcC/Jegw5NmBMyAFbCH+bgBL+AHgkAoiASxIAmkgfEwehHc5zIwGUwHc0AxKAXLwGpQATaAzWA72AX2gQZwGJwAZ8BFcBlcB3fh7ukAL0E3eAd6EQQhITSEgRgh5ogN4oS4ISwkAAlFopEEJA3JQLIRCaJApiPzkFJkBVKBbEJqkL3IQeQEch5pQ24jD5FO5A3yCcVQKqqHmqK26HCUhbLRKDQJHYdmo5PQInQ+ugQtR6vRnWg9egK9iF5H29GXaA8GME3MALPAnDEWxsFisXQsC5NhM7ESrAyrxuqwJvicr2LtWBf2ESfiDJyJO8MdHIEn43x8Ej4TX4xX4NvxevwUfhV/iHfjXwk0ggnBieBL4BJGE7IJkwnFhDLCVsIBwml4ljoI74hEogHRjugNz2IaMYc4jbiYuI64m3ic2EZ8TOwhkUhGJCeSPymWxCMVkIpJa0k7ScdIV0gdpA8amhrmGm4aYRrpGhKNuRplGjs0jmpc0Xim0UvWJtuQfcmxZAF5KnkpeQu5iXyJ3EHupehQ7Cj+lCRKDmUOpZxSRzlNuUd5q6mpaanpoxmvKdacrVmuuUfznOZDzY9UXaojlUMdS1VQl1C3UY9Tb1Pf0mg0W1oQLZ1WQFtCq6GdpD2gfdBiaLlocbUEWrO0KrXqta5ovaKT6TZ0Nn08vYheRt9Pv0Tv0iZr22pztHnaM7UrtQ9q39Tu0WHojNCJ1cnXWayzQ+e8znNdkq6tbqiuQHe+7mbdk7qPGRjDisFh8BnzGFsYpxkdekQ9Oz2uXo5eqd4uvVa9bn1dfQ/9FP0p+pX6R/TbDTADWwOuQZ7BUoN9BjcMPg0xHcIeIhyyaEjdkCtD3hsONQwyFBqWGO42vG74yYhpFGqUa7TcqMHovjFu7GgcbzzZeL3xaeOuoXpD/Ybyh5YM3Tf0jglq4miSYDLNZLNJi0mPqZlpuKnUdK3pSdMuMwOzILMcs1VmR806zRnmAeZi81Xmx8xfMPWZbGYes5x5itltYWIRYaGw2GTRatFraWeZbDnXcrflfSuKFcsqy2qVVbNVt7W59Sjr6da11ndsyDYsG5HNGpuzNu9t7WxTbRfYNtg+tzO049oV2dXa3bOn2QfaT7Kvtr/mQHRgOeQ6rHO47Ig6ejqKHCsdLzmhTl5OYqd1Tm3DCMN8hkmGVQ+76Ux1ZjsXOtc6P3QxcIl2mevS4PJquPXw9OHLh58d/tXV0zXPdYvr3RG6IyJHzB3RNOKNm6Mb363S7Zo7zT3MfZZ7o/trDycPocd6j1ueDM9Rngs8mz2/eHl7ybzqvDq9rb0zvKu8b7L0WHGsxaxzPgSfYJ9ZPod9Pvp6+Rb47vP908/ZL9dvh9/zkXYjhSO3jHzsb+nP89/k3x7ADMgI2BjQHmgRyAusDnwUZBUkCNoa9IztwM5h72S/CnYNlgUfCH7P8eXM4BwPwULCQ0pCWkN1Q5NDK0IfhFmGZYfVhnWHe4ZPCz8eQYiIilgecZNryuVza7jdkd6RMyJPRVGjEqMqoh5FO0bLoptGoaMiR60cdS/GJkYS0xALYrmxK2Pvx9nFTYo7FE+Mj4uvjH+aMCJhesLZREbihMQdie+SgpOWJt1Ntk9WJDen0FPGptSkvE8NSV2R2j56+OgZoy+mGaeJ0xrTSekp6VvTe8aEjlk9pmOs59jisTfG2Y2bMu78eOPxeeOPTKBP4E3Yn0HISM3YkfGZF8ur5vVkcjOrMrv5HP4a/ktBkGCVoFPoL1whfJbln7Ui63m2f/bK7E5RoKhM1CXmiCvEr3MicjbkvM+Nzd2W25eXmrc7XyM/I/+gRFeSKzk10WzilIltUidpsbR9ku+k1ZO6ZVGyrXJEPk7eWKAHP+pbFPaKnxQPCwMKKws/TE6ZvH+KzhTJlJapjlMXTX1WFFb0yzR8Gn9a83SL6XOmP5zBnrFpJjIzc2bzLKtZ82d1zA6fvX0OZU7unN/mus5dMfeveanzmuabzp89//FP4T/VFmsVy4pvLvBbsGEhvlC8sHWR+6K1i76WCEoulLqWlpV+XsxffOHnET+X/9y3JGtJ61KvpeuXEZdJlt1YHrh8+wqdFUUrHq8ctbJ+FXNVyaq/Vk9Yfb7Mo2zDGsoaxZr28ujyxrXWa5et/VwhqrheGVy5u8qkalHV+3WCdVfWB62v22C6oXTDp43ijbc2hW+qr7atLttM3Fy4+emWlC1nf2H9UrPVeGvp1i/bJNvatydsP1XjXVOzw2TH0lq0VlHbuXPszsu7QnY11jnXbdptsLt0D9ij2PNib8beG/ui9jXvZ+2v+9Xm16oDjAMl9Uj91PruBlFDe2NaY9vByIPNTX5NBw65HNp22OJw5RH9I0uPUo7OP9p3rOhYz3Hp8a4T2SceN09ovnty9Mlrp+JPtZ6OOn3uTNiZk2fZZ4+d8z93+Lzv+YMXWBcaLnpdrG/xbDnwm+dvB1q9WusveV9qvOxzualtZNvRK4FXTlwNuXrmGvfaxesx19tuJN+4dXPszfZbglvPb+fdfn2n8E7v3dn3CPdK7mvfL3tg8qD6d4ffd7d7tR95GPKw5VHio7uP+Y9fPpE/+dwx/yntadkz82c1z92eH+4M67z8YsyLjpfSl71dxX/o/FH1yv7Vr38G/dnSPbq747Xsdd+bxW+N3m77y+Ov5p64ngfv8t/1vi/5YPRh+0fWx7OfUj896538mfS5/IvDl6avUV/v9eX39Ul5Ml7/pwAGB5qVBcCbbQDQ0gBgwL6NMkbVC/YLoupf+xH4T1jVL/aLFwB18Ps9vgt+3dwEYM8W2H5BfjrsVeNoACT5ANTdfXCoRZ7l7qbiosI+hfCgr+8t7NlIKwH4sqyvr7e6r+/LZhgs7B2PS1Q9qFKIsGfYyP2SmZ8J/o2o+tPvcvzxDpQReIAf7/8C3Y6Q551eUKMAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAACCgAwAEAAAAAQAAACAAAAAAj05AyQAABrxJREFUWAnlV2lMVFcUPrMPDDDjMBAYxAXBEWSJgoqKrYBV1FSNqZoaLVaT2iZdky4/ahNtm9SIfxrTJS2tWFNTtSY2AVHbilutoFYQkbINyg4OAzMw+9Zz7swbHpvVP/5oz+S9d9+555z7nfUBwP+dBBMF4HDNtYKSuqsnlHI5CEHk9aHQWEHi8Yn2OZ6Arfx3L3iERpsNPlhUsGFdYsYFvg6txWMZ9H6jt/Wgy+cJFwpEQVMjxjkoAQ49BCNH0qt/h22ACH8urxt+vX+vCLcy8RpFnLUg86y+Ztm+a2cuR4WGgVQkBrPDDiaHDYR4CEd+09yb/8DgLi1QwIs30tfIw8Dj84LBNgyfLH5+UX7C3KoRzQkiUNZat5+UxUIRWF1OiFFEwLzoePTCg3rcMX4fyXPwBdZ8q7gWMX0HtA0ZQSwQMhCnWm7vx608vihnkfF+uluZ8cWdS9VT5KHMY/3gQ3hzfj7sSs/h6zz2umNoAHaUfw8ykQSxCmDY5YA92QUpz81Iq+eMCLkFPesGevZbUUgUCDcpeSbxkK832drNi5qMpdMGVzr0n/HlgwAq2psTL3U0FMQolIEiAnB5PKCQSPny49Y+BEjXRKQJDWd7tO3FG9m+2tG8/mZXyzROPtgFlx7c+5S8p6KhGiDShITBXUMnlLXcYQaousiQ2+uFvOnJQKkiokhd62yGB2YjhElkQV6f1QwysRgjin6iyRBMRY/FBKX6ux+j0A4SZDVQVlcXc7C2vDtELGGVSxtEEiykAbuFXZQKOpgK04JAz216GzKwODnaVloMPzfdgkRVNBYwTg88lLpAG6YKRAERIFCb24mzRQDvp6+OytXpDCwCrfbevdQmSVOiA54SMgEMYvvZPW5QYyRUMgVESGXMOIFRyfzecwCytQngQTeVUjkWmxNb1wpmpx2MNgtEyEKwK4QMiAIjpB80wF+m+x+i7jvimy0tyo9ul74Si/mh8HJt0Y7tk6aJA506BpwIwh9+AIfHxVpSLAqWD8MQirWiRlDRmHcCT3UsxCjY3C643NHIoqHClHkwihqcMRUdDa/X9NTsEUVtWv5lraFrPg0eyjyBMOHweSszD2aqNHCxvZF5Q/3cajZA00AftAz2wRbdAuYZOx1vP9ZXQpm+lrVaL+be5LRBP6ZPhd7vTMuBNqyPh5YhBCICOaYa34UKqUolbh7oW8UQM9+xovFHeUqPiodQiQQutjWCEcNJ6SBvqAYcGJF6Yw/40EvSpUnXazFjulxsalLLET8yRMEimDtNBycbboET21KBfCKpUAzVvW2rBeX6Gl3xnT8req2m2OgQbBv8ObD9rG4HrJuVARlRU5kitRq1pQPnOhUj5ZhrP4paGOY+BNNAhUsXFSJNQKqDk423oHt4ELsqnEXYYBuCmNCI9jXTU/IZnBN1VTHHGm9UYSHGR6EQzX3KdTd6JUVjM5UamB6hZm0ZikUkRw83zs7EFMiZN3SraPsbah92Ml2j3QqdwwOs2PqxuKld6aIUUxtiZ+g3xs1ZtDUr1+CPB24cqf8tsrSxqapjaDAhNmykICkiTvScwk+htzgdYHHb4ZcNb0CKJpbOZrT7/FE43VwNCQiWUkB5picVIkd91iFIjoypX5WUunjzrCwT8YODqDB5Rf+ZpuvzTrU0XK/v706maibElGXKl1TmF3ViURGQsV1AkZsREcmmHZs6gVwzE3ijoaRVTKl+LT11aZY2y8rxR+AhZ01StnnX3KRMrUJ5m4qKColRME5+SJzy5M+gAhPpwvwnR2orU5W6BfzDaXMUAGIsiV9iS1XOWTg3MvY65ZGIb46/ZpuPuNGIpi9iijr2yncFhdl7c3NpoIyicQBolwSLV+9YnKzWXm5HA0EI2HfBUfWIYBBIurDFYWncrN9L1u58Bl8npAkBcJJH1r78bA4aaMbBw2ZFwH06W4IDhU/krb8t/Z/wrmET5MQlln+e/+IKvtzY9SMBkDAZyNEmllM66Bvgx+Dzf+F41hgfQXh8HjDiBJytjj791cpta3giEy4DPk24N4q5vbT4NH5u1yuxC/AcmBquZn8r0GynViOAA9j/Qzh40qLiTny9cvuWUQYmeXlsAKT/6vmjx3HYbKa/E/xfSlcwNeES+graYWls4g9FeZsKJzlvHPuJAJD2uxeOH/mjS/9SJIKgkQsC/6CiIZMbr/vmwPIXdo875RGMf62BsboH87YULpua+C2NVPoI0ZRsM/fD1jkLDz3p4WNtP9F7UeXZQ+kl+3xph/f6DlSV0z8dT5/eu3DyWFHluZKnf/J/6cR/AKRO76KDiL72AAAAAElFTkSuQmCC"/>
</defs>
</svg>
......@@ -2,6 +2,8 @@ import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch';
import appConfig from 'configs/app/config';
import { httpLogger } from 'lib/api/logger';
import * as cookies from 'lib/cookies';
// first arg can be only a string
......@@ -13,9 +15,15 @@ export default function fetchFactory(_req: NextApiRequest) {
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
};
const url = `https://blockscout.com${ path }`;
const url = new URL(path, appConfig.api.endpoint);
return nodeFetch(url, {
httpLogger.logger.info({
message: 'Trying to call API',
url,
req: _req,
});
return nodeFetch(url.toString(), {
headers,
...init,
});
......
import * as Sentry from '@sentry/nextjs';
// import * as Sentry from '@sentry/nextjs';
import type { NextApiRequest } from 'next';
import * as cookies from 'lib/cookies';
import appConfig from 'configs/app/config';
export default function getUrlWithNetwork(_req: NextApiRequest, path: string) {
const networkType = _req.cookies[cookies.NAMES.NETWORK_TYPE];
const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE];
if (!networkType) {
Sentry.captureException(new Error('Incorrect network'), { extra: { networkType, networkSubType } });
}
return `/${ networkType }${ networkSubType ? '/' + networkSubType : '' }/${ path }`;
return [
appConfig.api.basePath,
path,
]
.filter((segment) => segment !== '' && segment !== '/')
.join('');
}
import { withSentry } from '@sentry/nextjs';
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>) {
const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
httpLogger(_req, res);
if (!_req.method || !allowedMethods.includes(_req.method as Methods)) {
res.setHeader('Allow', allowedMethods);
res.status(405).end(`Method ${ _req.method } Not Allowed`);
......@@ -16,7 +18,7 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = getUrlWithNetwork(_req, `api${ getUrl(_req) }`);
const url = getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const fetch = fetchFactory(_req);
const response = await fetch(url, {
method: _req.method,
......@@ -39,8 +41,10 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
responseError = defaultError;
}
httpLogger.logger.error({ err: responseError, url: _req.url });
res.status(500).json(responseError);
};
return withSentry(handler);
return handler;
}
import pino from 'pino-http';
export const httpLogger = pino();
import BigNumber from 'bignumber.js';
export default function compareBns(value1: string | number, value2: string | number) {
const value1Bn = new BigNumber(value1);
const value2Bn = new BigNumber(value2);
if (value1Bn.isGreaterThan(value2Bn)) {
return 1;
}
if (value1Bn.isLessThan(value2Bn)) {
return -1;
}
return 0;
}
import BigNumber from 'bignumber.js';
import type { Block } from 'types/api/block';
export default function getBlockReward(block: Block) {
const txFees = BigNumber(block.tx_fees || 0);
const burntFees = BigNumber(block.burnt_fees || 0);
const minerReward = block.rewards?.find(({ type }) => type === 'Miner Reward' || type === 'Validator Reward')?.reward;
const totalReward = BigNumber(minerReward || 0);
const staticReward = totalReward.minus(txFees).plus(burntFees);
return {
totalReward,
staticReward,
txFees,
burntFees,
};
}
import BigNumber from 'bignumber.js';
export const WEI = new BigNumber(10 ** 18);
export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
export const ZERO = new BigNumber(0);
......@@ -5,14 +5,12 @@ import isBrowser from './isBrowser';
export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed',
NETWORK_TYPE='network_type',
NETWORK_SUB_TYPE='network_sub_type',
API_TOKEN='_explorer_key',
}
export function get(name?: string | undefined | null) {
if (!isBrowser()) {
return () => {};
return undefined;
}
return Cookies.get(name);
}
......
import availableNetworks from 'lib/networks/availableNetworks';
import appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks';
import getMarketplaceApps from '../getMarketplaceApps';
const KEY_WORDS = {
BLOB: 'blob:',
......@@ -11,20 +14,26 @@ const KEY_WORDS = {
UNSAFE_EVAL: '\'unsafe-eval\'',
};
const MAIN_DOMAINS = [ '*.blockscout.com', 'blockscout.com' ];
const isDev = process.env.NODE_ENV === 'development';
const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
// eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssets() {
const icons = availableNetworks
const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string')
.map(({ icon }) => new URL(icon as string));
const logos = availableNetworks
.filter(({ logo }) => typeof logo === 'string')
.map(({ logo }) => new URL(logo as string));
const logo = appConfig.network.logo ? new URL(appConfig.network.logo) : undefined;
return icons.concat(logos);
return logo ? icons.concat(logo) : icons;
}
function getMarketplaceAppsOrigins() {
return getMarketplaceApps().map(({ url }) => url);
}
function getMarketplaceAppsLogosOrigins() {
return getMarketplaceApps().map(({ logo }) => logo);
}
function makePolicyMap() {
......@@ -40,7 +49,7 @@ function makePolicyMap() {
...MAIN_DOMAINS,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason
isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// client error monitoring
'sentry.io', '*.sentry.io',
......@@ -51,7 +60,7 @@ function makePolicyMap() {
// next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
isDev ? KEY_WORDS.UNSAFE_EVAL : '',
appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS,
......@@ -85,6 +94,9 @@ function makePolicyMap() {
// network assets
...networkExternalAssets.map((url) => url.host),
// marketplace apps logos
...getMarketplaceAppsLogosOrigins(),
],
'font-src': [
......@@ -103,9 +115,13 @@ function makePolicyMap() {
KEY_WORDS.NONE,
],
'report-uri': [
process.env.SENTRY_CSP_REPORT_URI,
],
'frame-src': getMarketplaceAppsOrigins(),
...(REPORT_URI ? {
'report-uri': [
REPORT_URI,
],
} : {}),
};
}
......
// eslint-disable-next-line no-restricted-imports
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
dayjs.locale('en');
const relativeTimeConfig = {
thresholds: [
{ l: 's', r: 1 },
{ l: 'ss', r: 59, d: 'second' },
{ l: 'm', r: 1 },
{ l: 'mm', r: 59, d: 'minute' },
{ l: 'h', r: 1 },
{ l: 'hh', r: 23, d: 'hour' },
{ l: 'd', r: 1 },
{ l: 'dd', r: 29, d: 'day' },
{ l: 'M', r: 1 },
{ l: 'MM', r: 11, d: 'month' },
{ l: 'y' },
{ l: 'yy', d: 'year' },
],
};
dayjs.extend(relativeTime);
dayjs.extend(relativeTime, relativeTimeConfig);
dayjs.extend(updateLocale);
dayjs.extend(localizedFormat);
dayjs.extend(duration);
dayjs.updateLocale('en', {
formats: {
LLLL: 'MMMM-DD-YYYY HH:mm:ss A Z UTC',
},
relativeTime: {
s: 'a sec',
ss: '%d secs',
future: 'in %s',
past: '%s ago',
m: '1 min',
mm: '%d mins',
h: '1 h',
hh: '%d h',
d: '1 d',
dd: '%d d',
M: '1 mo',
MM: '%d mo',
y: '1 y',
yy: '%d y',
},
});
dayjs.locale('en');
export default dayjs;
import data from 'data/marketplaceApps.json';
export default function getMarketplaceApps() {
return data;
}
import BigNumber from 'bignumber.js';
import { WEI, GWEI } from 'lib/consts';
export default function getValueWithUnit(value: string | number, unit: 'wei' | 'gwei' | 'ether' = 'wei') {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
unitBn = WEI;
break;
case 'gwei':
unitBn = GWEI;
break;
default:
unitBn = new BigNumber(1);
}
const valueBn = new BigNumber(value);
const valueCurr = valueBn.dividedBy(unitBn);
return valueCurr;
}
export default function hexToAddress(hex: string) {
const shortenHex = hex.slice(0, 66);
return shortenHex.slice(0, 2) + shortenHex.slice(26);
}
export default function hexToBytes(hex: string) {
const bytes = [];
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substring(c, c + 2), 16));
}
return bytes;
}
import hexToBytes from 'lib/hexToBytes';
export default function hexToUtf8(hex: string) {
const utf8decoder = new TextDecoder();
const bytes = new Uint8Array(hexToBytes(hex));
return utf8decoder.decode(bytes);
}
import * as Sentry from '@sentry/react';
import React from 'react';
import { config, configureScope } from 'configs/sentry/react';
export default function useConfigSentry() {
React.useEffect(() => {
// gotta init sentry in browser
Sentry.init(config);
Sentry.configureScope(configureScope);
}, []);
}
import * as Sentry from '@sentry/nextjs';
import * as Sentry from '@sentry/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -30,6 +30,12 @@ export default function useFetch() {
return fetch(path, reqParams).then(response => {
if (!response.ok) {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
......@@ -37,12 +43,6 @@ export default function useFetch() {
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
......
import React from 'react';
import React, { useMemo } from 'react';
import appConfig from 'configs/app/config';
import marketplaceApps from 'data/marketplaceApps.json';
import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg';
import blocksIcon from 'icons/block.svg';
import gearIcon from 'icons/gear.svg';
// import gearIcon from 'icons/gear.svg';
import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg';
import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
export default function useNavItems() {
const link = useLink();
const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainIds.includes(appConfig.network.id)),
[ ])
.length > 0;
const currentRoute = useCurrentRoute()();
return React.useMemo(() => {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute === 'blocks' },
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' },
{ text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
];
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' } : null,
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
].filter(notEmpty);
const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
{ text: 'Private tags', url: link('private_tags_address'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' },
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' },
......@@ -38,5 +49,5 @@ export default function useNavItems() {
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' };
return { mainNavItems, accountNavItems, profileItem };
}, [ link, currentRoute ]);
}, [ isMarketplaceFilled, currentRoute ]);
}
import { useRouter } from 'next/router';
import findNetwork from 'lib/networks/findNetwork';
export default function useNetwork() {
const router = useRouter();
const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_sub_type === 'string' ? router.query.network_sub_type : undefined,
});
return selectedNetwork;
}
const BASE_PATH = '/[network_type]/[network_sub_type]';
module.exports = BASE_PATH;
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags_address', 'private_tags_tx', 'public_tags', 'api_keys', 'custom_abi' ];
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags', 'public_tags', 'api_keys', 'custom_abi' ];
import type { RouteName } from 'lib/link/routes';
export default function isAccountRoute(route: RouteName) {
......
import isBrowser from 'lib/isBrowser';
import findNetwork from 'lib/networks/findNetwork';
import appConfig from 'configs/app/config';
import { ROUTES } from './routes';
import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g;
export function link(routeName: RouteName, urlParams?: Record<string, string | undefined>, queryParams?: Record<string, string>): string {
export default function link(
routeName: RouteName,
urlParams?: Record<string, Array<string> | string | undefined>,
queryParams?: Record<string, string>,
): string {
const route = ROUTES[routeName];
if (!route) {
return '';
}
const network = findNetwork({
network_type: urlParams?.network_type || '',
network_sub_type: urlParams?.network_sub_type,
});
const refinedUrlParams: typeof urlParams = {
network_type: appConfig.network.type,
network_sub_type: appConfig.network.subtype,
...urlParams,
};
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !network?.subType) {
if (paramName === 'network_sub_type' && !refinedUrlParams.network_sub_type) {
return '';
}
const paramValue = urlParams?.[paramName];
let paramValue = refinedUrlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dunno know how to manage it, fix me if you find an issue
paramValue = paramValue.join(',');
}
return paramValue ? `/${ paramValue }` : '';
});
const url = new URL(path, isBrowser() ? window.location.origin : 'https://blockscout.com');
const url = new URL(path, appConfig.baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
......
const BASE_PATH = require('./basePath');
const paths = {
network_index: `${ BASE_PATH }`,
watchlist: `${ BASE_PATH }/account/watchlist`,
private_tags: `${ BASE_PATH }/account/tag_address`,
public_tags: `${ BASE_PATH }/account/public_tags_request`,
api_keys: `${ BASE_PATH }/account/api_key`,
custom_abi: `${ BASE_PATH }/account/custom_abi`,
profile: `${ BASE_PATH }/auth/profile`,
txs: `${ BASE_PATH }/txs`,
tx: `${ BASE_PATH }/tx/[id]`,
blocks: `${ BASE_PATH }/blocks`,
block: `${ BASE_PATH }/block/[id]`,
tokens: `${ BASE_PATH }/tokens`,
token_index: `${ BASE_PATH }/token/[hash]`,
token_instance_item: `${ BASE_PATH }/token/[hash]/instance/[id]`,
address_index: `${ BASE_PATH }/address/[id]`,
address_contract_verification: `${ BASE_PATH }/address/[id]/contract_verifications/new`,
apps: `${ BASE_PATH }/apps`,
app_index: `${ BASE_PATH }/apps/[id]`,
search_results: `${ BASE_PATH }/search-results`,
other: `${ BASE_PATH }/search-results`,
// no slash required, it is correct
auth: `${ BASE_PATH }auth/auth0`,
};
module.exports = paths;
import appConfig from 'configs/app/config';
import PATHS from './paths.js';
export interface Route {
pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
......@@ -5,107 +9,95 @@ export interface Route {
export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/[network_type]/[network_sub_type]';
export const ROUTES = {
// NETWORK MAIN PAGE
network_index: {
pattern: `${ BASE_PATH }`,
pattern: PATHS.network_index,
crossNetworkNavigation: true,
},
// ACCOUNT
watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
},
private_tags_address: {
pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
pattern: PATHS.watchlist,
},
private_tags_tx: {
pattern: `${ BASE_PATH }/account/tag_transaction`,
crossNetworkNavigation: true,
private_tags: {
pattern: PATHS.private_tags,
},
public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`,
crossNetworkNavigation: true,
pattern: PATHS.public_tags,
},
api_keys: {
pattern: `${ BASE_PATH }/account/api_key`,
crossNetworkNavigation: true,
pattern: PATHS.api_keys,
},
custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`,
crossNetworkNavigation: true,
pattern: PATHS.custom_abi,
},
profile: {
pattern: `${ BASE_PATH }/auth/profile`,
crossNetworkNavigation: true,
pattern: PATHS.profile,
},
// TRANSACTIONS
txs: {
pattern: `${ BASE_PATH }/txs`,
pattern: PATHS.txs,
crossNetworkNavigation: true,
},
tx_index: {
pattern: `${ BASE_PATH }/tx/[id]`,
},
tx_internal: {
pattern: `${ BASE_PATH }/tx/[id]/internal-transactions`,
},
tx_logs: {
pattern: `${ BASE_PATH }/tx/[id]/logs`,
},
tx_raw_trace: {
pattern: `${ BASE_PATH }/tx/[id]/raw-trace`,
},
tx_state: {
pattern: `${ BASE_PATH }/tx/[id]/state`,
tx: {
pattern: PATHS.tx,
},
// BLOCKS
blocks: {
pattern: `${ BASE_PATH }/blocks`,
pattern: PATHS.blocks,
crossNetworkNavigation: true,
},
block: {
pattern: PATHS.block,
},
// TOKENS
tokens: {
pattern: `${ BASE_PATH }/tokens`,
pattern: PATHS.tokens,
crossNetworkNavigation: true,
},
token_index: {
pattern: `${ BASE_PATH }/token/[id]`,
pattern: PATHS.token_index,
crossNetworkNavigation: true,
},
token_instance_item: {
pattern: PATHS.token_instance_item,
},
// ADDRESSES
address_index: {
pattern: `${ BASE_PATH }/address/[id]`,
pattern: PATHS.address_index,
crossNetworkNavigation: true,
},
address_contract_verification: {
pattern: PATHS.address_contract_verification,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: `${ BASE_PATH }/apps`,
pattern: PATHS.apps,
},
app_index: {
pattern: PATHS.app_index,
},
// SEARCH
search_results: {
pattern: `${ BASE_PATH }/apps`,
pattern: PATHS.search_results,
},
// ??? what URL will be here
other: {
pattern: `${ BASE_PATH }/search-results`,
pattern: PATHS.other,
},
// AUTH
auth: {
// no slash required, it is correct
pattern: `${ BASE_PATH }auth/auth0`,
pattern: PATHS.auth,
},
};
......@@ -121,6 +113,6 @@ function checkRoutes(route: Record<string, Route>) {
return route;
}
if (process.env.NODE_ENV === 'development') {
if (appConfig.isDev) {
checkRoutes(ROUTES);
}
import { useRouter } from 'next/router';
import React from 'react';
import { link } from 'lib/link/link';
type LinkBuilderParams = Parameters<typeof link>;
export default function useLink() {
const router = useRouter();
const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type;
return React.useCallback((...args: LinkBuilderParams) => {
if (typeof networkType !== 'string' || typeof networkSubType !== 'string') {
return '';
}
return link(args[0], { network_type: networkType, network_sub_type: networkSubType, ...args[1] }, args[2]);
}, [ networkType, networkSubType ]);
}
import type { Network } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
import parseNetworkConfig from './parseNetworkConfig';
// will change later when we agree how to host network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': gnosisIcon,
'xdai/optimism': optimismIcon,
'xdai/aox': arbitrumIcon,
'eth/mainnet': ethereumIcon,
'etc/mainnet': ethereumClassicIcon,
'poa/core': poaIcon,
'rsk/mainnet': rskIcon,
'xdai/testnet': arbitrumIcon,
'poa/sokol': poaSokolIcon,
'artis/sigma1': artisIcon,
};
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'eth/mainnet': require('icons/networks/logos/eth.svg'),
'etc/mainnet': require('icons/networks/logos/etc.svg'),
'poa/core': require('icons/networks/logos/poa.svg'),
'rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'poa/sokol': require('icons/networks/logos/sokol.svg'),
'artis/sigma1': require('icons/networks/logos/artis.svg'),
'lukso/l14': require('icons/networks/logos/lukso.svg'),
astar: require('icons/networks/logos/astar.svg'),
shiden: require('icons/networks/logos/shiden.svg'),
shibuya: require('icons/networks/logos/shibuya.svg'),
};
const NETWORKS: Array<Network> = (() => {
const networksFromConfig: Array<Network> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
logo: network.logo || LOGOS[network.type + (network.subType ? `/${ network.subType }` : '')],
icon: network.icon || ICONS[network.type + (network.subType ? `/${ network.subType }` : '')],
}));
})();
export default NETWORKS;
// for easy .env.example update
// const FOR_CONFIG = JSON.stringify([
// {
// 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',
// logo: 'https://www.fillmurray.com/240/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',
// chainId: 99,
// },
// {
// name: 'RSK',
// shortName: 'RBTC',
// type: 'rsk',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 30,
// },
// {
// name: 'Gnosis Chain Testnet',
// type: 'xdai',
// subType: 'testnet',
// group: 'testnets',
// isAccountSupported: true,
// },
// {
// 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,
// },
// ]);
import type { FeaturedNetwork } from 'types/networks';
import appConfig from 'configs/app/config';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': gnosisIcon,
'/xdai/optimism': optimismIcon,
'/xdai/aox': arbitrumIcon,
'/eth/mainnet': ethereumIcon,
'/etc/mainnet': ethereumClassicIcon,
'/poa/core': poaIcon,
'/rsk/mainnet': rskIcon,
'/xdai/testnet': arbitrumIcon,
'/poa/sokol': poaSokolIcon,
'/artis/sigma1': artisIcon,
};
// for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([
// {
// title: 'Gnosis Chain',
// basePath: '/xdai/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Optimism on Gnosis Chain',
// basePath: '/xdai/optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// },
// {
// title: 'Arbitrum on xDai',
// basePath: '/xdai/aox',
// group: 'mainnets',
// },
// {
// title: 'Ethereum',
// basePath: '/eth/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Ethereum Classic',
// basePath: '/etx/mainnet',
// group: 'mainnets',
// },
// {
// title: 'POA',
// basePath: '/poa/core',
// group: 'mainnets',
// },
// {
// title: 'RSK',
// basePath: '/rsk/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Gnosis Chain Testnet',
// basePath: '/xdai/testnet',
// group: 'testnets',
// },
// {
// title: 'POA Sokol',
// basePath: '/poa/sokol',
// group: 'testnets',
// },
// {
// title: 'ARTIS Σ1',
// basePath: '/artis/sigma1',
// group: 'other',
// },
// {
// title: 'LUKSO L14',
// basePath: '/lukso/l14',
// group: 'other',
// },
// {
// title: 'Astar',
// basePath: '/astar',
// group: 'other',
// },
// ]).replaceAll('"', '\'');
function parseNetworkConfig() {
try {
return JSON.parse(appConfig.featuredNetworks || '[]');
} catch (error) {
return [];
}
}
const featuredNetworks: Array<FeaturedNetwork> = (() => {
const networksFromConfig: Array<FeaturedNetwork> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
icon: network.icon || ICONS[network.basePath],
}));
})();
export default featuredNetworks;
import availableNetworks from 'lib/networks/availableNetworks';
interface Params {
network_type: string;
network_sub_type?: string;
}
export default function findNetwork(params: Params) {
return availableNetworks.find((network) =>
network.type === params.network_type &&
network.subType ? network.subType === params.network_sub_type : network.type === params.network_type,
);
}
import NETWORKS from './availableNetworks';
export default function getAvailablePaths() {
return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType || 'mainnet' } }));
}
import findNetwork from './findNetwork';
import appConfig from 'configs/app/config';
export default function getNetworkTitle({ network_type: type, network_sub_type: subType }: {network_type?: string; network_sub_type?: string}) {
const currentNetwork = findNetwork({ network_type: type || '', network_sub_type: subType });
if (currentNetwork) {
return currentNetwork.name + (currentNetwork.shortName ? ` (${ currentNetwork.shortName })` : '') + ' Explorer';
}
return '';
export default function getNetworkTitle() {
return appConfig.network.name + (appConfig.network.shortName ? ` (${ appConfig.network.shortName })` : '') + ' Explorer';
}
const supportedNetworks = process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS?.replaceAll('\'', '"');
// should be CommonJS module since it used for next.config.js
function parseNetworkConfig() {
try {
return JSON.parse(supportedNetworks || '[]');
} catch (error) {
return [];
}
}
module.exports = parseNetworkConfig;
import { useRouter } from 'next/router';
import React from 'react';
import useNetwork from 'lib/hooks/useNetwork';
import isAccountRoute from 'lib/link/isAccountRoute';
import { link } from 'lib/link/link';
import appConfig from 'configs/app/config';
import link from 'lib/link/link';
import { ROUTES } from 'lib/link/routes';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import NETWORKS from 'lib/networks/availableNetworks';
import featuredNetworks from 'lib/networks/featuredNetworks';
export default function useNetworkNavigationItems() {
const selectedNetwork = useNetwork();
const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName];
const router = useRouter();
const isAccount = isAccountRoute(currentRouteName);
return React.useMemo(() => {
return NETWORKS.map((network) => {
const routeName = (() => {
if ('crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation) {
if ((isAccount && network.isAccountSupported) || !isAccount) {
return currentRouteName;
}
}
return 'network_index';
})();
const url = link(routeName, { ...router.query, network_type: network.type, network_sub_type: network.subType });
return featuredNetworks.map((network) => {
const routeName = 'crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation ? currentRouteName : 'network_index';
const [ , networkType, networkSubtype ] = network.basePath.split('/');
const url = link(routeName, { ...router.query, network_type: networkType, network_sub_type: networkSubtype });
return {
...network,
url: url,
isActive: selectedNetwork?.type === network.type && selectedNetwork?.subType === network?.subType,
isActive: appConfig.network.basePath === network.basePath,
};
});
}, [ currentRoute, currentRouteName, isAccount, router.query, selectedNetwork?.subType, selectedNetwork?.type ]);
}, [ currentRoute, currentRouteName, router.query ]);
}
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from './types';
import Block from 'ui/pages/Block';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
}
const BlockNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block/>
</>
);
};
export default BlockNextPage;
import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle();
return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '',
description: params ? `View the transactions, token transfers, and uncles for block number ${ params.id }` : '',
};
}
export type PageParams = {
network_type: string;
network_sub_type: string;
id: string;
}
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Blocks from 'ui/pages/Blocks';
import getSeo from './getSeo';
const BlocksNextPage: NextPage = () => {
const { title } = getSeo();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
</>
);
};
export default BlocksNextPage;
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo() {
return {
title: getNetworkTitle(),
};
}
export type PageParams = {
network_type: string;
network_sub_type: string;
}
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
return { paths: [], fallback: 'blocking' };
};
......@@ -4,22 +4,23 @@ import React from 'react';
import type { PageParams } from './types';
import type { Props as TransactionProps } from 'ui/pages/Transaction';
import Transaction from 'ui/pages/Transaction';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
tab: TransactionProps['tab'];
}
const TransactionNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
const { title } = getSeo(pageParams);
const TransactionNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
<Head><title>{ title }</title></Head>
<Transaction tab={ tab }/>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
</>
);
};
......
......@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
const networkTitle = getNetworkTitle();
return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '',
......
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
export default function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
export default function getConfirmationString(durations: Array<number>) {
if (durations.length === 0) {
return '';
}
const [ lower, upper ] = durations.map((time) => time / 1_000);
if (!upper) {
return `Confirmed within ${ lower } secs`;
}
if (lower === 0) {
return `Confirmed within <= ${ upper } secs`;
}
return `Confirmed within ${ lower } - ${ upper } secs`;
}
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy';
import { link } from 'lib/link/link';
import findNetwork from 'lib/networks/findNetwork';
import link from 'lib/link/link';
const cspPolicy = getCspPolicy();
......@@ -20,9 +20,8 @@ export function middleware(req: NextRequest) {
network_type: networkType,
network_sub_type: networkSubtype,
};
const selectedNetwork = findNetwork(networkParams);
if (!selectedNetwork) {
if (appConfig.network.type !== networkType && appConfig.network.subtype !== networkSubtype) {
const url = req.nextUrl.clone();
url.pathname = `/404`;
return NextResponse.rewrite(url);
......
const { withSentryConfig } = require('@sentry/nextjs');
const withReactSvg = require('next-react-svg');
const path = require('path');
......@@ -27,24 +26,6 @@ const moduleExports = {
redirects,
headers,
output: 'standalone',
sentry: {
hideSourceMaps: true,
},
};
const sentryWebpackPluginOptions = {
// Additional config options for the Sentry Webpack plugin. Keep in mind that
// the following options are set automatically, and overriding them is not
// recommended:
// release, url, org, project, authToken, configFile, stripPrefix,
// urlPrefix, include, ignore
silent: true, // Suppresses all logs
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options.
deploy: {
env: process.env.VERCEL_ENV || process.env.NODE_ENV,
},
};
module.exports = withReactSvg(withSentryConfig(moduleExports, sentryWebpackPluginOptions));
module.exports = withReactSvg(moduleExports);
......@@ -9,28 +9,41 @@
},
"scripts": {
"dev": "next dev",
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"build": "next build",
"build:vercel": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.secrets blockscout",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons"
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test-ct": "playwright test -c playwright-ct.config.ts",
"test-docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.27.0-focal ./run-tests.sh"
},
"dependencies": {
"@chakra-ui/icons": "^2.0.2",
"@chakra-ui/react": "2.3.1",
"@chakra-ui/theme-tools": "^2.0.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@sentry/nextjs": "^7.12.1",
"@sentry/react": "^7.13.0",
"@sentry/tracing": "^7.13.0",
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"@types/react-scroll": "^1.8.4",
"bignumber.js": "^9.1.0",
"dayjs": "^1.11.5",
"ethers": "^5.7.1",
"framer-motion": "^6",
"lodash": "^4.0.0",
"next": "12.2.5",
"next-react-svg": "1.1.3",
"node-fetch": "^3.2.9",
"pino-http": "^8.2.1",
"pino-pretty": "^9.1.1",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-hook-form": "^7.33.1",
......@@ -41,10 +54,12 @@
"use-font-face-observer": "^1.2.1"
},
"devDependencies": {
"@playwright/experimental-ct-react": "^1.26.1",
"@types/node": "17.0.36",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"dotenv-cli": "^6.0.0",
"eslint": "8.16.0",
"eslint-config-next": "^12.3.0",
"eslint-plugin-es5": "^1.5.0",
......@@ -52,6 +67,7 @@
"eslint-plugin-regexp": "^1.7.0",
"husky": "^8.0.0",
"lint-staged": ">=10",
"playwright": "^1.26.1",
"svgo": "^2.8.0",
"typescript": "4.7.2"
},
......
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const ApiKeysPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
export default ApiKeysPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const CustomAbiPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
export default CustomAbiPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const PublicTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default PublicTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,17 +14,17 @@ type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<PrivateTags tab="address"/>
<PrivateTags/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const WatchListPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head>
......@@ -28,5 +28,5 @@ const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
export default WatchListPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -2,13 +2,13 @@ import Head from 'next/head';
import React from 'react';
import Apps from 'ui/pages/Apps';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage = () => {
return (
<Page>
<PageHeader text="Apps"/>
<PageTitle text="Apps"/>
<Head><title>Apps</title></Head>
<Apps/>
......@@ -17,3 +17,6 @@ const AppsPage = () => {
};
export default AppsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import MarketplaceApp from 'ui/pages/MarketplaceApp';
import Page from 'ui/shared/Page/Page';
const AppPage: NextPage = () => {
const router = useRouter();
const [ isLoading, setIsLoading ] = useState(true);
const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined);
const { id }: { id?: string } = router.query;
useEffect(() => {
if (!id) {
return;
}
const app = marketplaceApps.find((app) => app.id === id);
setApp(app);
setIsLoading(false);
}, [ id ]);
if (app || isLoading) {
return (
<>
<Head><title>{ app ? `Blockscout | ${ app.title }` : 'Loading app..' }</title></Head>
<MarketplaceApp app={ app } isLoading={ isLoading }/>
</>
);
}
return (
<Page>
<Head><title>Blockscout | No app found</title></Head>
<EmptySearchResult text={ `Couldn${ apos }t find an app.` }/>
</Page>
);
};
export default AppPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -3,17 +3,19 @@ import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
import BlockNextPage from 'lib/next/block/BlockNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="logs"/>;
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<BlockNextPage pageParams={ pageParams }/>
);
};
export default TransactionPage;
export default BlockPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -3,17 +3,19 @@ import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="state"/>;
const BlockPage: NextPage<Props> = () => {
return (
<BlocksNextPage/>
);
};
export default TransactionPage;
export default BlockPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import type { NextPage } from 'next';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import Head from 'next/head';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const Home: NextPage = () => {
const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
import Home from 'ui/pages/Home';
const HomePage: NextPage = () => {
return (
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageHeader text={
`Home Page for ${ selectedNetwork?.name } network`
}/>
{ /* will be deleted when we move to new CI */ }
{ isFormVisible && (
<>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Page>
<>
<Head><title>Home Page</title></Head>
<Home/>
</>
);
};
export default Home;
export default HomePage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -11,11 +11,11 @@ type Props = {
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<TransactionNextPage tab="details" pageParams={ pageParams }/>
<TransactionNextPage pageParams={ pageParams }/>
);
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="internal_txn"/>;
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage';
type Props = {
pageParams: PageParams;
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="raw_trace"/>;
};
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -3,7 +3,7 @@ import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PrivateTags from 'ui/pages/PrivateTags';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
......@@ -14,17 +14,17 @@ type Props = {
pageParams: PageParams;
}
const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<PrivateTags tab="transaction"/>
<Transactions/>
</>
);
};
export default TransactionTagsPage;
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -4,10 +4,13 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
queries: {
......
......@@ -21,6 +21,8 @@ import type { NextPageContext } from 'next';
import NextErrorComponent from 'next/error';
import React from 'react';
import sentryConfig from 'configs/sentry/nextjs';
type ContextOrProps = {
req?: NextPageContext['req'];
res?: NextPageContext['res'];
......@@ -34,6 +36,8 @@ const CustomErrorComponent = (props: { statusCode: number }) => {
};
CustomErrorComponent.getInitialProps = async(contextData: ContextOrProps) => {
Sentry.init(sentryConfig);
// In case this is running in a serverless function, await this in order to give Sentry
// time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(contextData);
......
......@@ -2,9 +2,12 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
const url = getUrlWithNetwork(_req, `api/account/v1/get_csrf`);
httpLogger(_req, res);
const url = getUrlWithNetwork(_req, `/api/account/v1/get_csrf`);
const fetch = fetchFactory(_req);
const response = await fetch(url);
......@@ -14,5 +17,8 @@ export default async function csrfHandler(_req: NextApiRequest, res: NextApiResp
return;
}
res.status(500).json({ statusText: response.statusText, status: response.status });
const responseError = { statusText: response.statusText, status: response.status };
httpLogger.logger.error({ err: responseError, url: _req.url });
res.status(500).json(responseError);
}
......@@ -6,15 +6,19 @@ import type { TWatchlistItem } from 'types/client/account';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => {
httpLogger(_req, res);
const fetch = fetchFactory(_req);
const url = getUrlWithNetwork(_req, 'api/account/v1/user/watchlist');
const url = getUrlWithNetwork(_req, '/api/account/v1/user/watchlist');
const watchlistResponse = await fetch(url, { method: 'GET' });
const watchlistData = await watchlistResponse.json() as WatchlistAddresses;
if (watchlistResponse.status !== 200) {
httpLogger.logger.error({ err: { statusText: 'Watchlist token error', status: 500 }, url: _req.url });
res.status(500).end(watchlistData || 'Unknown error');
return;
}
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/blocks/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/blocks${ req.query.type ? `?type=${ req.query.type }` : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/internal-transactions`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/logs`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/raw-trace`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
const searchParamsStr = new URLSearchParams(searchParams).toString();
return `/v2/transactions/${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './',
testMatch: /.*\.pw\.tsx/,
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: Boolean(process.env.CI),
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
headless: true,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
};
export default config;
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from '../theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/playwright/index.ts"></script>
</body>
</html>
// Import styles, initialize component theme here.
// import '../src/common.css';
export {};
#!/bin/sh
yarn install
yarn test-ct
\ No newline at end of file
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.NODE_ENV;
Sentry.init({
environment: ENV,
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
defaults.url=https://sentry.io/
defaults.org=block-scout
defaults.project=new-ui
cli.executable=../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.VERCEL_ENV || process.env.NODE_ENV;
Sentry.init({
environment: ENV,
dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0,
// ...
// Note: if you want to override the automatic release value, do not set a
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps
});
......@@ -47,7 +47,7 @@ const variantOutline = defineStyle((props) => {
const isGrayTheme = c === 'gray' || c === 'gray-dark';
const color = isGrayTheme ? mode('blackAlpha.800', 'whiteAlpha.800')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const borderColor = isGrayTheme ? mode('gray.200', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const borderColor = isGrayTheme ? mode('gray.300', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const activeBg = isGrayTheme ? mode('blue.50', 'gray.600')(props) : mode(`${ c }.50`, 'gray.600')(props);
const activeColor = (() => {
if (c === 'gray') {
......@@ -106,15 +106,65 @@ const variantSimple = defineStyle((props) => {
};
});
const variantGhost = defineStyle((props) => {
const { colorScheme: c } = props;
const activeBg = mode(`${ c }.50`, 'gray.800')(props);
return {
bg: 'transparent',
color: mode(`${ c }.700`, 'gray.400')(props),
_active: {
color: mode(`${ c }.700`, 'gray.50')(props),
bg: mode(`${ c }.50`, 'gray.800')(props),
},
_hover: {
color: `${ c }.400`,
_active: {
bg: props.isActive ? activeBg : 'transparent',
color: mode(`${ c }.700`, 'gray.50')(props),
},
},
};
});
const variantSubtle = defineStyle((props) => {
const { colorScheme: c } = props;
if (c === 'gray') {
return {
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
_hover: {
color: 'blue.400',
_disabled: {
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
},
},
};
}
return {
bg: `${ c }.100`,
color: `${ c }.600`,
_hover: {
color: 'blue.400',
},
};
});
const variants = {
solid: variantSolid,
outline: variantOutline,
simple: variantSimple,
ghost: variantGhost,
subtle: variantSubtle,
};
const baseStyle = defineStyle({
fontWeight: 600,
borderRadius: 'base',
overflow: 'hidden',
});
const sizes = {
......
import { formAnatomy as parts } from '@chakra-ui/anatomy';
import {
createMultiStyleConfigHelpers,
} from '@chakra-ui/styled-system';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor, mode } from '@chakra-ui/theme-tools';
import type { Dict } from '@chakra-ui/utils';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import FormLabel from './FormLabel';
import Input from './Input';
import Textarea from './Textarea';
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys);
const getActiveLabelStyles = (theme: Dict, fc: string, bc: string, size: 'md' | 'lg') => {
const baseStyles = {
backgroundColor: bc,
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
};
switch (size) {
case 'md': {
return {
...baseStyles,
padding: '10px 16px 2px 16px',
};
}
case 'lg': {
return {
...baseStyles,
padding: '16px 24px 2px 24px',
};
}
}
};
const getDefaultLabelStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
};
}
case 'lg': {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
};
}
}
};
const getPaddingX = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return '16px';
}
function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) {
const { theme } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
case 'lg': {
return '24px';
const activeLabelStyles = {
...FormLabel.variants?.floating?.(props)._focusWithin,
...FormLabel.sizes?.[size](props)._focusWithin,
} || {};
const activeInputStyles = (() => {
switch (size) {
case 'md': {
return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
}
}
}
};
})();
const getActiveInputStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
const inputPx = (() => {
switch (size) {
case 'md': {
return '16px';
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
case 'lg': {
return '24px';
}
}
}
};
const variantFloating = definePartsStyle((props) => {
const { theme, backgroundColor, size = 'md' } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
const px = getPaddingX(size);
const activeInputStyles = getActiveInputStyles(size);
const activeLabelStyles = getActiveLabelStyles(theme, fc, bc, size);
})();
return {
container: {
// active styles
_focusWithin: {
label: {
...activeLabelStyles,
},
'input, textarea': {
...activeInputStyles,
},
'label .chakra-form__required-indicator': {
color: getColor(theme, fc),
},
},
// label's styles
label: {
...getDefaultLabelStyles(size),
left: '2px',
top: '2px',
zIndex: 2,
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': {
...activeLabelStyles,
label: activeLabelStyles,
'input, textarea': activeInputStyles,
},
// label styles
label: FormLabel.sizes?.[size](props) || {},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles,
'input[aria-invalid=true] + label, textarea[aria-invalid=true] + label': {
color: getColor(theme, ec),
},
// input's styles
// input styles
input: Input.sizes?.[size].field,
textarea: Textarea.sizes?.[size],
'input, textarea': {
padding: px,
padding: inputPx,
},
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
'input[disabled] + label, textarea[disabled] + label': {
backgroundColor: 'transparent',
},
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': {
...activeInputStyles,
},
// indicator's styles
// indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, fc),
},
......@@ -153,6 +84,11 @@ const variantFloating = definePartsStyle((props) => {
color: getColor(theme, ec),
},
},
};
}
const baseStyle = definePartsStyle((props) => {
return {
requiredIndicator: {
marginStart: 0,
color: mode('gray.500', 'whiteAlpha.400')(props),
......@@ -160,12 +96,42 @@ const variantFloating = definePartsStyle((props) => {
};
});
const variantFloating = definePartsStyle((props) => {
return {
container: {
label: FormLabel.variants?.floating(props) || {},
},
};
});
const sizes = {
lg: definePartsStyle((props) => {
if (props.variant === 'floating') {
return getFloatingVariantStylesForSize('lg', props);
}
return {};
}),
md: definePartsStyle((props) => {
if (props.variant === 'floating') {
return getFloatingVariantStylesForSize('md', props);
}
return {};
}),
};
const variants = {
floating: variantFloating,
};
const Form = defineMultiStyleConfig({
baseStyle,
variants,
sizes,
defaultProps: {
size: 'md',
},
});
export default Form;
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
fontSize: 'md',
marginEnd: '3',
mb: '2',
fontWeight: 'medium',
transitionProperty: 'common',
transitionDuration: 'normal',
opacity: 1,
_disabled: {
opacity: 0.4,
},
});
const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props;
const { focusColor: fc } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
return {
left: '2px',
top: '2px',
zIndex: 2,
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
_focusWithin: {
backgroundColor: bc,
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
},
};
});
const variants = {
floating: variantFloating,
};
const sizes = {
lg: defineStyle((props) => {
if (props.variant === 'floating') {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
_focusWithin: {
padding: '16px 24px 2px 24px',
},
};
}
return {};
}),
md: defineStyle((props) => {
if (props.variant === 'floating') {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
_focusWithin: {
padding: '10px 16px 2px 16px',
},
};
}
return {};
}),
};
const FormLabel = defineStyleConfig({
variants,
baseStyle,
sizes,
});
export default FormLabel;
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const baseStyle = defineStyle((props) => {
const { emptyColor, color } = props;
return {
borderColor: color || 'blue.500',
borderBottomColor: emptyColor || mode('blackAlpha.200', 'whiteAlpha.200')(props),
borderLeftColor: emptyColor || mode('blackAlpha.200', 'whiteAlpha.200')(props),
};
});
const Spinner = defineStyleConfig({
baseStyle,
defaultProps: {
size: 'md',
},
});
export default Spinner;
......@@ -53,6 +53,19 @@ const sizes = {
fontWeight: 500,
},
}),
xs: definePartsStyle({
th: {
px: '6px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '6px',
py: 6,
fontSize: 'sm',
fontWeight: 500,
},
}),
};
const variants = {
......
......@@ -23,11 +23,8 @@ const sizes = {
minH: 6,
minW: 6,
fontSize: 'sm',
lineHeight: 'sm',
px: 2,
py: '2px',
},
label: {
lineHeight: 5,
},
}),
......
......@@ -4,6 +4,7 @@ import Button from './Button';
import Checkbox from './Checkbox';
import Drawer from './Drawer';
import Form from './Form';
import FormLabel from './FormLabel';
import Heading from './Heading';
import Input from './Input';
import Link from './Link';
......@@ -11,6 +12,7 @@ import Modal from './Modal';
import Popover from './Popover';
import Radio from './Radio';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Table from './Table';
import Tabs from './Tabs';
import Tag from './Tag';
......@@ -27,11 +29,13 @@ const components = {
Heading,
Input,
Form,
FormLabel,
Link,
Modal,
Popover,
Radio,
Skeleton,
Spinner,
Tabs,
Table,
Tag,
......
......@@ -7,6 +7,7 @@ const global = (props: StyleFunctionProps) => ({
body: {
bg: mode('white', 'black')(props),
...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent',
},
form: {
w: '100%',
......
export interface AddressParam {
hash: string;
implementation_name: string;
name: string;
is_contract: boolean;
}
import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward';
export type BlockType = 'block' | 'reorg' | 'uncle';
export interface Block {
height: number;
timestamp: string;
tx_count: number;
miner: AddressParam;
size: number;
hash: string;
parent_hash: string;
difficulty: string;
total_difficulty: string;
gas_used: string | null;
gas_limit: string;
nonce: number;
base_fee_per_gas: number | null;
burnt_fees: number | null;
priority_fee: number | null;
extra_data: string | null;
state_root: string | null;
rewards?: Array<Reward>;
gas_target_percentage: number | null;
gas_used_percentage: number | null;
burnt_fees_percentage: number | null;
type: BlockType;
tx_fees: string | null;
uncles_hashes: Array<string>;
}
export interface BlocksResponse {
items: Array<Block>;
next_page_params: {
block_number: number;
items_count: number;
};
}
export interface DecodedInput {
method_call: string;
method_id: string;
parameters: Array<DecodedInputParams>;
}
export interface DecodedInputParams {
name: string;
type: string;
value: string;
indexed?: boolean;
}
export interface Fee {
type: string;
value: string;
}
import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
error: string | null;
success: boolean;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: number;
index: number;
block: number;
timestamp: string;
}
export interface InternalTransactionsResponse {
items: Array<InternalTransaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
transaction_index: number;
};
}
import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput';
export interface Log {
address: AddressParam;
topics: Array<string>;
data: string;
index: number;
decoded: DecodedInput | null;
}
export interface LogsResponse {
items: Array<Log>;
next_page_params: {
index: number;
items_count: number;
transaction_hash: string;
};
}
export interface RawTrace {
action: {
callType: string;
from: string;
gas: string;
input: string;
to: string;
value: string;
};
result: {
gasUsed: string;
output: string;
};
error: string | null;
subtraces: number;
traceAddress: Array<number>;
type: string;
}
export type RawTracesResponse = Array<RawTrace>;
export interface Reward {
reward: number;
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
}
import type { AddressParam } from './addressParams';
export type ERC1155TotalPayload = {
value: string;
token_id: string;
}
export type TokenTransfer = (
{
token_type: 'ERC-20';
total: {
value: string;
};
} |
{
token_type: 'ERC-721';
total: {
token_id: string;
};
} |
{
token_type: 'ERC-1155';
total: ERC1155TotalPayload | Array<ERC1155TotalPayload>;
}
) & TokenTransferBase
interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
txHash: string;
from: AddressParam;
to: AddressParam;
token_address: string;
token_symbol: string;
exchange_rate: string;
}
......@@ -6,7 +6,7 @@ export type Tokenlist = {
export type TokenlistItem = {
balance: number;
contractAddress: string;
decimals?: number;
decimals: number | null;
id: number;
name: string;
symbol: string;
......
import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
raw: string;
decoded: string;
} | DecodedInput;
export interface Transaction {
hash: string;
result: string;
confirmations: number;
status: 'ok' | 'error' | null;
block: number | null;
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: string;
fee: Fee;
gas_price: number;
type: number;
gas_used: string | null;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
priority_fee: number | null;
base_fee_per_gas: number | null;
tx_burnt_fee: number | null;
nonce: number;
position: number;
revert_reason: TransactionRevertReason | null;
raw_input: string;
decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string;
}
export interface TransactionsResponse {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
};
}
export type AppCategory = {
id: string;
name: string;
export enum MarketplaceCategoryId {
'all',
'favorites',
'defi',
'exchanges',
'finance',
'games',
'marketplaces',
'nft',
'security',
'social',
'tools',
'yieldFarming',
}
export type MarketplaceCategoriesIds = keyof typeof MarketplaceCategoryId;
export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = {
id: string;
title: string;
logo: string;
shortDescription: string;
categories: Array<AppCategory>;
categories: Array<MarketplaceCategoriesIds>;
}
export type AppItemOverview = AppItemPreview & {
chainIds: Array<string>;
author: string;
url: string;
description: string;
......
export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | undefined;
......@@ -2,16 +2,9 @@ import type { FunctionComponent, SVGAttributes } from 'react';
export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export interface Network {
name: string;
// https://chainlist.org/
chainId?: number;
shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string;
subType?: string;
group: 'mainnets' | 'testnets' | 'other';
export interface FeaturedNetwork {
title: string;
basePath: string;
group: NetworkGroup;
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
logo?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean;
}
export type Unit = 'wei' | 'gwei' | 'ether';
import { Box, Heading, Image, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
import type { AppItemPreview } from 'types/client/apps';
const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview) => {
const categoriesLabel = categories.map(c => c.name).join(', ');
import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
interface Props extends AppItemPreview {
onInfoClick: (id: string) => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppCard = ({ id,
title,
logo,
shortDescription,
categories,
onInfoClick,
isFavorite,
onFavoriteClick,
}: Props) => {
const categoriesLabel = categories.map(c => APP_CATEGORIES[c]).filter(notEmpty).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault();
onInfoClick(id);
}, [ onInfoClick, id ]);
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
return (
<Box
borderRadius={{ base: 'none', sm: 'md' }}
<LinkBox
_hover={{
boxShadow: 'md',
}}
_focusWithin={{
boxShadow: 'md',
}}
borderRadius="md"
height="100%"
padding={{ base: '16px', sm: '20px' }}
boxShadow={ `0 0 0 1px ${ useColorModeValue('var(--chakra-colors-gray-200)', 'var(--chakra-colors-gray-600)') }` }
padding={{ base: 3, sm: '20px' }}
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
role="group"
>
<Box overflow="hidden" height="100%">
<Box
display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }}
height="100%"
>
<Box
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
>
<Image
borderRadius={ 8 }
src={ logo }
alt={ `${ title } app icon` }
/>
</Box>
<Heading
gridColumn={{ base: 2, sm: 'auto' }}
as="h3"
marginBottom={ 2 }
fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold"
>
{ title }
<NextLink href={ link('app_index', { id: id }) } passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
</NextLink>
</Heading>
<Text
......@@ -49,9 +104,54 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview)
>
{ shortDescription }
</Text>
<Box
position="absolute"
right={{ base: 3, sm: '20px' }}
bottom={{ base: 3, sm: '20px' }}
paddingLeft={ 8 }
bgGradient={ `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)` }
>
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
maxW="100%"
overflow="hidden"
href="#"
onClick={ handleInfoClick }
>
More
<Icon
as={ northEastIcon }
marginLeft={ 1 }
/>
</Link>
</Box>
<IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
_groupHover={{ display: 'block' }}
position="absolute"
right={{ base: 3, sm: '10px' }}
top={{ base: 3, sm: '14px' }}
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/>
}
/>
</Box>
</Box>
</LinkBox>
);
};
export default AppCard;
export default React.memo(AppCard);
import { Box, Heading, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
export const AppCardSkeleton = () => {
return (
<Box
borderRadius="md"
height="100%"
padding={{ base: 3, sm: '20px' }}
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
>
<Box
display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }}
height="100%"
>
<Box
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
>
<SkeletonCircle w="100%" h="100%"/>
</Box>
<Heading
gridColumn={{ base: 2, sm: 'auto' }}
marginBottom={ 2 }
>
<Skeleton h={ 4 } w="50%"/>
</Heading>
<Box>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } w="50%"/>
</Box>
</Box>
</Box>
);
};
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react';
import { Grid, GridItem, Heading, VisuallyHidden } from '@chakra-ui/react';
import React from 'react';
import type { AppItemPreview } from 'types/client/apps';
import { apos } from 'lib/html-entities';
import AppCard from 'ui/apps/AppCard';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import AppModal from './AppModal';
type Props = {
apps: Array<AppItemPreview>;
onAppClick: (id: string) => void;
displayedAppId: string | null;
onModalClose: () => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppList = ({ apps }: Props) => {
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, onFavoriteClick }: Props) => {
return (
<>
<VisuallyHidden>
......@@ -20,32 +28,43 @@ const AppList = ({ apps }: Props) => {
{ apps.length > 0 ? (
<Grid
templateColumns={{
base: 'repeat(auto-fill, minmax(160px, 1fr))',
sm: 'repeat(auto-fill, minmax(200px, 1fr))',
sm: 'repeat(auto-fill, minmax(178px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '1px', sm: '24px' }}
gap={{ base: '16px', sm: '24px' }}
>
{ apps.map((app) => (
<GridItem
key={ app.id }
>
<AppCard
onInfoClick={ onAppClick }
id={ app.id }
title={ app.title }
logo={ app.logo }
shortDescription={ app.shortDescription }
categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick }
/>
</GridItem>
)) }
</Grid>
) : (
<EmptySearchResult/>
<EmptySearchResult text={ `Couldn${ apos }t find an app that matches your filter query.` }/>
) }
{ displayedAppId && (
<AppModal
id={ displayedAppId }
onClose={ onModalClose }
isFavorite={ favoriteApps.includes(displayedAppId) }
onFavoriteClick={ onFavoriteClick }
/>
) }
</>
);
};
export default AppList;
export default React.memo(AppList);
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
const AppListSkeleton = () => {
return (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ applicationStubs.map((app, index) => (
<GridItem
key={ index }
>
<AppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
export default AppListSkeleton;
import { LinkIcon, StarIcon } from '@chakra-ui/icons';
import {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react';
import type { FunctionComponent } from 'react';
import NextLink from 'next/link';
import React, { useCallback } from 'react';
import type { AppCategory, AppItemOverview } from 'types/client/apps';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
type Props = {
id: string | null;
id: string;
onClose: () => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppModal = ({
id,
onClose,
isFavorite,
onFavoriteClick,
}: Props) => {
const handleFavorite = useCallback(() => {
// TODO: implement
}, []);
if (!id) {
return null;
}
const {
title,
author,
description,
url,
site,
github,
telegram,
twitter,
logo,
categories,
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview;
const isFavorite = false;
} = marketplaceApps.find(app => app.id === id) as AppItemOverview;
const socialLinks = [
Boolean(telegram) && {
telegram ? {
icon: tgIcon,
url: telegram,
},
Boolean(twitter) && {
} : null,
twitter ? {
icon: twIcon,
url: twitter,
},
Boolean(github) && {
} : null,
github ? {
icon: ghIcon,
url: github,
},
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>;
} : null,
].filter(notEmpty);
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
return (
<Modal
......@@ -96,9 +98,6 @@ const AppModal = ({
fontWeight="medium"
lineHeight={ 1 }
color="blue.600"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
>
{ title }
</Heading>
......@@ -118,15 +117,16 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }}
>
<Box display="flex">
<Button
href={ url }
as="a"
size="sm"
marginRight={ 2 }
width={{ base: '100%', sm: 'auto' }}
>
Launch app
</Button>
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button
as="a"
size="sm"
marginRight={ 2 }
width={{ base: '100%', sm: 'auto' }}
>
Launch app
</Button>
</NextLink>
<IconButton
aria-label="Mark as favorite"
......@@ -135,10 +135,10 @@ const AppModal = ({
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavorite }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<Icon as={ StarIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 }/> }
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/> }
/>
</Box>
</Box>
......@@ -156,14 +156,14 @@ const AppModal = ({
</Heading>
<Box marginBottom={ 2 }>
{ categories.map((category: AppCategory) => (
{ categories.map((category: MarketplaceCategoriesIds) => APP_CATEGORIES[category] && (
<Tag
colorScheme="blue"
marginRight={ 2 }
marginBottom={ 2 }
key={ category.id }
key={ category }
>
{ category.name }
{ APP_CATEGORIES[category] }
</Tag>
)) }
</Box>
......@@ -188,10 +188,10 @@ const AppModal = ({
overflow="hidden"
>
<Icon
as={ LinkIcon }
as={ linkIcon }
display="inline"
verticalAlign="baseline"
boxSize={ 3 }
boxSize="18px"
marginRight={ 2 }
/>
......
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem';
import { APP_CATEGORIES } from './constants';
const categoriesList = Object.keys(APP_CATEGORIES).map((id: string) => ({
id: id,
name: APP_CATEGORIES[id as MarketplaceCategoriesIds],
})) as Array<MarketplaceCategory>;
type Props = {
selectedCategoryId: MarketplaceCategoriesIds;
onSelect: (category: MarketplaceCategoriesIds) => void;
}
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId);
return (
<Menu>
<MenuButton
as={ Button }
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
size="md"
variant="outline"
colorScheme="gray"
flexShrink={ 0 }
>
<Box
as="span"
display="flex"
alignItems="center"
>
{ selectedCategory?.name }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
{ categoriesList.map((category: MarketplaceCategory) => (
<CategoriesMenuItem
key={ category.id }
id={ category.id }
name={ category.name }
onClick={ onSelect }
/>
)) }
</MenuList>
</Menu>
);
};
export default React.memo(CategoriesMenu);
import { Icon, MenuItem } from '@chakra-ui/react';
import type { FunctionComponent, SVGAttributes } from 'react';
import React, { useCallback } from 'react';
import type { MarketplaceCategoriesIds } from 'types/client/apps';
import starFilledIcon from 'icons/star_filled.svg';
type Props = {
id: MarketplaceCategoriesIds;
name: string;
onClick: (category: MarketplaceCategoriesIds) => void;
}
const ICONS = {
favorites: starFilledIcon,
} as { [key in MarketplaceCategoriesIds]: FunctionComponent<SVGAttributes<SVGElement>> };
const CategoriesMenuItem = ({ id, name, onClick }: Props) => {
const handleSelection = useCallback(() => {
onClick(id);
}, [ id, onClick ]);
return (
<MenuItem
key={ id }
onClick={ handleSelection }
display="flex"
alignItems="center"
>
{ id in ICONS && (
<Icon mr={ 3 } as={ ICONS[id] } w={ 4 } h={ 4 } color="blackAlpha.800"/>
) }
{ name }
</MenuItem>
);
};
export default CategoriesMenuItem;
......@@ -2,9 +2,12 @@ import { Box, Heading, Icon, Text } from '@chakra-ui/react';
import React from 'react';
import emptyIcon from 'icons/empty_search_result.svg';
import { apos } from 'lib/html-entities';
const EmptySearchResult = () => {
interface Props {
text: string;
}
const EmptySearchResult = ({ text }: Props) => {
return (
<Box
display="flex"
......@@ -31,7 +34,7 @@ const EmptySearchResult = () => {
variant="secondary"
align="center"
>
Couldn{ apos }t find an app that matches your filter query.
{ text }
</Text>
</Box>
);
......
import type { AppCategory } from 'types/client/apps';
import type { MarketplaceCategoriesIds } from 'types/client/apps';
export const APP_CATEGORIES: Array<AppCategory> = [
{
id: 'defi',
name: 'DeFi',
},
{
id: 'exchanges',
name: 'Exchanges',
},
{
id: 'finance',
name: 'Finance',
},
{
id: 'games',
name: 'Games',
},
{
id: 'marketplaces',
name: 'Marketplaces',
},
{
id: 'nft',
name: 'NFT',
},
{
id: 'security',
name: 'Security',
},
{
id: 'social',
name: 'Social',
},
{
id: 'tools',
name: 'Tools',
},
{
id: 'Yield-farming',
name: 'yield-farming',
},
];
export const APP_CATEGORIES: {[key in MarketplaceCategoriesIds]: string} = {
favorites: 'Favorites',
all: 'All apps',
defi: 'DeFi',
exchanges: 'Exchanges',
finance: 'Finance',
games: 'Games',
marketplaces: 'Marketplaces',
nft: 'NFT',
security: 'Security',
social: 'Social',
tools: 'Tools',
yieldFarming: 'Yield farming',
};
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import appConfig from 'configs/app/config';
import marketplaceApps from 'data/marketplaceApps.json';
const favoriteAppsLocalStorageKey = 'favoriteApps';
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]');
} catch (e) {
return [];
}
}
function isAppNameMatches(q: string, app: AppItemOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOverview, favoriteApps: Array<string>) {
return category === 'all' ||
(category === 'favorites' && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplaceApps() {
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const [ category, setCategory ] = useState<MarketplaceCategoriesIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => setFilterQuery(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
const filterApps = useCallback((q: string, category: MarketplaceCategoriesIds) => {
const apps = defaultAppList
?.filter(app => {
return isAppNameMatches(q, app) && isAppCategoryMatches(category, app, favoriteApps);
});
setDisplayedApps(apps || []);
}, [ defaultAppList, favoriteApps ]);
const handleCategoryChange = useCallback((newCategory: MarketplaceCategoriesIds) => {
setCategory(newCategory);
}, []);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
useEffect(() => {
filterApps(filterQuery, category);
}, [ filterQuery, category, filterApps ]);
useEffect(() => {
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainIds.includes(appConfig.network.id))
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ ]);
return React.useMemo(() => ({
category,
handleCategoryChange,
debounceFilterApps,
isLoading,
displayedApps,
showAppInfo,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
}), [ category,
clearDisplayedAppId,
debounceFilterApps,
displayedAppId, displayedApps,
favoriteApps,
handleCategoryChange,
handleFavoriteClick,
isLoading,
showAppInfo,
]);
}
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import PrevNext from 'ui/shared/PrevNext';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, Block>(
[ 'block', router.query.id ],
async() => await fetch(`/api/blocks/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
scroller.scrollTo('BlockDetails__cutLink', {
duration: 500,
smooth: true,
});
}, []);
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
const increment = direction === 'next' ? +1 : -1;
const nextId = String(Number(router.query.id) + increment);
const url = link('block', { id: nextId });
router.push(url, undefined);
}, [ router ]);
if (isLoading) {
return <BlockDetailsSkeleton/>;
}
if (isError) {
const is404 = error?.error?.status === 404;
return is404 ? <Alert>This block has not been processed yet.</Alert> : <DataFetchAlert/>;
}
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<DetailsInfoItem
title="Block height"
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain."
>
{ data.height }
{ data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> }
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous block"
nextLabel="View next block"
isPrevDisabled={ data.height === 0 }
/>
</DetailsInfoItem>
<DetailsInfoItem
title="Size"
hint="Size of the block in bytes."
>
{ data.size.toLocaleString('en') }
</DetailsInfoItem>
<DetailsInfoItem
title="Timestamp"
hint="Date & time at which block was produced."
>
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text>
<TextSeparator/>
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="The number of transactions in the block."
>
<Link href={ link('block', { id: router.query.id }, { tab: 'transactions' }) }>
{ data.tx_count } transactions
</Link>
</DetailsInfoItem>
<DetailsInfoItem
title="Mined by"
hint="A block producer who successfully included the block onto the blockchain."
columnGap={ 1 }
>
<AddressLink hash={ data.miner.hash }/>
{ data.miner.name && <Text>(Miner: { data.miner.name })</Text> }
{ /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
{ !totalReward.isEqualTo(ZERO) && (
<DetailsInfoItem
title="Block reward"
hint={
`For each block, the miner is rewarded with a finite amount of ${ appConfig.network.currency || 'native token' }
on top of the fees paid for all transactions in the block.`
}
columnGap={ 1 }
>
<Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
{ (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && (
<Text variant="secondary" whiteSpace="break-spaces">(
<Tooltip label="Static block reward">
<span>{ staticReward.dividedBy(WEI).toFixed() }</span>
</Tooltip>
{ !txFees.isEqualTo(ZERO) && (
<>
{ space }+{ space }
<Tooltip label="Txn fees">
<span>{ txFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
) }
{ !burntFees.isEqualTo(ZERO) && (
<>
{ space }-{ space }
<Tooltip label="Burnt fees">
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
) }
)</Text>
) }
</DetailsInfoItem>
) }
{ sectionGap }
<DetailsInfoItem
title="Gas used"
hint="The total gas amount used in the block and its percentage of gas filled in the block."
>
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization
ml={ 4 }
mr={ 5 }
colorScheme="gray"
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
/>
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas limit"
hint="Total gas limit provided by all transactions in the block."
>
<Text>{ BigNumber(data.gas_limit).toFormat() }</Text>
</DetailsInfoItem>
{ data.base_fee_per_gas && (
<DetailsInfoItem
title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency } </Text>
<Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
</Text>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Burnt fees"
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"/>
<Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
{ !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
<Utilization
ml={ 4 }
value={ burntFees.dividedBy(txFees).toNumber() }
/>
</Box>
</Tooltip>
) }
</DetailsInfoItem>
{ data.priority_fee !== null && data.priority_fee > 0 && (
<DetailsInfoItem
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</DetailsInfoItem>
) }
{ /* api doesn't support extra data yet */ }
{ /* <DetailsInfoItem
title="Extra data"
hint="Any data that can be included by the miner in the block."
>
<Text whiteSpace="pre">{ data.extra_data } </Text>
<Text variant="secondary">(Hex: { data.extra_data })</Text>
</DetailsInfoItem> */ }
{ /* CUT */ }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="BlockDetails__cutLink">
<Link
mt={ 6 }
display="inline-block"
fontSize="sm"
textDecorationLine="underline"
textDecorationStyle="dashed"
onClick={ handleCutClick }
>
{ isExpanded ? 'Hide details' : 'View details' }
</Link>
</Element>
</GridItem>
{ /* ADDITIONAL INFO */ }
{ isExpanded && (
<>
{ sectionGap }
<DetailsInfoItem
title="Difficulty"
hint="Block difficulty for miner, used to calibrate block generation time."
>
{ BigNumber(data.difficulty).toFormat() }
</DetailsInfoItem>
<DetailsInfoItem
title="Total difficulty"
hint="Total difficulty of the chain until this block."
>
{ BigNumber(data.total_difficulty).toFormat() }
</DetailsInfoItem>
{ sectionGap }
<DetailsInfoItem
title="Hash"
hint="The SHA256 hash of the block."
flexWrap="nowrap"
>
<Box overflow="hidden">
<HashStringShortenDynamic hash={ data.hash }/>
</Box>
<CopyToClipboard text={ data.hash }/>
</DetailsInfoItem>
{ data.height > 0 && (
<DetailsInfoItem
title="Parent hash"
hint="The hash of the block from which this block was generated."
flexWrap="nowrap"
>
<AddressLink hash={ data.parent_hash } type="block" id={ String(data.height - 1) }/>
<CopyToClipboard text={ data.parent_hash }/>
</DetailsInfoItem>
) }
{ /* api doesn't support state root yet */ }
{ /* <DetailsInfoItem
title="State root"
hint="The root of the state trie."
>
<Text wordBreak="break-all" whiteSpace="break-spaces">{ data.state_root }</Text>
</DetailsInfoItem> */ }
<DetailsInfoItem
title="Nonce"
hint="Block nonce is a value used during mining to demonstrate proof of work for a block."
>
{ data.nonce }
</DetailsInfoItem>
{ data.rewards
?.filter(({ type }) => type !== 'Validator Reward' && type !== 'Miner Reward')
.map(({ type, reward }) => (
<DetailsInfoItem
key={ type }
title={ type }
hint="Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees."
>
{ BigNumber(reward).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</DetailsInfoItem>
)) }
</>
) }
</Grid>
);
};
export default BlockDetails;
import React from 'react';
import TxsContent from 'ui/txs/TxsContent';
const BlockTxs = () => {
return <TxsContent showDescription={ false } showSortButton={ false } txs={ [] }/>;
};
export default BlockTxs;
import { Grid, GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const SkeletonRow = ({ w = '100%' }: { w?: string }) => (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
</GridItem>
</>
);
const BlockDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<SkeletonRow w="25%"/>
<SkeletonRow w="25%"/>
<SkeletonRow w="65%"/>
<SkeletonRow w="25%"/>
<SkeletonRow/>
<SkeletonRow/>
{ sectionGap }
<SkeletonRow w="50%"/>
<SkeletonRow w="25%"/>
<SkeletonRow w="50%"/>
<SkeletonRow w="50%"/>
<SkeletonRow w="50%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default BlockDetailsSkeleton;
import { Box, Text, Show, Alert, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { BlockType, BlocksResponse } from 'types/api/block';
import useFetch from 'lib/hooks/useFetch';
import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
interface Props {
type?: BlockType;
}
const BlocksContent = ({ type }: Props) => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ 'blocks', type ],
async() => await fetch(`/api/blocks${ type ? `?type=${ type }` : '' }`),
);
if (isLoading) {
return (
<>
<Show below="lg" key="skeleton-mobile">
<BlocksSkeletonMobile/>
</Show>
<Show above="lg" key="skeleton-desktop">
<Skeleton h={ 6 } mb={ 8 } w="150px"/>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Alert>There are no blocks.</Alert>;
}
return (
<>
<Text>Total of { data.items[0].height.toLocaleString() } blocks</Text>
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
<Show above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/>
</Box>
</>
);
};
export default BlocksContent;
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { Block } from 'types/api/block';
import BlocksListItem from 'ui/blocks/BlocksListItem';
interface Props {
data: Array<Block>;
}
const BlocksList = ({ data }: Props) => {
return (
<Box mt={ 8 }>
{ data.map((item) => <BlocksListItem key={ item.height } data={ item }/>) }
</Box>
);
};
export default BlocksList;
import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization';
interface Props {
data: Block;
isPending?: boolean;
}
const BlocksListItem = ({ data, isPending }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const { totalReward, burntFees, txFees } = getBlockReward(data);
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm" color="blue.500" emptyColor={ spinnerEmptyColor }/> }
<Link
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
>
{ data.height }
</Link>
</Flex>
<Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Miner</Text>
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text>
<Text variant="secondary">{ data.tx_count }</Text>
</Flex>
<Box>
<Text fontWeight={ 500 }>Gas used</Text>
<Flex columnGap={ 4 }>
<Text variant="secondary">{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
</Flex>
</Box>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text>
<Text variant="secondary">{ totalReward.div(WEI).toFixed() }</Text>
</Flex>
<Flex>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" ml={ 2 }/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
</AccountListItemMobile>
);
};
export default BlocksListItem;
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const BlocksSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 } justifyContent="space-between">
<Skeleton w="75px"/>
<Skeleton w="90px"/>
</Flex>
<Skeleton h={ 6 } w="130px"/>
<Skeleton h={ 6 } w="180px"/>
<Skeleton h={ 6 } w="60px"/>
<Skeleton h={ 6 } w="100%"/>
<Skeleton h={ 6 } w="170px"/>
<Skeleton h={ 6 } w="100%"/>
</Flex>
)) }
</Box>
);
};
export default BlocksSkeletonMobile;
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import BlocksTableItem from 'ui/blocks/BlocksTableItem';
interface Props {
data: Array<Block>;
}
const BlocksTable = ({ data }: Props) => {
return (
<TableContainer width="100%" mt={ 8 }>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead>
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size</Th>
<Th width="21%" minW="144px">Miner</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency }</Th>
<Th width="22%">Burnt fees { appConfig.network.currency }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item }/>) }
</Tbody>
</Table>
</TableContainer>
);
};
export default BlocksTable;
import { Tr, Td, Text, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Block } from 'types/api/block';
import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization';
interface Props {
data: Block;
isPending?: boolean;
}
const BlocksTableItem = ({ data, isPending }: Props) => {
const { totalReward, burntFees, txFees } = getBlockReward(data);
return (
<Tr>
<Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<Link
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
>
{ data.height }
</Link>
</Tooltip>
</Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm">
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td>
<Td isNumeric fontSize="sm">{ data.tx_count }</Td>
<Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Flex mt={ 2 }>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
<GasUsedToTargetRatio ml={ 2 } value={ data.gas_target_percentage || undefined }/>
</Flex>
</Td>
<Td fontSize="sm">{ totalReward.dividedBy(WEI).toFixed() }</Td>
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
</Box>
</Tooltip>
</Td>
</Tr>
);
};
export default React.memo(BlocksTableItem);
......@@ -12,8 +12,8 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
......@@ -128,7 +128,7 @@ const ApiKeysPage: React.FC = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="API keys"/>
<PageTitle text="API keys"/>
{ content }
</Box>
</Page>
......
import debounce from 'lodash/debounce';
import React, { useCallback, useState } from 'react';
import { Box, Icon, Link } from '@chakra-ui/react';
import React from 'react';
import type { AppItemOverview } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps';
import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg';
import AppList from 'ui/apps/AppList';
import AppModal from 'ui/apps/AppModal';
import FilterInput from 'ui/apps/FilterInput';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput';
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ]
.sort((a, b) => a.title.localeCompare(b.title));
import useMarketplaceApps from '../apps/useMarkeplaceApps';
const Apps = () => {
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>(defaultDisplayedApps);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>('component');
const {
isLoading,
category,
handleCategoryChange,
debounceFilterApps,
showAppInfo,
displayedApps,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
} = useMarketplaceApps();
const filterApps = (q: string) => {
const apps = displayedApps
.filter(app => app.title.toLowerCase().includes(q.toLowerCase()));
return (
<>
<Box
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<CategoriesMenu
selectedCategoryId={ category }
onSelect={ handleCategoryChange }
/>
setDisplayedApps(apps);
};
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
</Box>
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []);
{ isLoading ? <AppListSkeleton/> : (
<AppList
apps={ displayedApps }
onAppClick={ showAppInfo }
displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId }
favoriteApps={ favoriteApps }
onFavoriteClick={ handleFavoriteClick }
/>
) }
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
{ config.marketplaceSubmitForm && (
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
marginTop={{ base: 8, sm: 16 }}
href={ config.marketplaceSubmitForm }
isExternal
>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
return (
<>
<FilterInput onChange={ debounceFilterApps }/>
<AppList apps={ displayedApps }/>
<AppModal
id={ displayedAppId }
onClose={ clearDisplayedAppId }
/>
Submit an App
</Link>
) }
</>
);
};
......
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import BlockDetails from 'ui/block/BlockDetails';
import BlockTxs from 'ui/block/BlockTxs';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <BlockDetails/> },
{ id: 'txs', title: 'Transactions', component: <BlockTxs/> },
];
const BlockPageContent = () => {
const router = useRouter();
if (!router.query.id) {
return null;
}
return (
<Page>
<PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs
tabs={ TABS }
/>
</Page>
);
};
export default BlockPageContent;
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import BlocksContent from 'ui/blocks/BlocksContent';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [
{ id: 'blocks', title: 'All', component: <BlocksContent/> },
{ id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg"/> },
{ id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle"/> },
];
const BlocksPageContent = () => {
return (
<Page>
<PageTitle text="Blocks"/>
<RoutedTabs
tabs={ TABS }
/>
</Page>
);
};
export default BlocksPageContent;
......@@ -11,8 +11,8 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
......@@ -116,7 +116,7 @@ const CustomAbiPage: React.FC = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="Custom ABI"/>
<PageTitle text="Custom ABI"/>
{ content }
</Box>
</Page>
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import * as Sentry from '@sentry/react';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => {
const router = useRouter();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && appConfig.isAccountSupported));
}, []);
const checkSentry = React.useCallback(() => {
Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } });
}, []);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
return (
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageTitle text={
`Home Page for ${ appConfig.network.name } network`
}/>
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
{ /* will be deleted when we move to new CI */ }
{ isFormVisible && (
<>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Page>
);
};
export default Home;
import { Box, Center, useColorMode } from '@chakra-ui/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
type Props = {
app?: AppItemOverview;
isLoading: boolean;
}
const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode();
const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false);
}, []);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' +
'allow-top-navigation-by-user-activation allow-popups';
const allowAttributeValue = 'clipboard-read; clipboard-write;';
useEffect(() => {
if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: Number(appConfig.network.id) }, app.url);
}
}, [ isFrameLoading, app, colorMode, ref ]);
return (
<Page wrapChildren={ false }>
<Center
as="main"
h="100vh"
paddingTop={{ base: '138px', lg: 0 }}
>
{ (isFrameLoading) && (
<ContentLoader/>
) }
{ app && (
<Box
allow={ allowAttributeValue }
ref={ ref }
sandbox={ sandboxAttributeValue }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ app.url }
title={ app.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
</Page>
);
};
export default MarketplaceApp;
......@@ -4,8 +4,8 @@ import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => {
......@@ -25,7 +25,6 @@ const MyProfile = () => {
<UserAvatar size={ 64 } data={ data }/>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.name || '' }
......@@ -34,7 +33,6 @@ const MyProfile = () => {
</FormControl>
<FormControl variant="floating" id="nickname" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.nickname || '' }
......@@ -43,7 +41,6 @@ const MyProfile = () => {
</FormControl>
<FormControl variant="floating" id="email" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.email }
......@@ -56,7 +53,7 @@ const MyProfile = () => {
return (
<Page>
<PageHeader text="My profile"/>
<PageTitle text="My profile"/>
{ content }
</Page>
);
......
import {
Box,
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useLink from 'lib/link/useLink';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const TABS = [ 'address', 'transaction' ] as const;
type TabName = typeof TABS[number];
type Props = {
tab: TabName;
}
const PrivateTags = ({ tab }: Props) => {
const [ , setActiveTab ] = useState<TabName>(tab);
const link = useLink();
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const onChangeTab = useCallback((index: number) => {
setActiveTab(TABS[index]);
const newUrl = link(TABS[index] === 'address' ? 'private_tags_address' : 'private_tags_tx');
history.replaceState(history.state, '', newUrl);
}, [ link ]);
const TABS: Array<RoutedTab> = [
{ id: 'address', title: 'Address', component: <PrivateAddressTags/> },
{ id: 'tx', title: 'Transaction', component: <PrivateTransactionTags/> },
];
const PrivateTags = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="Private tags"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onChangeTab } defaultIndex={ TABS.indexOf(tab) }>
<TabList marginBottom={{ base: 6, lg: 8 }}>
<Tab>Address</Tab>
<Tab>Transaction</Tab>
</TabList>
<TabPanels>
<TabPanel padding={ 0 }>
<PrivateAddressTags/>
</TabPanel>
<TabPanel padding={ 0 }>
<PrivateTransactionTags/>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
<PageTitle text="Private tags"/>
<RoutedTabs tabs={ TABS }/>
</Page>
);
};
......
import { ArrowBackIcon } from '@chakra-ui/icons';
import { Box, Link, Text } from '@chakra-ui/react';
import { Link, Text, Icon } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll';
import type { PublicTag } from 'types/api/account';
import eastArrowIcon from 'icons/arrows/east.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import useToast from 'lib/hooks/useToast';
import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
type TScreen = 'data' | 'form';
......@@ -77,16 +77,14 @@ const PublicTagsComponent: React.FC = () => {
return (
<Page>
<Box h="100%">
{ isMobile && screen === 'form' && (
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<ArrowBackIcon w={ 6 } h={ 6 }/>
<Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text>
</Link>
) }
<PageHeader text={ header }/>
{ content }
</Box>
{ isMobile && screen === 'form' && (
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
<Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text>
</Link>
) }
<PageTitle text={ header }/>
{ content }
</Page>
);
};
......
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { Flex, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { RouteName } from 'lib/link/routes';
import useLink from 'lib/link/useLink';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import link from 'lib/link/link';
import ExternalLink from 'ui/shared/ExternalLink';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState';
interface Tab {
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state';
name: string;
path?: string;
component?: React.ReactNode;
routeName: RouteName;
}
const TABS: Array<Tab> = [
{ type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> },
{ type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> },
{ type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> },
{ type: 'state', routeName: 'tx_state', name: 'State', component: <TxState/> },
{ type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> },
// import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready
// { id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
];
export interface Props {
tab: Tab['type'];
}
const TransactionPageContent = ({ tab }: Props) => {
const [ , setActiveTab ] = React.useState<Tab['type']>(tab);
const router = useRouter();
const link = useLink();
const handleTabChange = React.useCallback((index: number) => {
const nextTab = TABS[index];
setActiveTab(nextTab.type);
const newUrl = link(nextTab.routeName, { id: router.query.id as string });
window.history.replaceState(history.state, '', newUrl);
}, [ setActiveTab, link, router.query.id ]);
const defaultIndex = TABS.map(({ type }) => type).indexOf(tab);
const TransactionPageContent = () => {
return (
<Page>
<PageHeader text="Transaction details"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } defaultIndex={ defaultIndex }>
<TabList marginBottom={{ base: 6, lg: 8 }} flexWrap="wrap">
{ TABS.map((tab) => <Tab key={ tab.type }>{ tab.name }</Tab>) }
</TabList>
<TabPanels>
{ TABS.map((tab) => <TabPanel padding={ 0 } key={ tab.type }>{ tab.component || tab.name }</TabPanel>) }
</TabPanels>
</Tabs>
{ /* TODO should be shown only when navigating from txs list */ }
<Link mb={ 6 } display="inline-flex" href={ link('txs') }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<PageTitle text="Transaction details"/>
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
mb={{ base: 6, lg: 'initial' }}
py={ 2.5 }
>
<ExternalLink title="Open in Tenderly" href="#"/>
<ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/>
</Flex>
</Flex>
<RoutedTabs
tabs={ TABS }
/>
</Page>
);
};
......
import {
Box,
} from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsPending from 'ui/txs/TxsPending';
import TxsValidated from 'ui/txs/TxsValidated';
const TABS: Array<RoutedTab> = [
{ id: 'validated', title: 'Validated', component: <TxsValidated/> },
{ id: 'pending', title: 'Pending', component: <TxsPending/> },
];
const Transactions = () => {
return (
<Page>
<Box h="100%">
<PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS }/>
</Box>
</Page>
);
};
export default Transactions;
......@@ -8,8 +8,8 @@ import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
......@@ -113,7 +113,7 @@ const WatchList: React.FC = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="Watch list"/>
<PageTitle text="Watch list"/>
{ content }
</Box>
</Page>
......
import type { InputProps } from '@chakra-ui/react';
import { IconButton, Icon, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
......@@ -17,7 +18,7 @@ interface Props {
error?: FieldError;
onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
size?: string;
size?: InputProps['size'];
}
const MAX_INPUTS_NUM = 10;
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
......@@ -12,7 +13,7 @@ const TEXT_INPUT_MAX_LENGTH = 255;
interface Props {
control: Control<Inputs>;
error?: FieldError;
size?: string;
size?: InputProps['size'];
}
export default function PublicTagFormComment({ control, error, size }: Props) {
......@@ -22,7 +23,6 @@ export default function PublicTagFormComment({ control, error, size }: Props) {
<Textarea
{ ...field }
isInvalid={ Boolean(error) }
size={ size }
/>
<FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
......
......@@ -16,7 +16,6 @@ import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
......@@ -57,9 +56,8 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const fetch = useFetch();
const inputSize = isMobile ? 'md' : 'lg';
const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: {
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form';
......@@ -14,7 +15,7 @@ interface Props<TInputs extends FieldValues> {
control: Control<TInputs, object>;
pattern?: RegExp;
error?: FieldError;
size?: string;
size?: InputProps['size'];
}
export default function PublicTagsFormInput<Inputs extends FieldValues>({
......@@ -31,7 +32,6 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
<FormControl variant="floating" id={ field.name } isRequired={ required } size={ size }>
<Input
{ ...field }
size={ size }
required={ required }
isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH }
......
import { VStack, useColorModeValue } from '@chakra-ui/react';
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
}
const AccountListItemMobile = ({ children }: Props) => {
const AccountListItemMobile = ({ children, className }: Props) => {
return (
<VStack
gap={ 4 }
<Flex
rowGap={ 6 }
alignItems="flex-start"
flexDirection="column"
paddingY={ 6 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px"
_last={{
borderBottomWidth: '1px',
}}
className={ className }
>
{ children }
</VStack>
</Flex>
);
};
export default AccountListItemMobile;
export default chakra(AccountListItemMobile);
import type { InputProps } from '@chakra-ui/react';
import {
Input,
FormControl,
......@@ -11,7 +12,7 @@ import { ADDRESS_LENGTH } from 'lib/validations/address';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
size?: string;
size?: InputProps['size'];
placeholder?: string;
backgroundColor?: string;
error?: FieldError;
......@@ -31,7 +32,6 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
{ ...field }
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
size={ size }
/>
<FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
</FormControl>
......
......@@ -19,7 +19,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => {
<AddressLink hash={ address } fontWeight="600" ml={ 2 }/>
<CopyToClipboard text={ address } ml={ 1 }/>
</Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={{ base: 0, lg: 8 }}>{ subtitle }</Text> }
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
</Box>
);
};
......
import { Box, Text, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Unit } from 'types/unit';
import getValueWithUnit from 'lib/getValueWithUnit';
interface Props {
value: string;
unit?: Unit;
currency?: string;
exchangeRate?: string | null;
className?: string;
accuracy?: number;
accuracyUsd?: number;
}
const CurrencyValue = ({ value, currency = '', unit, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
const valueCurr = getValueWithUnit(value, unit);
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdContent;
if (exchangeRate !== undefined && exchangeRate !== null) {
const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn);
let usdResult: string;
if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
} else {
usdResult = usdBn.toFormat();
}
usdContent = (
<Text as="span" variant="secondary" fontWeight={ 400 }>(${ usdResult })</Text>
);
}
return (
<Box as="span" className={ className } display="inline-flex" rowGap={ 3 } columnGap={ 1 }>
<Text display="inline-block">
{ valueResult }{ currency ? ` ${ currency }` : '' }
</Text>
{ usdContent }
</Box>
);
};
export default React.memo(chakra(CurrencyValue));
......@@ -13,7 +13,7 @@ interface Props extends HTMLChakraProps<'div'> {
const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
return (
<>
<GridItem py={ 2 } lineHeight={ 5 } { ...styles } whiteSpace="nowrap">
<GridItem py={{ base: 1, lg: 2 }} lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="center">
<Tooltip
label={ hint }
......@@ -24,10 +24,20 @@ const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
<Text fontWeight={ 500 }>{ title }</Text>
<Text fontWeight={{ base: 700, lg: 500 }}>{ title }</Text>
</Flex>
</GridItem>
<GridItem display="flex" alignItems="center" py={ 2 } lineHeight={ 5 } whiteSpace="nowrap" { ...styles }>
<GridItem
display="flex"
alignItems="center"
flexWrap="wrap"
rowGap={ 3 }
pl={{ base: 7, lg: 0 }}
py={{ base: 1, lg: 2 }}
lineHeight={ 5 }
whiteSpace="nowrap"
{ ...styles }
>
{ children }
</GridItem>
</>
......
import { Link, Icon } from '@chakra-ui/react';
import React from 'react';
import arrowIcon from 'icons/arrows/north-east.svg';
interface Props {
href: string;
title: string;
}
const ExternalLink = ({ href, title }: Props) => {
return (
<Link fontSize="sm" display="inline-flex" alignItems="center" target="_blank" href={ href }>
{ title }
<Icon as={ arrowIcon } boxSize={ 4 }/>
</Link>
);
};
export default React.memo(ExternalLink);
import { Box, Button, Circle, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>;
interface Props {
isActive: boolean;
appliedFiltersNum?: number;
onClick: () => void;
}
const FilterButton = ({ isActive, appliedFiltersNum, onClick }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
return (
<Button
ref={ ref }
rightIcon={ appliedFiltersNum ? <Circle bg={ badgeBgColor } size={ 5 } color={ badgeColor }>{ appliedFiltersNum }</Circle> : undefined }
size="sm"
fontWeight="500"
variant="outline"
colorScheme="gray-dark"
onClick={ onClick }
isActive={ isActive }
px={ 1.5 }
flexShrink={ 0 }
>
{ FilterIcon }
<Box display={{ base: 'none', lg: 'block' }}>Filter</Box>
</Button>
);
};
export default React.forwardRef(FilterButton);
import { SearchIcon } from '@chakra-ui/icons';
import { Input, InputGroup, InputLeftElement, useColorModeValue } from '@chakra-ui/react';
import { Input, InputGroup, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
import searchIcon from 'icons/search.svg';
type Props = {
onChange: (q: string) => void;
onChange: (searchTerm: string) => void;
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string;
}
const FilterInput = ({ onChange }: Props) => {
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
const [ filterQuery, setFilterQuery ] = useState('');
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
......@@ -19,22 +23,25 @@ const FilterInput = ({ onChange }: Props) => {
return (
<InputGroup
size="sm"
size={ size }
className={ className }
>
<InputLeftElement
pointerEvents="none"
>
<SearchIcon color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
<Icon as={ searchIcon } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement>
<Input
size="sm"
size={ size }
value={ filterQuery }
onChange={ handleFilterQueryChange }
marginBottom={{ base: '4', lg: '6' }}
placeholder={ placeholder }
borderWidth="2px"
textOverflow="ellipsis"
/>
</InputGroup>
);
};
export default FilterInput;
export default chakra(FilterInput);
import { SearchIcon } from '@chakra-ui/icons';
import { Flex, Icon, Button, Circle, InputGroup, InputLeftElement, Input, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React from 'react';
import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 }/>;
const Filters = () => {
const [ isActive, setIsActive ] = React.useState(false);
const [ value, setValue ] = React.useState('');
const handleClick = React.useCallback(() => {
setIsActive(flag => !flag);
}, []);
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
}, []);
const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
return (
<Flex>
<Button
leftIcon={ FilterIcon }
rightIcon={ isActive ? <Circle bg={ badgeBgColor } size={ 5 } color={ badgeColor }>2</Circle> : undefined }
size="sm"
variant="outline"
colorScheme="gray-dark"
borderWidth="1px"
onClick={ handleClick }
isActive={ isActive }
px={ 1.5 }
>
Filter
</Button>
<InputGroup size="xs" ml={ 3 } maxW="360px">
<InputLeftElement ml={ 1 }>
<SearchIcon w={ 5 } h={ 5 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
placeholder="Search by addresses, hash, method..."
ml="1px"
onChange={ handleChange }
borderColor={ inputBorderColor }
value={ value }
size="xs"
/>
</InputGroup>
</Flex>
);
};
export default Filters;
import { Stat, StatArrow, Text, chakra } from '@chakra-ui/react';
import React from 'react';
type Props = ({
value: number;
} | {
used: number;
target: number;
}) & {
className?: string;
}
const GasUsedToTargetRatio = (props: Props) => {
const percentage = (() => {
if ('value' in props) {
return props.value;
}
return (props.used / props.target - 1) * 100;
})();
return (
<Stat className={ props.className }>
<StatArrow type={ percentage >= 0 ? 'increase' : 'decrease' }/>
<Text as="span" color={ percentage >= 0 ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ Math.abs(percentage).toLocaleString('en', { maximumFractionDigits: 2 }) } %
</Text>
</Stat>
);
};
export default React.memo(chakra(GasUsedToTargetRatio));
import { Box, HStack, VStack } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import Header from 'ui/blocks/header/Header';
import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop';
interface Props {
children: React.ReactNode;
}
const Page = ({ children }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const fetch = useFetch();
const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type;
useQuery<unknown, unknown, unknown>([ 'csrf' ], async() => await fetch('/api/account/csrf'));
React.useEffect(() => {
if (typeof networkType === 'string') {
cookies.set(cookies.NAMES.NETWORK_TYPE, networkType);
}
if (typeof networkSubType === 'string') {
cookies.set(cookies.NAMES.NETWORK_SUB_TYPE, networkSubType);
}
}, [ networkType, networkSubType ]);
return (
<HStack
w="100%"
minH="100vh"
alignItems="stretch"
>
{ !isMobile && <NavigationDesktop/> }
<VStack width="100%" paddingX={ isMobile ? 4 : 8 } paddingTop={ isMobile ? 0 : 9 } paddingBottom={ 10 } spacing={ 0 }>
<Header/>
<Box
as="main"
w="100%"
paddingTop={ isMobile ? '138px' : '52px' }
>
{ children }
</Box>
</VStack>
</HStack>
);
};
export default Page;
import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import useFetch from 'lib/hooks/useFetch';
import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
interface Props {
children: React.ReactNode;
wrapChildren?: boolean;
}
const Page = ({ children, wrapChildren = true }: Props) => {
const fetch = useFetch();
useQuery<unknown, unknown, unknown>([ 'csrf' ], async() => await fetch('/api/account/csrf'));
const renderedChildren = wrapChildren ? (
<PageContent>{ children }</PageContent>
) : children;
return (
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header/>
{ renderedChildren }
</Flex>
</Flex>
);
};
export default Page;
import { Box } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
}
const PageContent = ({ children }: Props) => {
return (
<Box
as="main"
w="100%"
paddingX={{ base: 4, lg: 12 }}
paddingBottom={ 10 }
paddingTop={{ base: '138px', lg: 0 }}
>
{ children }
</Box>
);
};
export default PageContent;
import { Heading } from '@chakra-ui/react';
import React from 'react';
const PageHeader = ({ text }: {text: string}) => {
const PageTitle = ({ text }: {text: string}) => {
return (
<Heading as="h1" size="lg" marginBottom={{ base: 6, lg: 8 }}>{ text }</Heading>
);
};
export default PageHeader;
export default PageTitle;
import { Button, Flex, Input, Icon, IconButton } from '@chakra-ui/react';
import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg';
type Props = {
currentPage: number;
maxPage?: number;
}
const MAX_PAGE_DEFAULT = 50;
const Pagination = ({ currentPage, maxPage }: Props) => {
const pageNumber = (
<Flex alignItems="center">
<Button
variant="outline"
colorScheme="gray"
size="sm"
isActive
borderWidth="1px"
fontWeight={ 400 }
mr={ 3 }
h={ 8 }
>
{ currentPage }
</Button>
of
<Button
variant="outline"
colorScheme="gray"
size="sm"
width={ 8 }
borderWidth="1px"
fontWeight={ 400 }
ml={ 3 }
>
{ maxPage || MAX_PAGE_DEFAULT }
</Button>
</Flex>
);
return (
<Flex
fontSize="sm"
width={{ base: '100%', lg: 'auto' }}
justifyContent={{ base: 'space-between', lg: 'unset' }}
alignItems="center"
>
<Flex alignItems="center" justifyContent="space-between" w={{ base: '100%', lg: 'auto' }}>
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 8 }
/>
{ pageNumber }
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 8 }
/>
</Flex>
<Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
Go to <Input w="84px" size="xs" ml={ 2 }/>
</Flex>
</Flex>
);
};
export default Pagination;
import { Box, Icon, IconButton, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react';
import eastArrow from 'icons/arrows/east-mini.svg';
interface Props {
className?: string;
onClick: (direction: 'prev' | 'next') => void;
prevLabel?: string;
nextLabel?: string;
isPrevDisabled?: boolean;
isNextDisabled?: boolean;
}
const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled }: Props) => {
const handelPrevClick = React.useCallback(() => {
onClick('prev');
}, [ onClick ]);
const handelNextClick = React.useCallback(() => {
onClick('next');
}, [ onClick ]);
return (
<Box className={ className }>
<Tooltip label={ prevLabel }>
<IconButton
aria-label="prev"
icon={ <Icon as={ eastArrow } boxSize={ 6 }/> }
h={ 6 }
borderRadius="sm"
variant="subtle"
colorScheme="gray"
onClick={ handelPrevClick }
disabled={ isPrevDisabled }
/>
</Tooltip>
<Tooltip label={ nextLabel }>
<IconButton
aria-label="next"
icon={ <Icon as={ eastArrow }boxSize={ 6 } transform="rotate(180deg)"/> }
h={ 6 }
borderRadius="sm"
variant="subtle"
colorScheme="gray"
ml="10px"
onClick={ handelNextClick }
disabled={ isNextDisabled }
/>
</Tooltip>
</Box>
);
};
export default chakra(PrevNext);
import { Box, Flex, Select, Textarea } from '@chakra-ui/react';
import React from 'react';
import hexToUtf8 from 'lib/hexToUtf8';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
type DataType = 'Hex' | 'UTF-8'
......@@ -26,7 +27,7 @@ const RawInputData = ({ hex }: Props) => {
<CopyToClipboard text={ hex }/>
</Flex>
<Textarea
value={ selectedDataType === 'Hex' ? hex : 'UTF-8 equivalent' }
value={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
w="100%"
maxH="220px"
mt={ 2 }
......@@ -38,4 +39,4 @@ const RawInputData = ({ hex }: Props) => {
);
};
export default RawInputData;
export default React.memo(RawInputData);
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import type { RoutedTab } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import RoutedTabsMenu from './RoutedTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};
interface Props {
tabs: Array<RoutedTab>;
}
const RoutedTabs = ({ tabs }: Props) => {
const router = useRouter();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
useEffect(() => {
if (router.isReady) {
let tabIndex = 0;
if (router.query.tab) {
tabIndex = tabs.findIndex(({ id }) => id === router.query.tab);
if (tabIndex < 0) {
tabIndex = 0;
}
}
setActiveTabIndex(tabIndex);
}
}, [ tabs, router ]);
const isMobile = useIsMobile();
const { tabsCut, tabsList, tabsRefs, listRef } = useAdaptiveTabs(tabs, isMobile);
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
router.query.tab = nextTab.id;
router.push(router);
}, [ tabs, router ]);
return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex }>
<TabList
marginBottom={{ base: 6, lg: 12 }}
flexWrap="nowrap"
whiteSpace="nowrap"
ref={ listRef }
overflowY="hidden"
overflowX={{ base: 'auto', lg: undefined }}
overscrollBehaviorX="contain"
css={{
'scroll-snap-type': 'x mandatory',
// hide scrollbar
'&::-webkit-scrollbar': { /* Chromiums */
display: 'none',
},
'-ms-overflow-style': 'none', /* IE and Edge */
'scrollbar-width': 'none', /* Firefox */
}}
>
{ tabsList.map((tab, index) => {
if (!tab.id) {
return (
<RoutedTabsMenu
key="menu"
tabs={ tabs }
activeTab={ tabs[activeTabIndex] }
tabsCut={ tabsCut }
isActive={ activeTabIndex >= tabsCut }
styles={ tabsCut < tabs.length ?
// initially our cut is 0 and we don't want to show the menu button too
// but we want to keep it in the tabs row so it won't collapse
// that's why we only change opacity but not the position itself
{ opacity: tabsCut === 0 ? 0 : 1 } :
hiddenItemStyles
}
onItemClick={ handleTabChange }
buttonRef={ tabsRefs[index] }
/>
);
}
return (
<Tab
key={ tab.id }
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
>
{ tab.title }
</Tab>
);
}) }
</TabList>
<TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
</TabPanels>
</Tabs>
);
};
export default React.memo(RoutedTabs);
import { Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Button,
useDisclosure,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react';
import type { MenuButton, RoutedTab } from './types';
import { menuButton } from './utils';
interface Props {
tabs: Array<RoutedTab | MenuButton>;
activeTab: RoutedTab;
tabsCut: number;
isActive: boolean;
styles?: StyleProps;
onItemClick: (index: number) => void;
buttonRef: React.RefObject<HTMLButtonElement>;
}
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
onClose();
const tabIndex = (event.target as HTMLButtonElement).getAttribute('data-index');
if (tabIndex) {
onItemClick(tabsCut + Number(tabIndex));
}
}, [ onClose, onItemClick, tabsCut ]);
return (
<Popover isLazy placement="bottom-end" key="more" isOpen={ isOpen } onClose={ onClose } onOpen={ onOpen } closeDelay={ 0 }>
<PopoverTrigger>
<Button
variant="ghost"
isActive={ isOpen || isActive }
ref={ buttonRef }
{ ...styles }
>
{ menuButton.title }
</Button>
</PopoverTrigger>
<PopoverContent w="auto">
<PopoverBody display="flex" flexDir="column">
{ tabs.slice(tabsCut).map((tab, index) => (
<Button
key={ tab.id }
variant="ghost"
onClick={ handleItemClick }
isActive={ activeTab.id === tab.id }
justifyContent="left"
data-index={ index }
>
{ tab.title }
</Button>
)) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(RoutedTabsMenu);
export interface RoutedTab {
id: string;
title: string;
component: React.ReactNode;
}
export interface MenuButton {
id: null;
title: string;
component: null;
}
import _debounce from 'lodash/debounce';
import React from 'react';
import type { RoutedTab } from './types';
import { menuButton } from './utils';
export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boolean) {
// to avoid flickering we set initial value to 0
// so there will be no displayed tabs initially
const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0);
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths.at(-1);
if (!listWidth || !menuWidth) {
return tabs.length;
}
const { visibleNum } = tabWidths.slice(0, -1).reduce((result, item, index) => {
if (!item) {
return result;
}
if (result.accWidth + item <= listWidth - menuWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
if (result.accWidth + item <= listWidth && index === tabWidths.length - 2) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
return result;
}, { visibleNum: 0, accWidth: 0 });
return visibleNum;
}, [ tabs.length, tabsRefs ]);
const tabsList = React.useMemo(() => {
if (disabled) {
return tabs;
}
return [ ...tabs, menuButton ];
}, [ tabs, disabled ]);
React.useEffect(() => {
setTabsRefs(disabled ? [] : tabsList.map((_, index) => tabsRefs[index] || React.createRef()));
// update refs only when disabled prop changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ disabled ]);
React.useEffect(() => {
if (tabsRefs.length > 0) {
setTabsCut(calculateCut());
}
}, [ calculateCut, tabsRefs ]);
React.useEffect(() => {
if (tabsRefs.length === 0) {
return;
}
const resizeHandler = _debounce(() => {
setTabsCut(calculateCut());
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
};
}, [ calculateCut, tabsRefs.length ]);
return React.useMemo(() => {
return {
tabsCut,
tabsList,
tabsRefs,
listRef,
};
}, [ tabsList, tabsCut, tabsRefs, listRef ]);
}
import type { MenuButton } from './types';
import { middot } from 'lib/html-entities';
export const menuButton: MenuButton = {
id: null,
title: `${ middot }${ middot }${ middot }`,
component: null,
};
import { Icon, IconButton, chakra } from '@chakra-ui/react';
import React from 'react';
import upDownArrow from 'icons/arrows/up-down.svg';
type Props = {
handleSort: () => void;
isSortActive: boolean;
className?: string;
}
const SortButton = ({ handleSort, isSortActive, className }: Props) => {
return (
<IconButton
icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> }
aria-label="sort"
size="sm"
variant="outline"
colorScheme="gray-dark"
minWidth="36px"
onClick={ handleSort }
isActive={ isSortActive }
className={ className }
/>
);
};
export default chakra(SortButton);
import { chakra } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react';
const TextSeparator = (props: StyleProps) => {
return <chakra.span mx={ 3 } { ...props }>|</chakra.span>;
};
export default React.memo(TextSeparator);
import { Center, Icon, Link, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import tokeIcon from 'icons/tokens/toke.svg';
import usdtIcon from 'icons/tokens/usdt.svg';
import useLink from 'lib/link/useLink';
// temporary solution
// don't know where to get icons and addresses yet
const TOKENS = {
USDT: {
fullName: 'Tether USD',
symbol: 'USDT',
icon: usdtIcon,
address: '0x9bD35A17C9C7c8820f89e0277e2046CDC57aCB15',
},
TOKE: {
fullName: 'Tokemak',
symbol: 'TOKE',
icon: tokeIcon,
address: '0x9bD35A17C9C7c8820f89e0277e2046CDC57aCB15',
},
};
interface Props {
symbol: string;
className?: string;
}
const Token = ({ symbol, className }: Props) => {
const token = TOKENS[symbol as keyof typeof TOKENS];
const link = useLink();
if (!token) {
return null;
}
const url = link('token_index', { id: token.address });
return (
<Center className={ className }>
<Icon as={ token.icon } boxSize={ 5 }/>
<Link href={ url } target="_blank" ml={ 1 }>
{ token.fullName }
</Link>
<Text ml={ 1 }>({ token.symbol })</Text>
</Center>
);
};
export default chakra(Token);
import { Image, chakra } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
const EmptyElement = () => null;
interface Props {
hash: string;
name?: string;
className?: string;
}
const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
${ appConfig.network.assetsPathname || appConfig.network.type }
/assets/
${ hash }
/logo.png
`;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>;
};
export default React.memo(chakra(TokenLogo));
import { Center, Link, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import link from 'lib/link/link';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
symbol: string;
hash: string;
name: string;
className?: string;
}
const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
const url = link('token_index', { hash });
return (
<Center className={ className } columnGap={ 1 }>
<TokenLogo boxSize={ 5 } hash={ hash } name={ name }/>
<Link href={ url } target="_blank">
{ name }
</Link>
<Text variant="secondary">({ symbol })</Text>
</Center>
);
};
export default chakra(TokenSnippet);
import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: Transaction['status'];
errorText?: string | null;
}
const TxStatus = ({ status, errorText }: Props) => {
let label;
let icon;
let colorScheme;
switch (status) {
case 'ok':
label = 'Success';
icon = successIcon;
colorScheme = 'green';
break;
case 'error':
label = 'Failed';
icon = errorIcon;
colorScheme = 'red';
break;
case null:
label = 'Pending';
icon = pendingIcon;
// FIXME: it's not gray on mockups
// need to implement new color scheme or redefine colors here
colorScheme = 'gray';
break;
}
return (
<Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
</Tag>
</Tooltip>
);
};
export default TxStatus;
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from '../../playwright/RenderWithChakra';
import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } });
test('Utilization light', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('Utilization dark', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Text, chakra } from '@chakra-ui/react';
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react';
import clamp from 'lodash/clamp';
import React from 'react';
interface Props {
className?: string;
value: number;
colorScheme?: 'green' | 'gray';
}
const WIDTH = 50;
const Utilization = ({ className, value }: Props) => {
const valueString = (value * 100).toFixed(2) + '%';
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString('en', { maximumFractionDigits: 2 }) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.500');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return (
<Flex className={ className } alignItems="center">
<Box bg="gray.100" w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg="green.500" w={ valueString } h="100%"/>
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg={ color } w={ valueString } h="100%"/>
</Box>
<Text color="green.500" ml="10px" fontWeight="bold">{ valueString }</Text>
<Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text>
</Flex>
);
};
......
import { Link, chakra, shouldForwardProp } from '@chakra-ui/react';
import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token';
type?: 'address' | 'transaction' | 'token' | 'block';
alias?: string;
className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string;
id?: string;
}
const AddressLink = ({ type, className, truncation = 'dynamic', hash, fontWeight }: Props) => {
const link = useLink();
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight }: Props) => {
let url;
if (type === 'transaction') {
url = link('tx_index', { id: hash });
url = link('tx', { id: id || hash });
} else if (type === 'token') {
url = link('token_index', { id: hash });
url = link('token_index', { hash: id || hash });
} else if (type === 'block') {
url = link('block', { id: id || hash });
} else {
url = link('address_index', { id: hash });
url = link('address_index', { id: id || hash });
}
const content = (() => {
if (alias) {
return (
<Tooltip label={ hash }>
<Box overflow="hidden" textOverflow="ellipsis">{ alias }</Box>
</Tooltip>
);
}
switch (truncation) {
case 'constant':
return <HashStringShorten hash={ hash }/>;
......
......@@ -2,10 +2,10 @@ import { Icon, Box, Flex, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useC
import React from 'react';
import burgerIcon from 'icons/burger.svg';
import NavigationMobile from 'ui/blocks/navigation/NavigationMobile';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenuButton from 'ui/blocks/networkMenu/NetworkMenuButton';
import NetworkMenuContentMobile from 'ui/blocks/networkMenu/NetworkMenuContentMobile';
import NavigationMobile from 'ui/snippets/navigation/NavigationMobile';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenuButton from 'ui/snippets/networkMenu/NetworkMenuButton';
import NetworkMenuContentMobile from 'ui/snippets/networkMenu/NetworkMenuContentMobile';
const Burger = () => {
const iconColor = useColorModeValue('gray.600', 'white');
......
import type { UseCheckboxProps } from '@chakra-ui/checkbox';
import { useCheckbox } from '@chakra-ui/checkbox';
import { SunIcon } from '@chakra-ui/icons';
import { useColorMode, useColorModeValue, Icon } from '@chakra-ui/react';
import type {
SystemStyleObject,
......@@ -16,6 +15,7 @@ import { dataAttr, __DEV__ } from '@chakra-ui/utils';
import * as React from 'react';
import moonIcon from 'icons/moon.svg';
import sunIcon from 'icons/sun.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
export interface ColorModeTogglerProps
......@@ -101,10 +101,11 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
data-hover={ dataAttr(state.isHovered) }
__css={ thumbStyles }
/>
<SunIcon
boxSize={ 4 }
margin={ 2 }
<Icon
boxSize={ 5 }
margin={ 1.5 }
zIndex="docked"
as={ sunIcon }
color={ useColorModeValue('gray.500', 'blue.600') }
{ ...transitionProps }
/>
......
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/blocks/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/blocks/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/blocks/searchBar/SearchBar';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
const Header = () => {
const isMobile = useIsMobile();
const bgColor = useColorModeValue('white', 'black');
if (isMobile) {
return (
<Box bgColor={ bgColor }>
return (
<>
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
<Flex
as="header"
position="fixed"
......@@ -36,21 +33,22 @@ const Header = () => {
</Flex>
<SearchBar/>
</Box>
);
}
return (
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
</>
);
};
......
import { VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react';
import { Box, VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import ghIcon from 'icons/social/git.svg';
import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [
{ link: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK, icon: ghIcon, label: 'Github link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK, icon: twIcon, label: 'Twitter link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK, icon: tgIcon, label: 'Telegram link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link !== undefined);
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
{ link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link);
const BLOCKSCOUT_VERSION = process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT_VERSION }`;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
interface Props {
isCollapsed?: boolean;
......@@ -24,22 +23,13 @@ interface Props {
}
const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isMobile = useIsMobile();
const width = (() => {
if (isMobile) {
return '100%';
}
return isCollapsed ? '20px' : '180px';
})();
const isExpanded = isCollapsed === false;
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return isMobile ? 6 : 20;
return { base: 6, lg: 20 };
})();
return (
......@@ -48,8 +38,8 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
spacing={ 8 }
borderTop="1px solid"
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
width={ width }
paddingTop={ isMobile ? 6 : 8 }
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
paddingTop={{ base: 6, lg: 8 }}
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
......@@ -57,23 +47,24 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
<Stack direction={ isCollapsed ? 'column' : 'row' }>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }>
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
{ !isCollapsed && (
<>
<Text variant="secondary">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
</>
{ SOCIAL_LINKS.length > 0 && (
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }>
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
) }
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mb={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
};
......
......@@ -2,7 +2,6 @@ import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors';
......@@ -18,21 +17,15 @@ interface Props {
const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
const colors = useColors();
const isMobile = useIsMobile();
const width = (() => {
if (isMobile) {
return '100%';
}
return isCollapsed ? '60px' : '180px';
})();
const isExpanded = isCollapsed === false;
return (
<Box as="li" listStyleType="none" w="100%">
<NextLink href={ url } passHref>
<Link
w={ width }
px={ px || (isCollapsed ? '15px' : 3) }
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } }
py={ 2.5 }
display="flex"
color={ isActive ? colors.text.active : colors.text.default }
......@@ -53,7 +46,14 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
>
<HStack spacing={ 3 }>
<Icon as={ icon } boxSize="30px"/>
{ !isCollapsed && <Text variant="inherit" fontSize="sm" lineHeight="20px">{ text }</Text> }
<Text
variant="inherit"
fontSize="sm"
lineHeight="20px"
display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}
>
{ text }
</Text>
</HStack>
</Tooltip>
</Link>
......
import { ChevronLeftIcon } from '@chakra-ui/icons';
import { Flex, Box, VStack, useColorModeValue, useBreakpointValue } from '@chakra-ui/react';
import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import NavFooter from './NavFooter';
import NavLink from './NavLink';
const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const [ isCollapsed, setCollapsedState ] = React.useState(navBarCollapsedCookie === 'true');
const isInBrowser = isBrowser();
const [ hasAccount, setHasAccount ] = React.useState(false);
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>();
React.useEffect(() => {
if (!navBarCollapsedCookie) {
setCollapsedState(!isLargeScreen);
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
if (isInBrowser) {
if (navBarCollapsedCookie === 'true') {
setCollapsedState(true);
}
if (navBarCollapsedCookie === 'false') {
setCollapsedState(false);
}
setHasAccount(Boolean(appConfig.isAccountSupported && isAuth && isInBrowser));
}
}, [ isLargeScreen, navBarCollapsedCookie ]);
}, [ isInBrowser ]);
const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag);
......@@ -40,16 +46,19 @@ const NavigationDesktop = () => {
borderColor: useColorModeValue('blackAlpha.200', 'whiteAlpha.200'),
};
const isExpanded = isCollapsed === false;
return (
<Flex
display={{ base: 'none', lg: 'flex' }}
position="relative"
flexDirection="column"
alignItems="flex-start"
borderRight="1px solid"
borderColor={ containerBorderColor }
px={ isCollapsed ? 4 : 6 }
px={{ lg: isExpanded ? 6 : 4, xl: isCollapsed ? 4 : 6 }}
py={ 12 }
width={ isCollapsed ? '92px' : '229px' }
width={{ lg: isExpanded ? '229px' : '92px', xl: isCollapsed ? '92px' : '229px' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Box
......@@ -78,19 +87,20 @@ const NavigationDesktop = () => {
</Box>
) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<ChevronLeftIcon
<Icon
as={ chevronIcon }
width={ 6 }
height={ 6 }
border="1px"
_hover={{ color: 'blue.400' }}
borderRadius="base"
{ ...chevronIconStyles }
transform={ isCollapsed ? 'rotate(180deg)' : 'rotate(0)' }
transform={{ lg: isExpanded ? 'rotate(0)' : 'rotate(180deg)', xl: isCollapsed ? 'rotate(180deg)' : 'rotate(0)' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'transform, left' }) }
transformOrigin="center"
position="fixed"
position="absolute"
top="104px"
left={ isCollapsed ? '80px' : '216px' }
left={{ lg: isExpanded ? '216px' : '80px', xl: isCollapsed ? '80px' : '216px' }}
cursor="pointer"
onClick={ handleTogglerClick }
/>
......
import { Box, VStack } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/blocks/navigation/NavFooter';
import NavLink from 'ui/blocks/navigation/NavLink';
import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink';
const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const hasAccount = appConfig.isAccountSupported && isAuth;
return (
<>
......
......@@ -3,11 +3,27 @@ import NextLink from 'next/link';
import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react';
import appConfig from 'configs/app/config';
import blockscoutLogo from 'icons/logo.svg';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
// predefined network logos
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'/eth/mainnet': require('icons/networks/logos/eth.svg'),
'/etc/mainnet': require('icons/networks/logos/etc.svg'),
'/poa/core': require('icons/networks/logos/poa.svg'),
'/rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'/xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'/poa/sokol': require('icons/networks/logos/sokol.svg'),
'/artis/sigma1': require('icons/networks/logos/artis.svg'),
'/lukso/l14': require('icons/networks/logos/lukso.svg'),
'/astar': require('icons/networks/logos/astar.svg'),
'/shiden': require('icons/networks/logos/shiden.svg'),
'/shibuya': require('icons/networks/logos/shibuya.svg'),
};
interface Props {
isCollapsed?: boolean;
onClick?: (event: React.SyntheticEvent) => void;
......@@ -15,10 +31,8 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white');
const link = useLink();
const href = link('network_index');
const network = useNetwork();
const logo = network?.logo;
const logo = appConfig.network.logo || LOGOS[appConfig.network.basePath];
const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' });
......@@ -29,7 +43,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<Image
h="20px"
src={ logo }
alt={ `${ network.type } ${ network.subType ? network.subType : '' } network icon` }
alt={ `${ appConfig.network.name } network icon` }
/>
);
} else if (typeof logo !== undefined) {
......@@ -57,7 +71,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<NextLink href={ href } passHref>
<Box
as="a"
width={ isCollapsed ? '0' : '113px' }
width={{ base: '113px', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }}
display="inline-flex"
overflow="hidden"
onClick={ onClick }
......
......@@ -4,7 +4,7 @@ import React from 'react';
import NetworkMenuButton from './NetworkMenuButton';
import NetworkMenuContentDesktop from './NetworkMenuContentDesktop';
interface Props {
isCollapsed: boolean;
isCollapsed?: boolean;
}
const NetworkMenu = ({ isCollapsed }: Props) => {
......@@ -13,7 +13,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => {
{ ({ isOpen }) => (
<>
<PopoverTrigger>
<Box marginLeft={ isCollapsed ? '0px' : '7px' }>
<Box marginLeft={{ base: '7px', lg: isCollapsed === false ? '7px' : '0px', xl: isCollapsed ? '0px' : '7px' }}>
<NetworkMenuButton isActive={ isOpen }/>
</Box>
</PopoverTrigger>
......
......@@ -3,18 +3,17 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import NETWORKS from 'lib/networks/availableNetworks';
import featuredNetworks from 'lib/networks/featuredNetworks';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const availableTabs = TABS.filter((tab) => NETWORKS.some(({ group }) => group === tab));
const availableTabs = TABS.filter((tab) => featuredNetworks.some(({ group }) => group === tab));
const NetworkMenuPopup = () => {
const selectedNetwork = useNetwork();
const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab);
return (
......@@ -35,7 +34,7 @@ const NetworkMenuPopup = () => {
.filter((network) => network.group === tab)
.map((network) => (
<NetworkMenuLink
key={ network.name }
key={ network.title }
{ ...network }
/>
)) }
......
......@@ -4,7 +4,6 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink';
......@@ -12,9 +11,9 @@ import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const NetworkMenuContentMobile = () => {
const selectedNetwork = useNetwork();
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTab(event.target.value as NetworkGroup);
......@@ -30,7 +29,7 @@ const NetworkMenuContentMobile = () => {
.filter(({ group }) => group === selectedTab)
.map((network) => (
<NetworkMenuLink
key={ network.name }
key={ network.title }
{ ...network }
isMobile
/>
......
......@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { Network } from 'types/networks';
import type { FeaturedNetwork } from 'types/networks';
import checkIcon from 'icons/check.svg';
import placeholderIcon from 'icons/networks/icons/placeholder.svg';
import useColors from './useColors';
interface Props extends Network {
interface Props extends FeaturedNetwork {
isActive: boolean;
isMobile?: boolean;
url: string;
}
const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: Props) => {
const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon });
const iconEl = typeof icon === 'string' ? (
<Image w="30px" h="30px" src={ icon } alt={ `${ type } ${ subType ? subType : '' } network icon` }/>
<Image w="30px" h="30px" src={ icon } alt={ `${ title } network icon` }/>
) : (
<Icon
as={ hasIcon ? icon : placeholderIcon }
......@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }:
fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' }
>
{ name }
{ title }
</Text>
{ isActive && (
<Icon
......
......@@ -5,7 +5,7 @@ import type { UserInfo } from 'types/api/account';
import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/blocks/navigation/NavLink';
import NavLink from 'ui/snippets/navigation/NavLink';
type Props = UserInfo;
......@@ -33,10 +33,10 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => {
>
{ email }
</Text>
<NavLink { ...profileItem } isActive={ undefined } px="0px"/>
<NavLink { ...profileItem } isActive={ undefined } px="0px" isCollapsed={ false }/>
<Box as="nav" mt={ 2 } pt={ 2 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ undefined } px="0px"/>) }
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ undefined } isCollapsed={ false } px="0px"/>) }
</VStack>
</Box>
<Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
......
......@@ -2,8 +2,8 @@ import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@c
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ProfileMenuContent from 'ui/blocks/profileMenu/ProfileMenuContent';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuDesktop = () => {
const { data } = useFetchProfileInfo();
......@@ -11,7 +11,7 @@ const ProfileMenuDesktop = () => {
return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<PopoverTrigger>
<Button variant="unstyled" display="inline-flex" height="auto">
<Button variant="unstyled" display="inline-flex" height="auto" flexShrink={ 0 }>
<UserAvatar size={ 50 } data={ data }/>
</Button>
</PopoverTrigger>
......
......@@ -2,9 +2,9 @@ import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclos
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ColorModeToggler from 'ui/blocks/header/ColorModeToggler';
import ProfileMenuContent from 'ui/blocks/profileMenu/ProfileMenuContent';
import UserAvatar from 'ui/shared/UserAvatar';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
......
import type { ChangeEvent, FormEvent } from 'react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import useLink from 'lib/link/useLink';
import link from 'lib/link/link';
import SearchBarDesktop from './SearchBarDesktop';
import SearchBarMobile from './SearchBarMobile';
const SearchBar = () => {
const [ value, setValue ] = React.useState('');
const link = useLink();
const isMobile = useIsMobile();
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
......@@ -20,16 +17,13 @@ const SearchBar = () => {
event.preventDefault();
const url = link('search_results', undefined, { q: value });
window.location.assign(url);
}, [ link, value ]);
if (isMobile) {
return (
<SearchBarMobile onChange={ handleChange } onSubmit={ handleSubmit }/>
);
}
}, [ value ]);
return (
<SearchBarDesktop onChange={ handleChange } onSubmit={ handleSubmit }/>
<>
<SearchBarDesktop onChange={ handleChange } onSubmit={ handleSubmit }/>
<SearchBarMobile onChange={ handleChange } onSubmit={ handleSubmit }/>
</>
);
};
......
import { SearchIcon } from '@chakra-ui/icons';
import { InputGroup, Input, InputLeftAddon, InputLeftElement, useColorModeValue } from '@chakra-ui/react';
import { InputGroup, Input, InputLeftAddon, InputLeftElement, Icon, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg';
interface Props {
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
......@@ -10,11 +11,11 @@ interface Props {
const SearchBarDesktop = ({ onChange, onSubmit }: Props) => {
return (
<form noValidate onSubmit={ onSubmit }>
<chakra.form noValidate onSubmit={ onSubmit } display={{ base: 'none', lg: 'block' }} w="100%">
<InputGroup>
<InputLeftAddon w="111px">All filters</InputLeftAddon>
<InputLeftElement w={ 6 } ml="132px" mr={ 2.5 }>
<SearchIcon w={ 5 } h={ 5 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
<Icon as={ searchIcon } boxSize={ 6 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement>
<Input
paddingInlineStart="50px"
......@@ -24,7 +25,7 @@ const SearchBarDesktop = ({ onChange, onSubmit }: Props) => {
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
/>
</InputGroup>
</form>
</chakra.form>
);
};
......
import { SearchIcon } from '@chakra-ui/icons';
import { InputGroup, Input, InputLeftElement, useColorModeValue, chakra } from '@chakra-ui/react';
import { InputGroup, Input, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import clamp from 'lodash/clamp';
import throttle from 'lodash/throttle';
import React from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg';
import isBrowser from 'lib/isBrowser';
const SCROLL_DIFF_THRESHOLD = 20;
......@@ -61,10 +61,12 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => {
transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' }
transitionProperty="transform"
transitionDuration="slow"
display={{ base: 'block', lg: 'none' }}
w="100%"
>
<InputGroup size="sm">
<InputLeftElement >
<SearchIcon w={ 4 } h={ 4 } color={ searchIconColor }/>
<Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
......
import { Flex, Link, Text, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props {
value: string;
tokenId: string;
hash: string;
symbol: string;
}
const NftTokenTransferSnippet = (props: Props) => {
const num = props.value === '1' ? '' : props.value;
const url = link('token_instance_item', { hash: props.hash, id: props.tokenId });
return (
<Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
<Text fontWeight={ 500 } as="span">For { num } token ID:</Text>
<Box display="inline-flex" alignItems="center">
<Icon as={ nftIcon } boxSize={ 6 } mr={ 1 }/>
<Link href={ url } fontWeight={ 600 }>{ props.tokenId }</Link>
</Box>
<TokenSnippet symbol={ props.symbol } hash={ props.hash } name="Foo"/>
</Flex>
);
};
export default React.memo(NftTokenTransferSnippet);
import { Center, Icon, Text } from '@chakra-ui/react';
import { Flex, Icon, Text } from '@chakra-ui/react';
import React from 'react';
import rightArrowIcon from 'icons/arrows/right.svg';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink';
import Token from 'ui/shared/Token';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet';
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer;
const TokenTransfer = (props: Props) => {
const isColumnLayout = props.token_type === 'ERC-1155' && Array.isArray(props.total);
const tokenSnippet = <TokenSnippet symbol={ props.token_symbol } hash={ props.token_address } name="Foo" ml={ 3 }/>;
const content = (() => {
switch (props.token_type) {
case 'ERC-20':
return (
<Flex>
<Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ props.total.value } unit="ether" exchangeRate={ props.exchange_rate } fontWeight={ 600 }/>
</Text>
{ tokenSnippet }
</Flex>
);
case 'ERC-721': {
return (
<NftTokenTransferSnippet
tokenId={ props.total.token_id }
value="1"
hash={ props.token_address }
symbol={ props.token_symbol }
/>
);
}
interface Props {
from: string;
to: string;
token: string;
amount: number;
usd: number;
}
case 'ERC-1155': {
const items = Array.isArray(props.total) ? props.total : [ props.total ];
return items.map((item) => (
<NftTokenTransferSnippet
key={ item.token_id }
tokenId={ item.token_id }
value={ item.value }
hash={ props.token_address }
symbol={ props.token_symbol }
/>
));
}
}
})();
const TokenTransfer = ({ from, to, amount, usd, token }: Props) => {
return (
<Center>
<AddressLink fontWeight="500" hash={ from } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink fontWeight="500" hash={ to } truncation="constant"/>
<Text fontWeight={ 500 } as="span" ml={ 4 }>For:{ space }
<Text fontWeight={ 600 } as="span">{ amount }</Text>{ space }
<Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text>
</Text>
<Token symbol={ token } ml={ 3 }/>
</Center>
<Flex
alignItems={ isColumnLayout ? 'flex-start' : 'center' }
flexWrap="wrap"
columnGap={ 3 }
rowGap={ 3 }
flexDir={ isColumnLayout ? 'column' : 'row' }
>
<Flex alignItems="center">
<AddressLink fontWeight="500" hash={ props.from.hash } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink fontWeight="500" hash={ props.to.hash } truncation="constant"/>
</Flex>
<Flex flexDir="column" rowGap={ 5 }>
{ content }
</Flex>
</Flex>
);
};
export default TokenTransfer;
export default React.memo(TokenTransfer);
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import TokenTransfer from 'ui/tx/TokenTransfer';
interface Props {
items: Array<TTokenTransfer>;
}
function getItemsNum(items: Array<TTokenTransfer>) {
const nonErc1155items = items.filter((item) => item.token_type !== 'ERC-1155').length;
const erc1155items = items
.filter((item) => item.token_type === 'ERC-1155')
.map((item) => {
if (Array.isArray(item.total)) {
return item.total.length;
}
return 1;
})
.reduce((sum, item) => sum + item, 0);
return nonErc1155items + erc1155items;
}
const TokenTransferList = ({ items }: Props) => {
const itemsNum = getItemsNum(items);
const hasScroll = itemsNum > 5;
const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600');
const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900');
return (
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
_after={ hasScroll ? {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: '20px',
height: '48px',
bgGradient: `linear(to-b, ${ gradientStartColor } 37.5%, ${ gradientEndColor } 77.5%)`,
} : undefined }
maxH={ hasScroll ? '200px' : 'auto' }
overflowY={ hasScroll ? 'scroll' : 'auto' }
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? 10 : 0 }
>
{ items.map((item, index) => <TokenTransfer key={ index } { ...item }/>) }
</Flex>
);
};
export default React.memo(TokenTransferList);
import { Flex, Text, Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { DecodedInput } from 'types/api/decodedInput';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -10,12 +12,13 @@ interface RowProps {
isLast?: boolean;
name: string;
type: string;
indexed?: boolean;
}
const PADDING = 4;
const GAP = 5;
const TableRow = ({ isLast, name, type, children }: RowProps) => {
const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
......@@ -38,6 +41,16 @@ const TableRow = ({ isLast, name, type, children }: RowProps) => {
>
{ type }
</GridItem>
{ indexed !== undefined && (
<GridItem
pr={ GAP }
pt={ GAP }
pb={ isLast ? PADDING : 0 }
bgColor={ bgColor }
>
{ indexed ? 'true' : 'false' }
</GridItem>
) }
<GridItem
pr={ PADDING }
pt={ GAP }
......@@ -51,82 +64,113 @@ const TableRow = ({ isLast, name, type, children }: RowProps) => {
);
};
const TxDecodedInputData = () => {
interface Props {
data: DecodedInput;
}
const TxDecodedInputData = ({ data }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hasIndexed = data.parameters.some(({ indexed }) => indexed !== undefined);
const gridTemplateColumns = hasIndexed ?
'minmax(80px, auto) minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)' :
'minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)';
const colNumber = hasIndexed ? 4 : 3;
return (
<Grid gridTemplateColumns="minmax(80px, auto) minmax(80px, auto) 1fr" fontSize="sm" lineHeight={ 5 } w="100%">
<Grid gridTemplateColumns={ gridTemplateColumns } fontSize="sm" lineHeight={ 5 } w="100%">
{ /* FIRST PART OF BLOCK */ }
<GridItem fontWeight={ 600 } pl={ PADDING } pr={ GAP }>Method Id</GridItem>
<GridItem colSpan={ 2 } pr={ PADDING }>0xddf252ad</GridItem>
<GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: colNumber, lg: undefined }}>
Method Id
</GridItem>
<GridItem colSpan={{ base: colNumber, lg: colNumber - 1 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>
{ data.method_id }
</GridItem>
<GridItem
py={ 2 }
mt={ 2 }
pl={ PADDING }
pr={ GAP }
pl={{ base: 0, lg: PADDING }}
pr={{ base: 0, lg: GAP }}
fontWeight={ 600 }
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px"
colSpan={{ base: colNumber, lg: undefined }}
>
Call
</GridItem>
<GridItem
py={ 2 }
mt={ 2 }
colSpan={ 2 }
pr={ PADDING }
py={{ base: 0, lg: 2 }}
mt={{ base: 0, lg: 2 }}
mb={{ base: 2, lg: 0 }}
colSpan={{ base: colNumber, lg: colNumber - 1 }}
pr={{ base: 0, lg: PADDING }}
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px"
borderTopWidth={{ base: '0px', lg: '1px' }}
whiteSpace="normal"
>
Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
{ data.method_call }
</GridItem>
{ /* TABLE INSIDE OF BLOCK */ }
<GridItem
pl={ PADDING }
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
{ data.parameters.length > 0 && (
<>
<GridItem
pl={ PADDING }
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Name
</GridItem>
<GridItem
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
</GridItem>
<GridItem
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Type
</GridItem>
<GridItem
pr={ PADDING }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
</GridItem>
{ hasIndexed && (
<GridItem
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Inde<wbr/>xed?
</GridItem>
) }
<GridItem
pr={ PADDING }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Data
</GridItem>
<TableRow name="from" type="address">
<Address justifyContent="space-between">
<AddressLink hash="0x0000000000000000000000000000000000000000"/>
<CopyToClipboard text="0x0000000000000000000000000000000000000000"/>
</Address>
</TableRow>
<TableRow name="from" type="address">
<Address justifyContent="space-between">
<AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
<CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
</Address>
</TableRow>
<TableRow name="tokenId" type="uint256" isLast>
<Flex alignItems="center" justifyContent="space-between">
<Text>116842</Text>
<CopyToClipboard text="116842"/>
</Flex>
</TableRow>
</GridItem>
</>
) }
{ data.parameters.map(({ name, type, value, indexed }, index) => {
return (
<TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }>
{ type === 'address' ? (
<Address justifyContent="space-between">
<AddressLink hash={ value }/>
<CopyToClipboard text={ value }/>
</Address>
) : (
<Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
<Text>{ String(value) }</Text>
<CopyToClipboard text={ value }/>
</Flex>
) }
</TableRow>
);
}) }
</Grid>
);
};
......
import { Grid, GridItem, Text, Box, Icon, Link, Tag, Flex } from '@chakra-ui/react';
import { Grid, GridItem, Text, Box, Icon, Link, Spinner } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import { tx } from 'data/tx';
import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import successIcon from 'icons/status/success.svg';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
// import errorIcon from 'icons/status/error.svg';
// import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import CurrencyValue from 'ui/shared/CurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
// import PrevNext from 'ui/shared/PrevNext';
import RawInputData from 'ui/shared/RawInputData';
import Token from 'ui/shared/Token';
import TextSeparator from 'ui/shared/TextSeparator';
// import TokenSnippet from 'ui/shared/TokenSnippet';
import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization';
import TokenTransfer from 'ui/tx/TokenTransfer';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TokenTransferList from 'ui/tx/TokenTransferList';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import type { Props as TxStatusProps } from 'ui/tx/TxStatus';
import TxStatus from 'ui/tx/TxStatus';
const TOKEN_TRANSFERS = [
{ title: 'Tokens Transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens Minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens Burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens Created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
];
const TxDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
const fetch = useFetch();
const leftSeparatorStyles = {
ml: 3,
pl: 3,
borderLeftWidth: '1px',
borderLeftColor: 'gray.700',
};
const { data, isLoading, isError } = useQuery<unknown, unknown, Transaction>(
[ 'tx', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const [ isExpanded, setIsExpanded ] = React.useState(false);
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
......@@ -38,130 +64,198 @@ const TxDetails = () => {
});
}, []);
if (isLoading) {
return <TxDetailsSkeleton/>;
}
if (isError) {
return <DataFetchAlert/>;
}
return (
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns="auto 1fr">
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
<DetailsInfoItem
title="Transaction hash"
hint="Unique character string (TxID) assigned to every verified transaction."
flexWrap="nowrap"
>
{ tx.hash }
<CopyToClipboard text={ tx.hash }/>
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
<Box overflow="hidden">
<HashStringShortenDynamic hash={ data.hash }/>
</Box>
<CopyToClipboard text={ data.hash }/>
{ /* api doesn't support navigation between certain address account tx */ }
{ /* <PrevNext ml={ 7 }/> */ }
</DetailsInfoItem>
<DetailsInfoItem
title="Status"
hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
>
<TxStatus status={ tx.status as TxStatusProps['status'] }/>
<TxStatus status={ data.status } errorText={ data.status === 'error' ? data.result : undefined }/>
</DetailsInfoItem>
{ data.revert_reason && (
<DetailsInfoItem
title="Revert reason"
hint="The revert reason of the transaction."
>
<TxRevertReason { ...data.revert_reason }/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Block"
hint="Block number containing the transaction."
>
<Text>{ tx.block_num }</Text>
<Text { ...leftSeparatorStyles } borderLeftColor="gray.500" variant="secondary">
{ tx.confirmation_num } Block confirmations
</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Timestamp"
hint="Date & time of transaction inclusion, including length of time for confirmation."
>
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ dayjs(tx.timestamp).fromNow() }</Text>
<Text { ...leftSeparatorStyles }>{ dayjs(tx.timestamp).format('LLLL') }</Text>
<Text { ...leftSeparatorStyles } borderLeftColor="gray.500" variant="secondary">
Confirmed within { tx.confirmation_duration } secs
</Text>
<Text>{ data.block === null ? 'Pending' : data.block }</Text>
{ Boolean(data.confirmations) && (
<>
<TextSeparator color="gray.500"/>
<Text variant="secondary">
{ data.confirmations } Block confirmations
</Text>
</>
) }
</DetailsInfoItem>
{ data.timestamp && (
<DetailsInfoItem
title="Timestamp"
hint="Date & time of transaction inclusion, including length of time for confirmation."
>
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text>
<TextSeparator/>
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }<TextSeparator color="gray.500"/></Text>
<Text variant="secondary">{ getConfirmationDuration(data.confirmation_duration) }</Text>
</DetailsInfoItem>
) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem
title="From"
hint="Address (external or contract) sending the transaction."
mt={ 8 }
>
<Address>
<AddressIcon hash={ tx.address_from }/>
<AddressLink ml={ 2 } hash={ tx.address_from }/>
<CopyToClipboard text={ tx.address_from }/>
<AddressIcon hash={ data.from.hash }/>
<AddressLink ml={ 2 } hash={ data.from.hash } alias={ data.from.name }/>
<CopyToClipboard text={ data.from.hash }/>
</Address>
</DetailsInfoItem>
<DetailsInfoItem
title="Interacted with contract"
title={ data.to.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction."
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
>
<Address>
<AddressIcon hash={ tx.address_to }/>
<AddressLink ml={ 2 } hash={ tx.address_to }/>
<CopyToClipboard text={ tx.address_to }/>
<Address mr={ 3 }>
<AddressIcon hash={ data.to.hash }/>
<AddressLink ml={ 2 } hash={ data.to.hash } alias={ data.to.name }/>
<CopyToClipboard text={ data.to.hash }/>
</Address>
<Tag colorScheme="orange" variant="solid" ml={ 3 }>SANA</Tag>
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500"/>
<Token symbol="USDT" ml={ 3 }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Token transferred"
hint="List of token transferred in the transaction."
>
<Flex flexDirection="column" alignItems="flex-start" rowGap={ 5 }>
{ tx.transferred_tokens.map((item) => <TokenTransfer key={ item.token } { ...item }/>) }
</Flex>
{ /* todo_tom Nikita should add to api later */ }
{ /* <Tag colorScheme="orange" variant="solid" flexShrink={ 0 }>SANA</Tag> */ }
{ /* <Tooltip label="Contract execution completed">
<chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500" cursor="pointer"/>
</chakra.span>
</Tooltip> */ }
{ /* <Tooltip label="Error occured during contract execution">
<chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } ml={ 2 } color="red.500" cursor="pointer"/>
</chakra.span>
</Tooltip> */ }
{ /* <TokenSnippet symbol="UP" name="User Pay" hash="0xA17ed5dFc62D0a3E74D69a0503AE9FdA65d9f212" ml={ 3 }/> */ }
</DetailsInfoItem>
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => {
const items = data.token_transfers?.filter((token) => token.type === type) || [];
if (items.length === 0) {
return null;
}
return (
<DetailsInfoItem
key={ type }
title={ title }
hint={ hint }
position="relative"
>
<TokenTransferList items={ items }/>
</DetailsInfoItem>
);
}) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem
title="Value"
hint="Value sent in the native token (and USD) if applicable."
mt={ 8 }
>
<Text>{ tx.amount.value } Ether</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.amount.value_usd.toFixed(2) })</Text>
<CurrencyValue value={ data.value } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transaction fee"
hint="Total transaction fee."
>
<Text>{ tx.fee.value } Ether</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
<CurrencyValue
value={ data.fee.value }
currency={ appConfig.network.currency }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
>
<Text>{ tx.gas_price.toLocaleString('en', { minimumFractionDigits: 18 }) } Ether</Text>
<Text variant="secondary" ml={ 1 }>({ (tx.gas_price * Math.pow(10, 18)).toFixed(0) } Gwei)</Text>
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text>
<Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas limit & usage by txn"
hint="Actual gas amount used by the transaction."
>
<Text>{ tx.gas_used.toLocaleString('en') }</Text>
<Text { ...leftSeparatorStyles }>{ tx.gas_limit.toLocaleString('en') }</Text>
<Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas fees (Gwei)"
// eslint-disable-next-line max-len
hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
>
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text>
</Box>
<Box { ...leftSeparatorStyles }>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text>
</Box>
<Box { ...leftSeparatorStyles }>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text>
</Box>
</DetailsInfoItem>
<DetailsInfoItem
title="Burnt fees"
hint="Amount of ETH burned for this transaction. Equals Block Base Fee per Gas * Gas Used."
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ tx.burnt_fees.value.toLocaleString('en', { minimumFractionDigits: 18 }) } Ether</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.burnt_fees.value_usd.toFixed(2) })</Text>
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<TextSeparator/>
<Text >{ BigNumber(data.gas_limit).toFormat() }</Text>
<Utilization ml={ 4 } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
</DetailsInfoItem>
<GridItem colSpan={ 2 }>
{ (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && (
<DetailsInfoItem
title="Gas fees (Gwei)"
// eslint-disable-next-line max-len
hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
>
{ data.base_fee_per_gas && (
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
{ (data.max_fee_per_gas || data.max_priority_fee_per_gas) && <TextSeparator/> }
</Box>
) }
{ data.max_fee_per_gas && (
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.max_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
{ data.max_priority_fee_per_gas && <TextSeparator/> }
</Box>
) }
{ data.max_priority_fee_per_gas && (
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.max_priority_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
</Box>
) }
</DetailsInfoItem>
) }
{ data.tx_burnt_fee && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
>
<Icon as={ flameIcon } mr={ 1 } boxSize={ 5 } color="gray.500"/>
<CurrencyValue
value={ String(data.tx_burnt_fee) }
currency={ appConfig.network.currency }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
</DetailsInfoItem>
) }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="TxDetails__cutLink">
<Link
mt={ 6 }
......@@ -177,37 +271,54 @@ const TxDetails = () => {
</GridItem>
{ isExpanded && (
<>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
<DetailsInfoItem
mt={ 4 }
title="Other"
hint="Other data related to this transaction."
>
<Box>
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text>
</Box>
<Box { ...leftSeparatorStyles }>
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ tx.nonce }</Text>
</Box>
<Box { ...leftSeparatorStyles }>
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
{
[
typeof data.type === 'number' && (
<Box key="type">
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ data.type }</Text>
{ data.type === 2 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-1559)</Text> }
</Box>
),
<Box key="nonce">
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ data.nonce }</Text>
</Box>,
data.position !== null && (
<Box key="position">
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ data.position }</Text>
</Box>
),
]
.filter(Boolean)
.map((item, index) => (
<>
{ index !== 0 && <TextSeparator/> }
{ item }
</>
))
}
</DetailsInfoItem>
<DetailsInfoItem
title="Raw input"
hint="Binary data included with the transaction. See logs tab for additional info."
>
<RawInputData hex={ tx.input_hex }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Decoded input data"
hint="hmmmmmmmmmmm"
>
<TxDecodedInputData/>
<RawInputData hex={ data.raw_input }/>
</DetailsInfoItem>
{ data.decoded_input && (
<DetailsInfoItem
title="Decoded input data"
hint="Decoded input data"
>
<TxDecodedInputData data={ data.decoded_input }/>
</DetailsInfoItem>
) }
</>
) }
</Grid>
......
import { Box, Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import { Box, Flex, Alert, Show } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { data } from 'data/txInternal';
import Filters from 'ui/shared/Filters';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
import type { Sort, SortField } from 'ui/tx/internals/utils';
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
value: [ 'value-desc', 'value-asc', undefined ],
'gas-limit': [ 'gas-limit-desc', 'gas-limit-asc', undefined ],
};
const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) => {
const sequence = SORT_SEQUENCE[field];
const curIndex = sequence.findIndex((sort) => sort === prevValue);
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
return sequence[nextIndex];
};
const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => {
switch (sort) {
case 'value-desc': {
const result = a.value > b.value ? -1 : 1;
return a.value === b.value ? 0 : result;
}
case 'value-asc': {
const result = a.value > b.value ? 1 : -1;
return a.value === b.value ? 0 : result;
}
// no gas limit in api yet
// case 'gas-limit-desc': {
// const result = a.gasLimit > b.gasLimit ? -1 : 1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
// case 'gas-limit-asc': {
// const result = a.gasLimit > b.gasLimit ? 1 : -1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
default:
return 0;
}
};
const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
item.to.hash.toLowerCase().includes(formattedSearchTerm);
};
const TxInternals = () => {
const router = useRouter();
const fetch = useFetch();
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ 'tx-internals', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/internal-transactions`),
{
enabled: Boolean(router.query.id),
},
);
const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue);
}, []);
const handleSortToggle = React.useCallback((field: SortField) => {
return () => {
setSort(getNextSortValue(field));
};
}, []);
if (isLoading) {
return (
<>
<Show below="lg"><TxInternalsSkeletonMobile/></Show>
<Show above="lg"><TxInternalsSkeletonDesktop/></Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Alert>There are no internal transactions for this transaction.</Alert>;
}
const content = (() => {
const filteredData = data.items
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
.filter(searchFn(searchTerm))
.sort(sortFn(sort));
if (filteredData.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
}
return isMobile ?
<TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle }/>;
})();
return (
<Box>
<Filters/>
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="20%">Type</Th>
<Th width="calc(20% + 40px)" pr="0">From</Th>
<Th width="calc(20% - 40px)" pl="0">To</Th>
<Th width="20%" isNumeric>Value</Th>
<Th width="20%" isNumeric>Gas limit</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem
key={ item.id }
{ ...item }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Flex mb={ 6 }>
<TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
</Flex>
{ content }
</Box>
);
};
......
import { Box } from '@chakra-ui/react';
import { Box, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { data } from 'data/txLogs';
import type { LogsResponse } from 'types/api/log';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
const TxLogs = () => {
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ 'tx-log', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/logs`),
{
enabled: Boolean(router.query.id),
},
);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<Box>
<TxLogSkeleton/>
<TxLogSkeleton/>
</Box>
);
}
if (data.items.length === 0) {
return <Alert>There are no logs for this transaction.</Alert>;
}
return (
<Box>
{ data.map((item, index) => <TxLogItem key={ index } { ...item } index={ index }/>) }
{ data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) }
</Box>
);
};
......
import { Flex, Textarea } from '@chakra-ui/react';
import { Flex, Textarea, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace';
import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
const data = [
{
action: {
callType: 'delegatecall',
from: '0x296033cb983747b68911244ec1a3f01d7708851b',
gas: '0x1AB35C9',
// eslint-disable-next-line max-len
input: '0x6a76120200000000000000000000000099759357a9923bb164a7ae8b85703a6882cb84ea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000014466d2a64d000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000013ef0000000000000000000000000000000000000000000000000000000000001bfb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000186b900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000f4e5b62da2eee3b5811dae1fae480f7623bd4cd000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000',
to: '0x3e5c63644e683549055b9be8653de26e0b4cd36e',
value: '0x0',
},
result: {
blockHash: '0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229',
blockNumber: '0xa4f285',
from: '0xea8a7ef30f894bce23b42314613458d13f9d43ea',
gas: '0x30d40',
gasPrice: '0x2e90edd000',
hash: '0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511',
input: '0x',
nonce: '0x1e7',
to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
transactionIndex: '0x0',
value: '0xde0b6b3a7640000',
v: '0x25',
r: '0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f',
s: '0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995',
},
},
{
action: {
callType: 'delegatecall',
from: '0x296033cb983747b68911244ec1a3f01d7708851b',
gas: '0x1AB35C9',
// eslint-disable-next-line max-len
input: '0x6a76120200000000000000000000000099759357a9923bb164a7ae8b85703a6882cb84ea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000014466d2a64d000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000013ef0000000000000000000000000000000000000000000000000000000000001bfb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000186b900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000f4e5b62da2eee3b5811dae1fae480f7623bd4cd000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000',
to: '0x3e5c63644e683549055b9be8653de26e0b4cd36e',
value: '0x0',
},
result: {
blockHash: '0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229',
blockNumber: '0xa4f285',
from: '0xea8a7ef30f894bce23b42314613458d13f9d43ea',
gas: '0x30d40',
gasPrice: '0x2e90edd000',
hash: '0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511',
input: '0x',
nonce: '0x1e7',
to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
transactionIndex: '0x0',
value: '0xde0b6b3a7640000',
v: '0x25',
r: '0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f',
s: '0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995',
const TxRawTrace = () => {
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
[ 'tx-raw-trace', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/raw-trace`),
{
enabled: Boolean(router.query.id),
},
},
];
);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Flex justifyContent="end" mb={ 2 }>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="500px"/>
</>
);
}
const TxRawTrace = () => {
const text = JSON.stringify(data, undefined, 4);
return (
<>
<Flex justifyContent="end" mb={ 2 }>
......@@ -69,7 +45,7 @@ const TxRawTrace = () => {
</Flex>
<Textarea
variant="filledInactive"
height="570px"
minHeight="500px"
p={ 4 }
value={ text }
/>
......
import {
Accordion,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import { Accordion, Text } from '@chakra-ui/react';
import React from 'react';
import { data } from 'data/txState';
import TxStateTableItem from './state/TxStateTableItem';
const CURRENCY = 'ETH';
import useIsMobile from 'lib/hooks/useIsMobile';
import TxStateList from 'ui/tx/state/TxStateList';
import TxStateTable from 'ui/tx/state/TxStateTable';
const TxState = () => {
const isMobile = useIsMobile();
const list = isMobile ? <TxStateList/> : <TxStateTable/>;
return (
<>
<Text>
A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes
</Text>
<Accordion allowToggle allowMultiple>
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ CURRENCY }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
<Accordion allowMultiple defaultIndex={ [] }>
{ list }
</Accordion>
</>
);
......
import { Tag, TagLabel, TagLeftIcon } from '@chakra-ui/react';
import React from 'react';
import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: 'success' | 'error';
}
const TxStatus = ({ status }: Props) => {
const label = status === 'success' ? 'Success' : 'Error';
const icon = status === 'success' ? successIcon : errorIcon;
const colorScheme = status === 'success' ? 'green' : 'red';
return (
<Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
</Tag>
);
};
export default TxStatus;
import { Grid, GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const SkeletonRow = ({ w = '100%' }: { w?: string }) => (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
</GridItem>
</>
);
const TxDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<SkeletonRow/>
<SkeletonRow w="20%"/>
<SkeletonRow w="50%"/>
<SkeletonRow/>
<SkeletonRow w="70%"/>
<SkeletonRow w="70%"/>
{ sectionGap }
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default TxDetailsSkeleton;
import { Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TransactionRevertReason } from 'types/api/transaction';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
type Props = TransactionRevertReason;
const TxRevertReason = (props: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
if ('raw' in props) {
return (
<Grid
bgColor={ bgColor }
p={ 4 }
fontSize="sm"
borderRadius="md"
templateColumns="auto minmax(0, 1fr)"
rowGap={ 2 }
columnGap={ 4 }
whiteSpace="normal"
>
<GridItem fontWeight={ 500 }>Raw:</GridItem>
<GridItem>{ props.raw }</GridItem>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ props.decoded }</GridItem>
</Grid>
);
}
return <TxDecodedInputData data={ props }/>;
};
export default React.memo(TxRevertReason);
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { TxInternalsType } from 'types/api/internalTransaction';
import FilterButton from 'ui/shared/FilterButton';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
interface Props {
appliedFiltersNum?: number;
defaultFilters: Array<TxInternalsType>;
onFilterChange: (nextValue: Array<TxInternalsType>) => void;
}
const TxInternalsFilter = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w={{ md: '100%', lg: '438px' }}>
<PopoverBody px={ 4 } py={ 6 } display="grid" gridTemplateColumns="1fr 1fr" rowGap={ 5 }>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
{ TX_INTERNALS_ITEMS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TxInternalsFilter);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
return (
<Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) }
</Box>
);
};
export default TxInternalsList;
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex>
<Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag>
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
</Address>
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
{ /* no gas limit in api yet */ }
{ /* <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text>
</HStack> */ }
</AccountListItemMobile>
);
};
export default TxInternalsListItem;
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '28%', '24px', '28%', '16%' ] }/>
</>
);
};
export default TxInternalsSkeletonDesktop;
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
</Flex>
)) }
</Box>
</>
);
};
export default TxInternalsSkeletonMobile;
import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/east.svg';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils';
interface Props {
data: Array<InternalTransaction>;
sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void;
}
const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th width="28%">Type</Th>
<Th width="28%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="28%">To</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { appConfig.network.currency }
</Link>
</Th>
{ /* no gas limit in api yet */ }
{ /* <Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit
</Link>
</Th> */ }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem key={ item.transaction_hash } { ...item }/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxInternalsTable;
import { Tr, Td, Tag, Icon } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import rightArrowIcon from 'icons/arrows/right.svg';
import type { InternalTransaction } from 'types/api/internalTransaction';
import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/tx/TxStatus';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction
interface Props {
type: string;
status: 'success' | 'error';
from: string;
to: string;
value: number;
gasLimit: number;
}
const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) => {
return (
<Tr alignItems="top">
<Td>
<Tag colorScheme="cyan" mr={ 2 }>{ capitalize(type) }</Tag>
<TxStatus status={ status }/>
{ typeTitle && (
<Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box>
) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Td>
<Td pr="0">
<Address>
<AddressIcon hash={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from }/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } flexShrink={ 0 } color="gray.500"/>
<Td>
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td pl="0">
<Address>
<AddressIcon hash={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to }/>
<Td px={ 0 }>
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td>
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
<Td isNumeric>
<Td isNumeric verticalAlign="middle">
{ value }
</Td>
<Td isNumeric>
{ /* no gas limit in api yet */ }
{ /* <Td isNumeric verticalAlign='middle'>
{ gasLimit.toLocaleString('en') }
</Td>
</Td> */ }
</Tr>
);
};
......
import type { TxInternalsType } from 'types/api/internalTransaction';
export type Sort = 'value-asc' | 'value-desc' | 'gas-limit-asc' | 'gas-limit-desc';
export type SortField = 'value' | 'gas-limit';
interface TxInternalsTypeItem {
title: string;
id: TxInternalsType;
}
export const TX_INTERNALS_ITEMS: Array<TxInternalsTypeItem> = [
{ title: 'Call', id: 'call' },
{ title: 'Delegate call', id: 'delegatecall' },
{ title: 'Static call', id: 'staticcall' },
{ title: 'Create', id: 'create' },
{ title: 'Create2', id: 'create2' },
{ title: 'Self-destruct', id: 'selfdestruct' },
{ title: 'Reward', id: 'reward' },
];
import { SearchIcon } from '@chakra-ui/icons';
import { Text, Grid, GridItem, Link, Tooltip, Button, useColorModeValue } from '@chakra-ui/react';
import { Text, Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link } from '@chakra-ui/react';
import React from 'react';
import type { Log } from 'types/api/log';
// import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxLogTopic from 'ui/tx/logs/TxLogTopic';
import DecodedInputData from 'ui/tx/TxDecodedInputData';
interface Props {
address: string;
topics: Array<{ hex: string }>;
data: string;
index: number;
}
type Props = Log;
const RowHeader = ({ children }: { children: React.ReactNode }) => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }}>
<Text fontWeight={ 500 }>{ children }</Text>
</GridItem>
);
const RowHeader = ({ children }: { children: React.ReactNode }) => <GridItem><Text fontWeight={ 500 }>{ children }</Text></GridItem>;
const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
const TxLogItem = ({ address, index, topics, data }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Grid gridTemplateColumns="200px 1fr" gap={ 8 } py={ 8 } _notFirst={{ borderTopWidth: '1px', borderTopColor: borderColor }}>
<Grid
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }}
gap={{ base: 2, lg: 8 }}
py={ 8 }
_notFirst={{
borderTopWidth: '1px',
borderTopColor: borderColor,
}}
_first={{
pt: 0,
}}
>
{ !decoded && (
<GridItem colSpan={{ base: 1, lg: 2 }}>
<Alert status="warning" display="inline-table" whiteSpace="normal">
To see accurate decoded input data, the contract must be verified.{ space }
<Link href={ link('address_contract_verification', { id: address.hash }) }>Verify the contract here</Link>
</Alert>
</GridItem>
) }
<RowHeader>Address</RowHeader>
<GridItem display="flex" alignItems="center">
<Address>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } ml={ 2 }/>
<Address mr={{ base: 9, lg: 0 }}>
<AddressIcon hash={ address.hash }/>
<AddressLink hash={ address.hash } alias={ address.name } ml={ 2 }/>
</Address>
<Tooltip label="Find matches topic">
<Link ml={ 2 }>
<SearchIcon w={ 5 } h={ 5 }/>
{ /* api doesn't have find topic feature yet */ }
{ /* <Tooltip label="Find matches topic">
<Link ml={ 2 } mr={{ base: 9, lg: 0 }} display="inline-flex">
<Icon as={ searchIcon } boxSize={ 5 }/>
</Link>
</Tooltip>
</Tooltip> */ }
<Tooltip label="Log index">
<Button variant="outline" isActive ml="auto" size="sm" fontWeight={ 400 }>
<Button variant="outline" colorScheme="gray" isActive ml="auto" size="sm" fontWeight={ 400 }>
{ index }
</Button>
</Tooltip>
</GridItem>
<RowHeader>Decode input data</RowHeader>
<GridItem>
<DecodedInputData/>
</GridItem>
{ decoded && (
<>
<RowHeader>Decode input data</RowHeader>
<GridItem>
<DecodedInputData data={ decoded }/>
</GridItem>
</>
) }
<RowHeader>Topics</RowHeader>
<GridItem>
{ topics.map((item, index) => <TxLogTopic key={ index } { ...item } index={ index }/>) }
{ topics.filter(Boolean).map((item, index) => (
<TxLogTopic
key={ index }
hex={ item }
index={ index }
/>
)) }
</GridItem>
<RowHeader>Data</RowHeader>
<GridItem p={ 4 } fontSize="sm" borderRadius="md" bgColor={ dataBgColor }>
......
import { Flex, Grid, GridItem, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const RowHeader = () => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }} _first={{ alignSelf: 'center' }}>
<Skeleton h={ 6 } borderRadius="full" w="150px"/>
</GridItem>
);
const TopicRow = () => (
<Flex columnGap={ 3 }>
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } w="70px" borderRadius="full"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full"/>
</Flex>
);
const TxLogSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Grid
gap={{ base: 2, lg: 8 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
py={ 8 }
_notFirst={{
borderTopWidth: '1px',
borderTopColor: borderColor,
}}
_first={{
pt: 0,
}}
>
<RowHeader/>
<GridItem display="flex" alignItems="center">
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full" ml={ 2 } mr={ 9 }/>
<Skeleton h={ 8 } w={ 8 } borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="150px" w="100%" borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem display="flex" flexDir="column" rowGap={ 3 }>
<TopicRow/>
<TopicRow/>
<TopicRow/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="60px" w="100%" borderRadius="base"/>
</GridItem>
</Grid>
);
};
export default TxLogSkeleton;
import { Flex, Button, Text, Select } from '@chakra-ui/react';
import { Flex, Button, Select, Box } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react';
import hexToAddress from 'lib/hexToAddress';
import hexToUtf8 from 'lib/hexToUtf8';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
hex: string;
index: number;
}
type DataType = 'Hex' | 'Dec'
const OPTIONS: Array<DataType> = [ 'Hex', 'Dec' ];
type DataType = 'hex' | 'text' | 'address' | 'number';
const VALUE_CONVERTERS: Record<DataType, (hex: string) => string> = {
hex: (hex) => hex,
text: hexToUtf8,
address: hexToAddress,
number: (hex) => BigInt(hex).toString(),
};
const OPTIONS: Array<DataType> = [ 'hex', 'address', 'text', 'number' ];
const TxLogTopic = ({ hex, index }: Props) => {
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('Hex');
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('hex');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedDataType(event.target.value as DataType);
}, []);
const value = VALUE_CONVERTERS[selectedDataType.toLowerCase() as Lowercase<DataType>](hex);
const content = (() => {
switch (selectedDataType) {
case 'hex':
case 'number':
case 'text': {
return (
<>
<Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ value }/>
</Box>
<CopyToClipboard text={ value }/>
</>
);
}
case 'address': {
return (
<Address>
<AddressLink hash={ value }/>
<CopyToClipboard text={ value }/>
</Address>
);
}
}
})();
return (
<Flex alignItems="center" px={ 3 } _notFirst={{ mt: 3 }}>
<Button variant="outline" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }>
<Flex alignItems="center" px={{ base: 0, lg: 3 }} _notFirst={{ mt: 3 }} overflow="hidden" maxW="100%">
<Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }>
{ index }
</Button>
{ /* temporary condition juse to show different states of the component */ }
{ /* delete when ther will be real data */ }
{ index > 0 && (
<Select size="sm" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } focusBorderColor="none" w="75px" mr={ 3 }>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
{ index !== 0 && (
<Select
size="sm"
borderRadius="base"
value={ selectedDataType }
onChange={ handleSelectChange }
focusBorderColor="none"
mr={ 3 }
flexShrink={ 0 }
w="auto"
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ capitalize(option) }</option>) }
</Select>
) }
<Text>{ hex }</Text>
{ content }
</Flex>
);
};
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { data } from 'data/txState';
import TxStateListItem from 'ui/tx/state/TxStateListItem';
const TxStateList = () => {
return (
<Box mt={ 6 }>
{ data.map((item, index) => <TxStateListItem key={ index } { ...item }/>) }
</Box>
);
};
export default TxStateList;
import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
import type { data } from 'data/txState';
import { nbsp } from 'lib/html-entities';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStateStorageItem from './TxStateStorageItem';
type Props = ArrayElement<typeof data>;
const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props) => {
const hasStorageData = Boolean(storage?.length);
return (
<AccountListItemMobile>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column">
{ ({ isExpanded }) => (
<>
<Flex mb={ 6 }>
<AccordionButton
_hover={{ background: 'unset' }}
padding="0"
mr={ 5 }
w="auto"
>
<Button
variant="outline"
borderWidth="1px"
// button can't be inside button (AccordionButton)
as="div"
isActive={ isExpanded }
size="sm"
fontWeight={ 400 }
isDisabled={ !hasStorageData }
colorScheme="gray"
// AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }}
>
{ storage?.length || '0' }
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
<Address flexGrow={ 1 }>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } ml={ 2 }/>
</Address>
</Flex>
{ hasStorageData && (
<AccordionPanel fontWeight={ 500 } p={ 0 }>
{ storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel>
) }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box>
<Text as="span">Miner </Text>
<Link>{ miner }</Link>
</Box>
<Box>
<Text as="span">Before { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ before.balance }</Text>
</Box>
{ typeof before.nonce !== 'undefined' && (
<Box>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ before.nonce }</Text>
</Box>
) }
<Box>
<Text as="span">After { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ after.balance }</Text>
</Box>
{ typeof after.nonce !== 'undefined' && (
<Box>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box>
) }
<Text>State difference { appConfig.network.currency }</Text>
<Stat>
{ diff }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Flex>
</>
) }
</AccordionItem>
</AccountListItemMobile>
);
};
export default TxStateListItem;
......@@ -2,11 +2,13 @@ import {
Grid,
GridItem,
Select,
Box,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type { TTxStateItemStorage } from 'data/txState';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => {
const gridData = [
......@@ -20,19 +22,27 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return (
<Grid
gridTemplateColumns="auto 1fr"
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}
columnGap={ 3 }
rowGap={ 4 }
px={ 6 }
py={ 4 }
background="blackAlpha.50"
rowGap={{ base: 2.5, lg: 4 }}
px={{ base: 3, lg: 6 }}
py={{ base: 3, lg: 4 }}
backgroundColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.100') }
borderRadius="12px"
mb={ 4 }
fontSize="sm"
>
{ gridData.map((item) => (
<>
<GridItem alignSelf="center" fontWeight={ 600 } textAlign="end">{ item.name }</GridItem>
<GridItem>
<React.Fragment key={ item.name }>
<GridItem
alignSelf="center"
fontWeight={ 600 }
textAlign={{ base: 'start', lg: 'end' }}
_notFirst={{ mt: { base: 0.5, lg: 0 } }}
>
{ item.name }
</GridItem>
<GridItem display="flex" flexDir="row" columnGap={ 3 } alignItems="center" >
{ item.select && (
<Select
size="sm"
......@@ -40,15 +50,17 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
focusBorderColor="none"
display="inline-block"
w="auto"
mr={ 3 }
flexShrink={ 0 }
background={ backgroundColor }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
{ item.value }
<Box fontWeight={ 500 } whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic fontWeight="500" hash={ item.value }/>
</Box>
</GridItem>
</>
</React.Fragment>
)) }
</Grid>
);
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import { data } from 'data/txState';
import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => {
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm" w="auto">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxStateTable;
import { Box, Heading, Text, Flex, Link, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => {
const sectionBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const sectionProps = {
borderBottom: '1px solid',
borderColor: sectionBorderColor,
paddingBottom: 4,
};
const sectionTitleProps = {
color: 'gray.500',
fontWeight: 600,
marginBottom: 3,
fontSize: 'sm',
};
return (
<>
<Heading as="h4" fontSize="18px" mb={ 6 }>Additional info </Heading>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
<CurrencyValue
value={ tx.fee.value }
currency={ appConfig.network.currency }
exchangeRate={ tx.exchange_rate }
accuracyUsd={ 2 }
/>
</Flex>
</Box>
{ tx.gas_used !== null && (
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas limit & usage by transaction</Text>
<Flex>
<Text>{ BigNumber(tx.gas_used).toFormat() }</Text>
<TextSeparator/>
<Text>{ BigNumber(tx.gas_limit).toFormat() }</Text>
<Utilization ml={ 4 } value={ Number(BigNumber(tx.gas_used).dividedBy(BigNumber(tx.gas_limit)).toFixed(2)) }/>
</Flex>
</Box>
) }
{ (tx.base_fee_per_gas !== null || tx.max_fee_per_gas !== null || tx.max_priority_fee_per_gas !== null) && (
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas fees (Gwei)</Text>
{ tx.base_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.base_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
{ tx.max_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.max_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
{ tx.max_priority_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.max_priority_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
</Box>
) }
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Others</Text>
<Box>
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type }</Text>
{ tx.type === 2 && <Text fontWeight="400" as="span" ml={ 1 } color="gray.500">(EIP-1559)</Text> }
</Box>
<Box>
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ tx.nonce }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
</Box>
<Link fontSize="sm" href={ link('tx', { id: tx.hash }) }>More details</Link>
</>
);
};
export default TxAdditionalInfo;
import {
Icon,
Center,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import infoIcon from 'icons/info.svg';
const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?: () => void}, ref: React.ForwardedRef<HTMLDivElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300');
return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="24px" h="24px" onClick={ onClick } cursor="pointer">
<Icon
as={ infoIcon }
boxSize={ 5 }
color={ infoColor }
_hover={{ color: 'blue.400' }}
/>
</Center>
);
};
export default React.forwardRef(TxAdditionalInfoButton);
import { Tag } from '@chakra-ui/react';
import React from 'react';
export interface Props {
type: 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
}
const TxStatus = ({ type }: Props) => {
let label;
let colorScheme;
switch (type) {
case 'contract-call':
label = 'Contract call';
colorScheme = 'blue';
break;
case 'transaction':
label = 'Transaction';
colorScheme = 'purple';
break;
case 'token-transfer':
label = 'Token transfer';
colorScheme = 'orange';
break;
case 'internal-tx':
label = 'Internal txn';
colorScheme = 'cyan';
break;
case 'multicall':
label = 'Multicall';
colorScheme = 'teal';
break;
}
return (
<Tag colorScheme={ colorScheme }>
{ label }
</Tag>
);
};
export default TxStatus;
import { Box, HStack, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns';
import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
showDescription?: boolean;
showSortButton?: boolean;
}
const TxsContent = ({ showSortButton = true, showDescription = true, txs }: Props) => {
const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
// sorting should be preserved with pagination!
const sort = useCallback((field: 'val' | 'fee') => () => {
if (field === 'val') {
setSorting((prevVal => {
if (prevVal === 'val-asc') {
return undefined;
}
if (prevVal === 'val-desc') {
return 'val-asc';
}
return 'val-desc';
}));
}
if (field === 'fee') {
setSorting((prevVal => {
if (prevVal === 'fee-asc') {
return undefined;
}
if (prevVal === 'fee-desc') {
return 'fee-asc';
}
return 'fee-desc';
}));
}
}, []);
useEffect(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value)));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value)));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value)));
break;
default:
setSortedTxs(txs);
}
}, [ sorting, txs ]);
return (
<>
{ showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
<HStack mb={ 6 }>
{ /* TODO */ }
<FilterButton
isActive={ false }
// eslint-disable-next-line react/jsx-no-bind
onClick={ () => {} }
appliedFiltersNum={ 0 }
/>
{ showSortButton && (
<SortButton
// eslint-disable-next-line react/jsx-no-bind
handleSort={ () => {} }
isSortActive={ Boolean(sorting) }
display={{ base: 'block', lg: 'none' }}
/>
) }
<FilterInput
// eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} }
maxW="360px"
size="xs"
placeholder="Search by addresses, hash, method..."
/>
</HStack>
<Show below="lg"><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Show above="lg"><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/>
</Box>
</>
);
};
export default TxsContent;
import {
HStack,
Box,
Flex,
Icon,
Link,
Modal,
ModalContent,
ModalCloseButton,
Text,
useColorModeValue,
useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: Transaction}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
</Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }>
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ tx.hash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address>
</Flex>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(tx.timestamp).fromNow() }</Text>
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
{ /* TODO: we don't recieve method from api */ }
<Text
as="span"
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ /* { tx.method } */ }
CommitHash
</Text>
</Flex>
{ tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
</Box>
) }
<Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.from.hash }/>
<AddressLink
hash={ tx.from.hash }
alias={ tx.from.name }
fontWeight="500"
ml={ 2 }
/>
</Address>
<Icon
as={ rightArrowIcon }
boxSize={ 6 }
mx={ 2 }
color="gray.500"
/>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.to.hash }/>
<AddressLink
hash={ tx.to.hash }
alias={ tx.to.name }
fontWeight="500"
ml={ 2 }
/>
</Address>
</Flex>
<Box mt={ 2 }>
<Text as="span">Value { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text>
</Box>
<Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text>
</Box>
</Box>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent paddingTop={ 4 }>
<ModalCloseButton/>
<TxAdditionalInfo tx={ tx }/>
</ModalContent>
</Modal>
</>
);
};
export default TxsListItem;
import { Show, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
const TxsValidated = () => {
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ 'transactions_pending' ], async() => fetch('/api/transactions/?filter=pending'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile isPending/></Show>
<Show above="lg"><TxsSkeletonDesktop isPending/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
return <TxsContent txs={ data.items } showDescription={ false }/>;
};
export default TxsValidated;
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = ({ isPending }: {isPending?: boolean}) => {
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
</>
);
};
export default TxsInternalsSkeletonDesktop;
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = ({ isPending }: {isPending?: boolean}) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
flexDirection="column"
paddingBottom={ 3 }
paddingTop={ 4 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
)) }
</Box>
</>
);
};
export default TxInternalsSkeletonMobile;
import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import TxsTableItem from './TxsTableItem';
type Props = {
txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void;
sorting: Sort;
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="810px" size="xs">
<Thead>
<Tr>
<Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th>
<Th width="11%">Block</Th>
<Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ appConfig.network.currency }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ appConfig.network.currency }` }
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxsTable;
import {
Box,
Tr,
Td,
Tag,
Link,
Icon,
VStack,
Text,
Tooltip,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Portal,
useColorModeValue,
Show,
} from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: Transaction}) => {
const addressFrom = (
<Address>
<Tooltip label={ tx.from.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.from.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 }/>
</Address>
);
const addressTo = (
<Address>
<Tooltip label={ tx.to.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 }/>
</Address>
);
const infoBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
return (
<Tr>
<Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 }>
{ ({ isOpen }) => (
<>
<PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger>
<Portal>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody>
<TxAdditionalInfo tx={ tx }/>
</PopoverBody>
</PopoverContent>
</Portal>
</>
) }
</Popover>
</Td>
<Td>
<VStack alignItems="start">
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td>
<VStack alignItems="start" lineHeight="24px">
<Address width="100%">
<AddressLink
hash={ tx.hash }
type="transaction"
fontWeight="700"
/>
</Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack>
</Td>
<Td>
{ /* TODO: we don't recieve method from api */ }
{ /* <TruncatedTextTooltip label={ tx.method }>
<Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
>
{ tx.method }
</Tag>
</TruncatedTextTooltip> */ }
<TruncatedTextTooltip label="CommitHash">
<Tag
colorScheme="gray"
>
CommitHash
</Tag>
</TruncatedTextTooltip>
</Td>
<Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td>
{ /* TODO: fix "show" problem */ }
<Show above="xl">
<Td>
{ addressFrom }
</Td>
<Td>
<Icon as={ rightArrowIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
</Td>
<Td>
{ addressTo }
</Td>
</Show>
<Show below="xl">
<Td colSpan={ 3 }>
<Box>
{ addressFrom }
<Icon
as={ rightArrowIcon }
boxSize={ 6 }
mt={ 2 }
mb={ 1 }
color="gray.500"
transform="rotate(90deg)"
/>
{ addressTo }
</Box>
</Td>
</Show>
<Td isNumeric>
<CurrencyValue value={ tx.value }/>
</Td>
<Td isNumeric>
<CurrencyValue value={ tx.fee.value } accuracy={ 8 }/>
</Td>
</Tr>
);
};
export default TxsTableItem;
import { Show, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
const TxsValidated = () => {
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ 'transactions_validated' ], async() => fetch('/api/transactions/?filter=validated'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile/></Show>
<Show above="lg"><TxsSkeletonDesktop/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
return <TxsContent txs={ data.items }/>;
};
export default TxsValidated;
......@@ -3,11 +3,12 @@ import React, { useCallback } from 'react';
import { Controller } from 'react-hook-form';
import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form';
import appConfig from 'configs/app/config';
import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ 'xDAI', 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
const NOTIFICATIONS_NAMES = [ appConfig.network.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
type Props<Inputs extends FieldValues> = {
control: Control<Inputs>;
......
import { HStack, VStack, Image, Text, Icon, useColorModeValue } from '@chakra-ui/react';
import { HStack, VStack, Text, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TWatchlistItem } from 'types/client/account';
import appConfig from 'configs/app/config';
import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg';
import { nbsp } from 'lib/html-entities';
import AddressSnippet from 'ui/shared/AddressSnippet';
import TokenLogo from 'ui/shared/TokenLogo';
// now this component works only for xDAI
// for other networks later we will use config or smth
const DECIMALS = 18;
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
......@@ -23,8 +23,9 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Image src="/xdai.png" srcSet="/xdai@2x.png 2x" alt="chain-logo" marginRight="10px" w="16px" h="16px"/>
<Text color={ mainTextColor }>{ `xDAI balance:${ nbsp }` + nativeBalance }</Text>
{ appConfig.network.nativeTokenAddress &&
<TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ appConfig.network.currency } balance:${ nbsp }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack>
{ item.tokens_count && (
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment