Commit 6760a274 authored by tom's avatar tom

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

parents 808184ca 4d8d344d
NEXT_PUBLIC_SUPPORTED_NETWORKS=APP_NEXT_NEXT_PUBLIC_SUPPORTED_NETWORKS # app config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=APP_NEXT_NEXT_PUBLIC_BLOCKSCOUT_VERSION NEXT_PUBLIC_APP_ENV=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_ENV__
NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK NEXT_PUBLIC_APP_INSTANCE=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_INSTANCE__
NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK NEXT_PUBLIC_APP_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PROTOCOL__
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=APP_NEXT_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN # network config
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__
NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__
NEXT_PUBLIC_NETWORK_SHORT_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_SHORT_NAME NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME__
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=APP_NEXT_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME NEXT_PUBLIC_NETWORK_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TYPE__
NEXT_PUBLIC_NETWORK_TYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_TYPE NEXT_PUBLIC_NETWORK_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_ID__
NEXT_PUBLIC_NETWORK_SUBTYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_SUBTYPE NEXT_PUBLIC_NETWORK_CURRENCY_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_NAME__
NEXT_PUBLIC_NETWORK_ID=APP_NEXT_NEXT_PUBLIC_NETWORK_ID NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL__
NEXT_PUBLIC_NETWORK_CURRENCY=APP_NEXT_NEXT_PUBLIC_NETWORK_CURRENCY NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS__
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=APP_NEXT_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS__
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=APP_NEXT_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED__
NEXT_PUBLIC_FEATURED_NETWORKS=APP_NEXT_NEXT_PUBLIC_FEATURED_NETWORKS NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE
NEXT_PUBLIC_APP_PROTOCOL=APP_NEXT_NEXT_PUBLIC_APP_PROTOCOL
NEXT_PUBLIC_APP_HOST=APP_NEXT_NEXT_PUBLIC_APP_HOST # ui config
NEXT_PUBLIC_APP_PORT=APP_NEXT_NEXT_PUBLIC_APP_PORT NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__
NEXT_PUBLIC_API_ENDPOINT=APP_NEXT_NEXT_PUBLIC_API_ENDPOINT NEXT_PUBLIC_FOOTER_GITHUB_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_GITHUB_LINK__
NEXT_PUBLIC_API_BASE_PATH=APP_NEXT_NEXT_PUBLIC_API_BASE_PATH NEXT_PUBLIC_FOOTER_TWITTER_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_TWITTER_LINK__
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK__
NEXT_PUBLIC_FOOTER_STAKING_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_STAKING_LINK__
NEXT_PUBLIC_FEATURED_NETWORKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FEATURED_NETWORKS__
NEXT_PUBLIC_NETWORK_EXPLORERS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_EXPLORERS__
NEXT_PUBLIC_MARKETPLACE_APP_LIST=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_APP_LIST__
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM__
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
# external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
...@@ -59,21 +59,16 @@ jobs: ...@@ -59,21 +59,16 @@ jobs:
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }} tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
SENTRY_DSN=${{ secrets.SENTRY_DSN }} GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_and_tests: deploy_and_tests:
needs: push_to_registry needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
with: with:
valuesDir: deploy/values/e2e
appName: e2e-front
appNamespace: e2e-front-$GITHUB_SHA_SHORT appNamespace: e2e-front-$GITHUB_SHA_SHORT
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }} frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth blockscoutIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT
scVerifierIngressHost: sc-verifier frontendIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT
gethIngressHost: e2e-geth-$GITHUB_SHA_SHORT
scVerifierIngressHost: e2e-sc-verifier-$GITHUB_SHA_SHORT
secrets: inherit secrets: inherit
name: Deploy review environment name: Deploy review environment
on: on:
# push:
pull_request: pull_request:
# push:
# branches-ignore:
# - 'main'
workflow_dispatch: workflow_dispatch:
env: env:
...@@ -66,10 +68,10 @@ jobs: ...@@ -66,10 +68,10 @@ jobs:
needs: push_to_registry needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with: with:
valuesDir: deploy/values/review env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout blockscoutIngressHost: blockscout
frontendIngressHost: frontend frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }} frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth gethIngressHost: geth
scVerifierIngressHost: sc-verifier scVerifierIngressHost: sc-verifier
......
...@@ -56,10 +56,10 @@ jobs: ...@@ -56,10 +56,10 @@ jobs:
needs: push_to_registry needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with: with:
valuesDir: deploy/values/review env_vars: VALUES_DIR=deploy/values/main,APP_NAME=bs-stack
appNamespace: front-main appNamespace: front-main
blockscoutIngressHost: blockscout blockscoutIngressHost: blockscout
frontendIngressHost: frontend frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:main frontendImage: ghcr.io/blockscout/frontend:main
gethIngressHost: geth gethIngressHost: geth
scVerifierIngressHost: sc-verifier scVerifierIngressHost: sc-verifier
......
[Design](https://www.figma.com/file/07zoJSAP7Vo655ertmlppA/My_Account?node-id=279%3A1006) | [API Doc](https://github.com/blockscout/blockscout-account/blob/account/apps/block_scout_web/API.md) | [Swagger](https://app.swaggerhub.com/apis/NIKITOSING4/blockscout-account-api/1.0) [Design](https://www.figma.com/file/07zoJSAP7Vo655ertmlppA/My_Account?node-id=279%3A1006) | [API Doc](https://github.com/blockscout/blockscout-account/blob/account/apps/block_scout_web/API.md) | [Core Swagger](https://app.swaggerhub.com/apis/NIKITOSING4/CoreBlockScoutAPI/1.0.0) | [Account Swagger](https://app.swaggerhub.com/apis/NIKITOSING4/blockscout-account-api/1.0)
----- -----
## Technology stack ## Technology stack
...@@ -24,7 +24,7 @@ For local development please follow next steps: ...@@ -24,7 +24,7 @@ For local development please follow next steps:
- clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secret values (see description [below](#environment-variables)) - 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 - 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 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` - 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 - navigate to the host from logs output
## Components visual testing ## Components visual testing
...@@ -44,28 +44,32 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -44,28 +44,32 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- | | --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` | | 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_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_TYPE | `string` *(optional)* | Network type (used for matching pre-defined assets, e.g network logo and icon, which are stored in the project). See all possible values here | `xdai_mainnet` |
| 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_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_CURRENCY_NAME | `string` | Network currency name | `Ether` |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` |
| NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS | `string` | Address of network's native token | `0x029a799563238d0e75e20be2f4bda0ea68d00172` | | 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_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) | `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_NETWORK_LOGO | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `https://www.fillmurray.com/240/40` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` | | NEXT_PUBLIC_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 ### UI configuration
| Variable | Type | Description | Default value | 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_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','url':'https://blockscout.com/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_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_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_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_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_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` |
| NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` |
### App configuration ### App configuration
...@@ -75,12 +79,13 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -75,12 +79,13 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` | | 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_HOST | `string` | App host | `blockscout.com` |
| NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` | | NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` |
| NEXT_PUBLIC_APP_ENV | `string` *(optional)* | Current app env (e.g development, review or production). Used for Sentry.io configuration | `production` |
### API configuration ### API configuration
| Variable | Type | Description | Default value | 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_HOST | `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 host in this variable | `my-host.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` | | NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
...@@ -89,13 +94,71 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -89,13 +94,71 @@ The app instance could be customized by passing following variables to NodeJS en
| Property | Type | Description | Example value | Property | Type | Description | Example value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| title | `string` | Displayed name of the network | `'Gnosis Chain'` | | title | `string` | Displayed name of the network | `'Gnosis Chain'` |
| basePath | `string` | Network explorer main page url | `'/xdai/mainnet'` | | url | `string` | Network explorer main page url | `'https://blockscout.com/xdai/mainnet'` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` | | group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `'https://www.fillmurray.com/60/60'` | | 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'` |
| type | `string` *(optional)* | Network type (used for matching pre-defined network icon, which is stored in the project). See all possible values here | `xdai_mainnet` |
### Network explorer configuration properties
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| title | `string` | Displayed name of the explorer | `'Anyblock'` |
| baseUrl | `string` | Base url of the explorer | `'https://explorer.anyblock.tools'` |
| paths | `Record<'tx' \| 'block' \| 'address', string>` | Map of explorer entities and their paths | `'paths':{'tx':'/ethereum/poa/core/tx'}` |
*Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>`
### External services configuration ### External services configuration
| Variable | Type | Description | Default value | Variable | Type | Description | Default value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` | | NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Sentry.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` | | SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Sentry.io app | `<secret>` |
\ No newline at end of file | NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` *(optional)* | Client id for [Auth0](https://auth0.com/) provider | `<secret>` |
### Marketplace app configuration properties
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` |
| title | `string` | Displayed title of the app. | `'The App'` |
| logo | `string` | URL to logo file. Should be at least 144x144. | `'https://foo.app/icon.png'` |
| shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` |
| categories | `Array<MarketplaceCategoryId>` | Displayed category. Select one of the following bellow. | `['security', 'tools']` |
| author | `string` | Displayed author of the app | `'Bob'` |
| url | `string` | URL of the app which will be launched in the iframe. | `'https://foo.app/launch'` |
| description | `string` | Displayed only in the modal dialog with additional info about the app. | `'The best app'` |
| site | `string` *(optional)* | Displayed site link | `'https://blockscout.com'` |
| twitter | `string` *(optional)* | Displayed twitter link | `'https://twitter.com/blockscoutcom'` |
| telegram | `string` *(optional)* | Displayed telegram link | `'https://t.me/poa_network'` |
| github | `string` *(optional)* | Displayed github link | `'https://github.com/blockscout'` |
#### Marketplace categories ids
For each application, you need to specify the `MarketplaceCategoryId` to which it belongs. Select one of the following:
- `defi`
- `exchanges`
- `finance`
- `games`
- `marketplaces`
- `nft`
- `security`
- `social`
- `tools`
- `yieldFarming`
### How to add new environment variable
If the variable should be exposed to the browser don't forget to add prefix `NEXT_PUBLIC_` to its name.
These are the steps that you have to follow to make everything work:
- create the variable placeholder for build-time in file `.env.template`; this is the most important step, without this the app will not receive any variables that are passed at run-time
- for local development purposes add the variable to either `configs/envs/.env.common` or `configs/envs/.env.<network>` files depending on if the variable has the same value for all network or specific value for each network
- add the variable to CI configs
- `deploy/values/review/values.yaml` - review environment
- `deploy/values/main/values.yaml` - production environment
- `deploy/values/e2e/values.yaml` - e2e-test environment
Keep in mind that all json-like values should be single-quoted, e.g `[{'foo': 'bar'}]`
/* eslint-disable no-restricted-properties */ /* eslint-disable no-restricted-properties */
import type { AppItemOverview } from 'types/client/apps';
import type { FeaturedNetwork, NetworkExplorer, PreDefinedNetwork } from 'types/networks';
const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"');
const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
try {
return JSON.parse(env || 'null');
} catch (error) {
return null;
}
};
const stripTrailingSlash = (str: string) => str.at(-1) === '/' ? str.slice(0, -1) : str;
const env = process.env.VERCEL_ENV || process.env.NODE_ENV; const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development'; const isDev = env === 'development';
const appPort = getEnvValue(process.env.NEXT_PUBLIC_APP_PORT);
const appSchema = getEnvValue(process.env.NEXT_PUBLIC_APP_PROTOCOL);
const appHost = getEnvValue(process.env.NEXT_PUBLIC_APP_HOST);
const baseUrl = [ const baseUrl = [
process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https', appSchema || 'https',
'://', '://',
process.env.NEXT_PUBLIC_VERCEL_URL || process.env.NEXT_PUBLIC_APP_HOST, process.env.NEXT_PUBLIC_VERCEL_URL || appHost,
process.env.NEXT_PUBLIC_APP_PORT ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '', appPort && ':' + appPort,
].join(''); ].filter(Boolean).join('');
const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST);
const logoutUrl = (() => {
try {
const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL);
const auth0ClientId = getEnvValue(process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID);
const returnUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_RETURN_URL);
if (!envUrl || !auth0ClientId || !returnUrl) {
throw Error();
}
const url = new URL(envUrl);
url.searchParams.set('client_id', auth0ClientId);
url.searchParams.set('returnTo', returnUrl);
return url.toString();
} catch (error) {
return;
}
})();
const DEFAULT_CURRENCY_DECIMALS = 18;
const config = Object.freeze({ const config = Object.freeze({
env, env,
isDev, isDev,
network: { network: {
type: process.env.NEXT_PUBLIC_NETWORK_TYPE, type: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TYPE) as PreDefinedNetwork | undefined,
subtype: process.env.NEXT_PUBLIC_NETWORK_SUBTYPE, logo: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_LOGO),
logo: process.env.NEXT_PUBLIC_NETWORK_LOGO, name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_NAME),
name: process.env.NEXT_PUBLIC_NETWORK_NAME, id: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ID),
id: process.env.NEXT_PUBLIC_NETWORK_ID, shortName: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME),
shortName: process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME, currency: {
currency: process.env.NEXT_PUBLIC_NETWORK_CURRENCY, name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_NAME),
assetsPathname: process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME, symbol: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL),
nativeTokenAddress: process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS, decimals: Number(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS)) || DEFAULT_CURRENCY_DECIMALS,
basePath: '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/'), },
assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME),
nativeTokenAddress: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS),
explorers: parseEnvJson<Array<NetworkExplorer>>(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_EXPLORERS)) || [],
verificationType: process.env.NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE || 'mining',
}, },
footerLinks: { footerLinks: {
github: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK, github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK),
twitter: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK, twitter: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK),
telegram: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK, telegram: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK),
staking: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK, staking: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK),
}, },
featuredNetworks: process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"'), featuredNetworks: parseEnvJson<Array<FeaturedNetwork>>(getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS)) || [],
blockScoutVersion: process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION, blockScoutVersion: getEnvValue(process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION),
isAccountSupported: process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?.replaceAll('\'', '"') === 'true', isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
marketplaceSubmitForm: process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM, marketplaceAppList: parseEnvJson<Array<AppItemOverview>>(getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_APP_LIST)) || [],
protocol: process.env.NEXT_PUBLIC_APP_PROTOCOL, marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
host: process.env.NEXT_PUBLIC_APP_HOST, protocol: appSchema,
port: process.env.NEXT_PUBLIC_APP_PORT, host: appHost,
port: appPort,
baseUrl, baseUrl,
logoutUrl,
api: { api: {
endpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || 'https://blockscout.com', endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
basePath: process.env.NEXT_PUBLIC_API_BASE_PATH || '', socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
}, },
}); });
......
...@@ -3,15 +3,12 @@ NEXT_PUBLIC_APP_PROTOCOL=http ...@@ -3,15 +3,12 @@ NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000 NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
# nav and footer config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom 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 # api config
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
\ No newline at end of file
# nav and footer config # ui config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking 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'}] NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]
# current network config # network config
NEXT_PUBLIC_NETWORK_NAME=POA NEXT_PUBLIC_NETWORK_NAME=POA
NEXT_PUBLIC_NETWORK_SHORT_NAME=POA NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa
NEXT_PUBLIC_NETWORK_TYPE=poa NEXT_PUBLIC_NETWORK_TYPE=poa_core
NEXT_PUBLIC_NETWORK_SUBTYPE=core
NEXT_PUBLIC_NETWORK_ID=99 NEXT_PUBLIC_NETWORK_ID=99
NEXT_PUBLIC_NETWORK_CURRENCY=POA NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172 NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout', 'id': 'token-approval-tracker', 'title': 'Token Approval Tracker', 'logo': 'https://approval-tracker.vercel.app/icon-192.png', 'categories': ['security', 'tools'], 'shortDescription': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'site': 'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', 'description': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'url': 'https://approval-tracker.vercel.app/'}]
# api config # api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core NEXT_PUBLIC_API_BASE_PATH=/poa/core
const BASE_PATH = require('../../lib/link/basePath.js'); const PATHS = require('../../lib/link/paths');
const PATHS = require('../../lib/link/paths.js');
const oldUrls = [ const oldUrls = [
{ {
...@@ -41,17 +40,10 @@ const oldUrls = [ ...@@ -41,17 +40,10 @@ const oldUrls = [
]; ];
async function redirects() { async function redirects() {
const homePagePath = '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/');
return [ return [
{
source: '/',
destination: homePagePath,
permanent: false,
},
...oldUrls.map(item => { ...oldUrls.map(item => {
const source = BASE_PATH.replaceAll('[', ':').replaceAll(']', '') + item.oldPath; const source = item.oldPath;
const destination = item.newPath.replaceAll('[', ':').replaceAll(']', ''); const destination = item.newPath;
return { source, destination, permanent: false }; return { source, destination, permanent: false };
}), }),
]; ];
......
async function rewrites() { async function rewrites() {
// there can be networks without subtype
// routing in nextjs allows optional params only at the end of the path
// if there are paths with subtype and subsubtype, we will change the routing
// but so far we think we're ok with this hack
//
// UPDATE: as for now I hardcoded all networks without subtype
// because we cannot do proper dynamic rewrites in middleware using runtime ENVs
// see issue - https://github.com/vercel/next.js/discussions/35231
// it seems like it's solved but it's not actually
return [ return [
{ source: '/astar/:slug*', destination: '/astar/mainnet/:slug*' }, { source: '/node-api/:slug*', destination: '/api/:slug*' },
{ source: '/shiden/:slug*', destination: '/shiden/mainnet/:slug*' }, ].filter(Boolean);
];
} }
module.exports = rewrites; module.exports = rewrites;
...@@ -2,7 +2,7 @@ import type * as Sentry from '@sentry/react'; ...@@ -2,7 +2,7 @@ import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
export const config: Sentry.BrowserOptions = { export const config: Sentry.BrowserOptions = {
environment: process.env.VERCEL_ENV || process.env.NODE_ENV, environment: process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_APP_ENV || process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ], integrations: [ new BrowserTracing() ],
......
[
{
"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"
}
]
declare module 'react-identicons' declare module 'react-identicons'
declare module 'data/marketplaceApps.json' {
import type { AppItemOverview } from './types/client/apps';
const value: Array<AppItemOverview>;
export default value;
}
...@@ -15,9 +15,9 @@ blockscout: ...@@ -15,9 +15,9 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_SECRET: ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str] _default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL: ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str] _default: ENC[AES256_GCM,data:v6LJLRwibjc6QgF1t6QNzy/FfL/i2G0mD2X6oxf0o4Q6A8cwovE03Wd5nXWTSyWrN95rJm0IQY7bjU3fXpDw0p/u4vu8dMpfM2bZMMOalXP4pA==,iv:VtNoyCEsesWn0PyWRXzLgbMcUa+voKa1sjsUY26RKnw=,tag:t02c9NVe4aGGo5feOP1OWQ==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL: ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:9cWXw4j6LAg9qUzKTGevJE4U1leMV+i9DD26VDyFqlin4M6O1r/xzoWQtHfrHzxP1RRBuGPIjBqV/WCM1u9cVmhgrs2FQvzZkHk=,iv:Qrxdp4CfnSkCXMxhzMZvvpFSkglzdviq4UYGhffLDNk=,tag:RJhq0+B9Sa5D8k8cAhZW5A==,type:str] _default: ENC[AES256_GCM,data:JQylcEDhJUOqXqBCycJ8XLwz6wpa3Uz3p5MhCEVolLnKkYoDFogGuFt1YkKt7EEtbZjqvSjIa0xQhpezX5hvypU9KxRxoVosUQ4=,iv:dbXG4N4t5jQgx68l0r5iLh5FGGg2O/vqbSDpyxzEauI=,tag:upkfr/3h35rZ93oBNJ3Y/w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL: ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str] _default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY: ACCOUNT_SENDGRID_API_KEY:
...@@ -65,21 +65,21 @@ geth: ...@@ -65,21 +65,21 @@ geth:
frontend: frontend:
environment: environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS: NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:IVCGBLqP7IdmbDe9UbIFvJSBD7+g52chKzakELt2XuHDp9JvC4E+7xxp,iv:bDHp2llHAqhgI5N8swQALSDc6X3S0JCsXbJnEEDDJOc=,tag:Z1WsJXkqtq79WydIgUiDiA==,type:str] _default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
NEXT_PUBLIC_SENTRY_DSN: NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str] _default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI: SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str] _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 NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:6MLOmBMJoB+dYoW8L4JYslO3F5tSFzhrkI+6rGo1a51s9sXZa9c=,iv:126unfUWSieigaq4Zne8321tSYoNy3EHk/qwEodqgH8=,tag:6BlrNkck5yANO2rqECjkuQ==,type:str] _default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-10-14T12:09:11Z" lastmodified: "2022-11-02T07:59:42Z"
mac: ENC[AES256_GCM,data:CnD+9hsC9ZyVhZPo+DXZfPH8svMuk50llaAm3JxgOlzhbJ4yp969WxLhZSORvj520b9geBPLZRU7ujLGiHKhrNzAK438LI2QttKQDt3WSbPwkIGDh/zuA201+gpT73awUNfMKCoHVjq4iQ6ty4KP/NCw1ZMcS/c1WVuRYE9RTl8=,iv:rl8eKiXwrBDjns2hiwJ6f28XyuhjH2soHeR1MBBu2Ig=,tag:vu5jGEPMkvmcl7m8huWl7g==,type:str] mac: ENC[AES256_GCM,data:OrV/dUWOtL23UFQLeIsKsGluTmse42d/4sgFMDs3UXdACsZu8twMt29Y/WaPHyq8Tpn5iYzhBLU6SCUmHxEhBNVzKBd5uCUbav1faS/zW6fSd9bEP7rmbUjaJGHliBkG3T4VCSZn53jR/OMNbSynIxZ0kRpVHr+RTcalaH7dLQ8=,iv:J3gXjFgFyZoPqL+VEnjkuKzA9UIIyK3UsvPWacBZKsY=,tag:/zY1jVjIm5BsDNJlfsg5uw==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
global: global:
env: e2e env: main
# enable Blockscout deploy # enable Blockscout deploy
blockscout: blockscout:
app: blockscout
enabled: true enabled: true
image: image:
_default: blockscout/blockscout:latest _default: blockscout/blockscout:latest
...@@ -10,17 +11,28 @@ blockscout: ...@@ -10,17 +11,28 @@ blockscout:
docker: docker:
port: 80 port: 80
targetPort: 4000 targetPort: 4000
# init container
init:
enabled: true
image:
_default: blockscout/blockscout:latest
service: service:
# ClusterIP, NodePort or LoadBalancer # ClusterIP, NodePort or LoadBalancer
type: ClusterIP type: ClusterIP
# enable ingress # enable ingress
ingress: ingress:
enabled: true enabled: true
annotations: {}
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
host: host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com _default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
#
tls: tls:
enabled: true enabled: true
path:
# - "/poa/sokol(/|$)(.*)"
- "/"
# probes # probes
livenessProbe: livenessProbe:
enabled: true enabled: true
...@@ -41,7 +53,7 @@ blockscout: ...@@ -41,7 +53,7 @@ blockscout:
_default: "2" _default: "2"
# enable service to connect to RDS # enable service to connect to RDS
rds: rds:
enable: false enabled: false
endpoint: endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com _default: <endpoint>.<region>.rds.amazonaws.com
# node label # node label
...@@ -81,15 +93,27 @@ blockscout: ...@@ -81,15 +93,27 @@ blockscout:
RUST_VERIFICATION_SERVICE_URL: RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043 _default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
_default: true _default: 'true'
DISABLE_REALTIME_INDEXER: DISABLE_REALTIME_INDEXER:
_default: 'false' _default: 'false'
SOCKET_ROOT: SOCKET_ROOT:
_default: "/" _default: "/"
NETWORK_PATH: NETWORK_PATH:
_default: "/" _default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES: ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true' _default: 'true'
API_BASE_PATH:
_default: "/"
APPS_MENU:
_default: 'true'
EXTERNAL_APPS:
_default: '[{"title": "Marketplace", "url": "/apps"}]'
JSON_RPC:
_default: https://sokol.poa.network
API_V2_ENABLED:
_default: 'true'
postgres: postgres:
enabled: true enabled: true
...@@ -153,7 +177,7 @@ geth: ...@@ -153,7 +177,7 @@ geth:
ingress: ingress:
enabled: true enabled: true
host: host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com _default: node.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
tls: tls:
enabled: false enabled: false
...@@ -180,7 +204,6 @@ scVerifier: ...@@ -180,7 +204,6 @@ scVerifier:
enabled: true enabled: true
host: host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com _default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
tls: tls:
enabled: true enabled: true
...@@ -247,6 +270,7 @@ scVerifier: ...@@ -247,6 +270,7 @@ scVerifier:
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED: SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false' _default: 'false'
frontend: frontend:
app: blockscout
enabled: true enabled: true
image: image:
_default: ghcr.io/blockscout/frontend:main _default: ghcr.io/blockscout/frontend:main
...@@ -257,12 +281,21 @@ frontend: ...@@ -257,12 +281,21 @@ frontend:
targetPort: 3000 targetPort: 3000
ingress: ingress:
enabled: true enabled: true
# annotations:
# - 'nginx.ingress.kubernetes.io/use-regex: "true"'
host: host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com _default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
# - "/(apps|auth/profile|account)"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/account"
resources: resources:
limits: limits:
memory: memory:
...@@ -279,20 +312,16 @@ frontend: ...@@ -279,20 +312,16 @@ frontend:
enabled: true enabled: true
app: blockscout app: blockscout
environment: environment:
NEXT_PUBLIC_APP_PROTOCOL:
_default: http
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta _default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK: NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout _default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK: NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom _default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: local _default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK: NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network _default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
...@@ -304,18 +333,30 @@ frontend: ...@@ -304,18 +333,30 @@ frontend:
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa _default: poa
NEXT_PUBLIC_NETWORK_TYPE: NEXT_PUBLIC_NETWORK_TYPE:
_default: poa _default: poa_core
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 77 _default: 77
NEXT_PUBLIC_NETWORK_CURRENCY: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA _default: SPOA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true' _default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS: 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'}]" _default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_API_ENDPOINT: NEXT_PUBLIC_API_HOST:
_default: https://blockscout.com _default: blockscout.com
NEXT_PUBLIC_API_BASE_PATH: NEXT_PUBLIC_API_BASE_PATH:
_default: / _default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST:
_default: "[{'author': 'Blockscout', 'id': 'token-approval-tracker', 'title': 'Token Approval Tracker', 'logo': 'https://approval-tracker.vercel.app/icon-192.png', 'categories': ['security', 'tools'], 'shortDescription': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'site': 'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', 'description': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'url': 'https://approval-tracker.vercel.app/'}]"
NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout
...@@ -15,9 +15,9 @@ blockscout: ...@@ -15,9 +15,9 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_SECRET: ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str] _default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL: ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str] _default: ENC[AES256_GCM,data:dQMvW68eW5EokGgekC1HsozsMAq+eqWwqfocgF6QR9+1VA1z5efNbMjTuWJqaSzXq/yWwfAogP8GJSzpS8T7jKATOeVxZ71I4LmlFO7jR4wMKw==,iv:UA0DcnH7z6y5X9C9Qk3WvRrgI3LT/UvH4Fl7gb765/0=,tag:cn78k5f+L8ed0WV3mDKykg==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL: ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:9cWXw4j6LAg9qUzKTGevJE4U1leMV+i9DD26VDyFqlin4M6O1r/xzoWQtHfrHzxP1RRBuGPIjBqV/WCM1u9cVmhgrs2FQvzZkHk=,iv:Qrxdp4CfnSkCXMxhzMZvvpFSkglzdviq4UYGhffLDNk=,tag:RJhq0+B9Sa5D8k8cAhZW5A==,type:str] _default: ENC[AES256_GCM,data:y4px71aTwkmIcqC5T6ui1wHlhRExyy3Id/Dcy3AaKuGhIHUE0URbghm8niB1Seq08U5NeC4LvSmYsbWsx390lHP2dTuWwTaQ33s=,iv:JR3T7ZXxv3eAUn/XzMEz4/U58vJhB/pPWIutGN9XrSk=,tag:MiKgiyfzTHFjbQYdkFIJ8Q==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL: ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str] _default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY: ACCOUNT_SENDGRID_API_KEY:
...@@ -70,16 +70,16 @@ frontend: ...@@ -70,16 +70,16 @@ frontend:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str] _default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI: SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str] _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 NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:KVzLMvMQc6f37c+OWu7vpAdDYwKI048XVY7EVijS2zz6jRRFacg=,iv:LeWSYZeaPdBsOxcGcca8L1Rp3ilsR+R13icX8Q/VUBA=,tag:Bj6NTTqb3R8oxAprZ+7CVQ==,type:str] _default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-10-14T12:08:39Z" lastmodified: "2022-11-01T14:58:11Z"
mac: ENC[AES256_GCM,data:esHv2aUvW4lMmoD5yRWu4OJEpOMkCa7TOyPS0HDkQL25g4TOdE+AfVZBE5wLeL1rePaId4bHnX0sF2Tov0d8xhCH3mv6+Vvmgi+75Oqu8+logQ4LyZSI0yIcvmdGVHhaO6u3u1qwYXHrityIVmiXQdBck5oq67uyT+jtSh1pXpc=,iv:YNWhRxSY0WLRm+wbVURpfxU2K67MAB5dpSILSMy9oCE=,tag:TsCcsGmMG4id5ITR5LrDjg==,type:str] mac: ENC[AES256_GCM,data:3AK4GRnUnAcQrdJ9JrdhSFqMYmYhE2RGiP+NPvO+mqBGDH26pRjN3lkNhHi/uObEQWiQZJLzLEOhSPl0/oDVYRTSGEeEiIlViEm/S5PuD57uFx6ogS9Iz88G/3hnc3HpTAIg2+NVwE7wF1/NK75WlivB1pUGk7OrazhZ+Fhyn5k=,iv:94YFEq/dmS07CqsKFZ2NAKMj3LqqyUB4+2XtHI3UiLg=,tag:4uti1G9eRoL2AwF0n7avdQ==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
global: global:
env: e2e env: review
# enable Blockscout deploy # enable Blockscout deploy
blockscout: blockscout:
app: blockscout
enabled: true enabled: true
image: image:
_default: blockscout/blockscout:latest _default: blockscout/blockscout:latest
...@@ -10,17 +11,28 @@ blockscout: ...@@ -10,17 +11,28 @@ blockscout:
docker: docker:
port: 80 port: 80
targetPort: 4000 targetPort: 4000
# init container
init:
enabled: true
image:
_default: blockscout/blockscout:latest
service: service:
# ClusterIP, NodePort or LoadBalancer # ClusterIP, NodePort or LoadBalancer
type: ClusterIP type: ClusterIP
# enable ingress # enable ingress
ingress: ingress:
enabled: true enabled: true
annotations: {}
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
host: host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com _default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
#
tls: tls:
enabled: true enabled: true
path:
# - "/poa/sokol(/|$)(.*)"
- "/"
# probes # probes
livenessProbe: livenessProbe:
enabled: true enabled: true
...@@ -41,7 +53,7 @@ blockscout: ...@@ -41,7 +53,7 @@ blockscout:
_default: "2" _default: "2"
# enable service to connect to RDS # enable service to connect to RDS
rds: rds:
enable: false enabled: false
endpoint: endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com _default: <endpoint>.<region>.rds.amazonaws.com
# node label # node label
...@@ -81,15 +93,27 @@ blockscout: ...@@ -81,15 +93,27 @@ blockscout:
RUST_VERIFICATION_SERVICE_URL: RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043 _default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
_default: true _default: 'true'
DISABLE_REALTIME_INDEXER: DISABLE_REALTIME_INDEXER:
_default: 'false' _default: 'false'
SOCKET_ROOT: SOCKET_ROOT:
_default: "/" _default: "/"
NETWORK_PATH: NETWORK_PATH:
_default: "/" _default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES: ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true' _default: 'true'
API_BASE_PATH:
_default: "/"
APPS_MENU:
_default: 'true'
EXTERNAL_APPS:
_default: '[{"title": "Marketplace", "url": "/apps"}]'
JSON_RPC:
_default: https://sokol.poa.network
API_V2_ENABLED:
_default: 'true'
postgres: postgres:
enabled: true enabled: true
...@@ -153,7 +177,7 @@ geth: ...@@ -153,7 +177,7 @@ geth:
ingress: ingress:
enabled: true enabled: true
host: host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com _default: node.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
tls: tls:
enabled: false enabled: false
...@@ -180,7 +204,6 @@ scVerifier: ...@@ -180,7 +204,6 @@ scVerifier:
enabled: true enabled: true
host: host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com _default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
tls: tls:
enabled: true enabled: true
...@@ -247,6 +270,7 @@ scVerifier: ...@@ -247,6 +270,7 @@ scVerifier:
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED: SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false' _default: 'false'
frontend: frontend:
app: blockscout
enabled: true enabled: true
image: image:
_default: ghcr.io/blockscout/frontend:main _default: ghcr.io/blockscout/frontend:main
...@@ -257,12 +281,21 @@ frontend: ...@@ -257,12 +281,21 @@ frontend:
targetPort: 3000 targetPort: 3000
ingress: ingress:
enabled: true enabled: true
# annotations:
# - 'nginx.ingress.kubernetes.io/use-regex: "true"'
host: host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com _default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
# - "/(apps|auth/profile|account)"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/account"
resources: resources:
limits: limits:
memory: memory:
...@@ -279,16 +312,16 @@ frontend: ...@@ -279,16 +312,16 @@ frontend:
enabled: true enabled: true
app: blockscout app: blockscout
environment: environment:
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta _default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK: NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout _default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK: NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom _default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: review _default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK: NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network _default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
...@@ -298,20 +331,32 @@ frontend: ...@@ -298,20 +331,32 @@ frontend:
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA _default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: sokol
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa _default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE: NEXT_PUBLIC_NETWORK_TYPE:
_default: sokol _default: poa_core
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 77 _default: 77
NEXT_PUBLIC_NETWORK_CURRENCY: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA _default: SPOA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true' _default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS: 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'}]" _default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_API_ENDPOINT: NEXT_PUBLIC_API_HOST:
_default: https://blockscout.com _default: blockscout.com
NEXT_PUBLIC_API_BASE_PATH: NEXT_PUBLIC_API_BASE_PATH:
_default: / _default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST:
_default: "[{'author': 'Blockscout', 'id': 'token-approval-tracker', 'title': 'Token Approval Tracker', 'logo': 'https://approval-tracker.vercel.app/icon-192.png', 'categories': ['security', 'tools'], 'shortDescription': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'site': 'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', 'description': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'url': 'https://approval-tracker.vercel.app/'}]"
NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout
import {
ChakraProvider,
cookieStorageManagerSSR,
localStorageManager,
} from '@chakra-ui/react';
import type { ChakraProviderProps } from '@chakra-ui/react';
import React from 'react';
interface Props extends ChakraProviderProps {
cookies?: string;
}
export function Chakra({ cookies, theme, children }: Props) {
const colorModeManager =
typeof cookies === 'string' ?
cookieStorageManagerSSR(cookies) :
localStorageManager;
return (
<ChakraProvider colorModeManager={ colorModeManager } theme={ theme }>
{ children }
</ChakraProvider>
);
}
import type { NextApiRequest } from 'next';
export default function getSearchParams(req: NextApiRequest) {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
return new URLSearchParams(searchParams).toString();
}
// import * as Sentry from '@sentry/nextjs';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
......
import React, { createContext, useContext } from 'react';
import type { Props as PageProps } from 'lib/next/getServerSideProps';
type Props = {
children: React.ReactNode;
pageProps: PageProps;
}
const AppContext = createContext<PageProps>({ cookies: '' });
export function AppWrapper({ children, pageProps }: Props) {
const appProps = { cookies: pageProps.cookies };
return (
<AppContext.Provider value={ appProps }>
{ children }
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
...@@ -4,3 +4,9 @@ export const WEI = new BigNumber(10 ** 18); ...@@ -4,3 +4,9 @@ export const WEI = new BigNumber(10 ** 18);
export const GWEI = new BigNumber(10 ** 9); export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI); export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
export const ZERO = new BigNumber(0); export const ZERO = new BigNumber(0);
export const SECOND = 1_000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
...@@ -6,11 +6,12 @@ import isBrowser from './isBrowser'; ...@@ -6,11 +6,12 @@ import isBrowser from './isBrowser';
export enum NAMES { export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
TXS_SORT='txs_sort',
} }
export function get(name?: string | undefined | null) { export function get(name?: NAMES | undefined | null, serverCookie?: string) {
if (!isBrowser()) { if (!isBrowser()) {
return undefined; return serverCookie ? getFromCookieString(serverCookie, name) : undefined;
} }
return Cookies.get(name); return Cookies.get(name);
} }
...@@ -20,3 +21,7 @@ export function set(name: string, value: string, attributes: Types.CookieAttribu ...@@ -20,3 +21,7 @@ export function set(name: string, value: string, attributes: Types.CookieAttribu
return Cookies.set(name, value, attributes); return Cookies.set(name, value, attributes);
} }
export function getFromCookieString(cookieString: string, name?: NAMES | undefined | null) {
return cookieString.split(`${ name }=`)[1]?.split(';')[0];
}
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks'; import featuredNetworks from 'lib/networks/featuredNetworks';
import getMarketplaceApps from '../getMarketplaceApps';
const KEY_WORDS = { const KEY_WORDS = {
BLOB: 'blob:', BLOB: 'blob:',
DATA: 'data:', DATA: 'data:',
...@@ -29,11 +27,22 @@ function getNetworksExternalAssets() { ...@@ -29,11 +27,22 @@ function getNetworksExternalAssets() {
} }
function getMarketplaceAppsOrigins() { function getMarketplaceAppsOrigins() {
return getMarketplaceApps().map(({ url }) => url); return appConfig.marketplaceAppList.map(({ url }) => url);
} }
function getMarketplaceAppsLogosOrigins() { function getMarketplaceAppsLogosOrigins() {
return getMarketplaceApps().map(({ logo }) => logo); return appConfig.marketplaceAppList.map(({ logo }) => new URL(logo));
}
// we cannot use lodash/uniq in middleware code since it calls new Set() and it'is causing an error in Nextjs
// "Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime"
function unique(array: Array<string | undefined>) {
const set: Record<string, boolean> = {};
for (const item of array) {
item && (set[item] = true);
}
return Object.keys(set);
} }
function makePolicyMap() { function makePolicyMap() {
...@@ -53,6 +62,8 @@ function makePolicyMap() { ...@@ -53,6 +62,8 @@ function makePolicyMap() {
// client error monitoring // client error monitoring
'sentry.io', '*.sentry.io', 'sentry.io', '*.sentry.io',
appConfig.api.socket,
], ],
'script-src': [ 'script-src': [
...@@ -92,11 +103,17 @@ function makePolicyMap() { ...@@ -92,11 +103,17 @@ function makePolicyMap() {
// github avatars // github avatars
'avatars.githubusercontent.com', 'avatars.githubusercontent.com',
// other github assets (e.g trustwallet token icons)
'raw.githubusercontent.com',
// auth0 assets
's.gravatar.com',
// network assets // network assets
...networkExternalAssets.map((url) => url.host), ...networkExternalAssets.map((url) => url.host),
// marketplace apps logos // marketplace apps logos
...getMarketplaceAppsLogosOrigins(), ...getMarketplaceAppsLogosOrigins().map((url) => url.host),
], ],
'font-src': [ 'font-src': [
...@@ -134,7 +151,8 @@ function getCspPolicy() { ...@@ -134,7 +151,8 @@ function getCspPolicy() {
return; return;
} }
return [ key, value.join(' ') ].join(' '); const uniqueValues = unique(value);
return [ key, uniqueValues.join(' ') ].join(' ');
}) })
.filter(Boolean) .filter(Boolean)
.join(';'); .join(';');
......
import data from 'data/marketplaceApps.json';
export default function getMarketplaceApps() {
return data;
}
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
interface Error { interface Error {
...@@ -14,14 +16,12 @@ interface Error { ...@@ -14,14 +16,12 @@ interface Error {
export default function useFetchProfileInfo() { export default function useFetchProfileInfo() {
const fetch = useFetch(); const fetch = useFetch();
const router = useRouter();
const url = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }/api/account/v1/user/info`, 'https://blockscout.com'); return useQuery<unknown, Error, UserInfo>([ QueryKeys.profile ], async() => {
const url = new URL(`${ appConfig.api.basePath }/api/account/v1/user/info`, appConfig.api.endpoint);
return useQuery<unknown, Error, UserInfo>([ 'profile' ], async() => { return fetch(url.toString(), { credentials: appConfig.isDev ? 'include' : 'same-origin' });
return fetch(url.toString(), { credentials: 'include' });
}, { }, {
refetchOnMount: false, refetchOnMount: false,
enabled: Boolean(router.query.network_type && router.query.network_sub_type), enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
} }
import { useBreakpointValue } from '@chakra-ui/react'; import { useBreakpointValue } from '@chakra-ui/react';
export default function useIsMobile() { export default function useIsMobile(ssr = true) {
return useBreakpointValue({ base: true, lg: false }); return useBreakpointValue({ base: true, lg: false }, { ssr });
} }
import React, { useMemo } from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import marketplaceApps from 'data/marketplaceApps.json';
import abiIcon from 'icons/ABI.svg'; import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg'; import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg'; import appsIcon from 'icons/apps.svg';
...@@ -18,20 +17,17 @@ import useCurrentRoute from 'lib/link/useCurrentRoute'; ...@@ -18,20 +17,17 @@ import useCurrentRoute from 'lib/link/useCurrentRoute';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
export default function useNavItems() { export default function useNavItems() {
const isMarketplaceFilled = useMemo(() => const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0;
marketplaceApps.filter(item => item.chainIds.includes(appConfig.network.id)),
[ ])
.length > 0;
const currentRoute = useCurrentRoute()(); const currentRoute = useCurrentRoute()();
return React.useMemo(() => { return React.useMemo(() => {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') }, { text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' } : null, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other' // 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/ // 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 // at this stage custom menu items is under development, we will implement it later
...@@ -39,14 +35,14 @@ export default function useNavItems() { ...@@ -39,14 +35,14 @@ export default function useNavItems() {
].filter(notEmpty); ].filter(notEmpty);
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' }, { text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist', isNewUi: true },
{ text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') }, { text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags'), isNewUi: true },
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' }, { text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags', isNewUi: true },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' }, { text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys', isNewUi: true },
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' }, { text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi', isNewUi: true },
]; ];
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' }; const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile', isNewUi: true };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ isMarketplaceFilled, currentRoute ]); }, [ isMarketplaceFilled, currentRoute ]);
......
import clamp from 'lodash/clamp';
import throttle from 'lodash/throttle';
import React from 'react';
import isBrowser from 'lib/isBrowser';
const SCROLL_DIFF_THRESHOLD = 20;
type Directions = 'up' | 'down';
export default function useScrollDirection() {
const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0);
const [ scrollDirection, setDirection ] = React.useState<Directions>();
const handleScroll = React.useCallback(() => {
const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight);
const scrollDiff = currentScrollPosition - prevScrollPosition.current;
if (window.pageYOffset === 0) {
setDirection(undefined);
} else if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) {
setDirection(scrollDiff < 0 ? 'up' : 'down');
}
prevScrollPosition.current = currentScrollPosition;
}, [ ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return scrollDirection;
}
import React from 'react';
import { DAY, HOUR, MINUTE, SECOND } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
function getUnits(diff: number) {
if (diff < MINUTE) {
return [ SECOND, MINUTE ];
}
if (diff < HOUR) {
return [ MINUTE, HOUR ];
}
if (diff < DAY) {
return [ HOUR, DAY ];
}
return [ DAY, 2 * DAY ];
}
function getUpdateParams(ts: string) {
const timeDiff = Date.now() - new Date(ts).getTime();
const [ unit, higherUnit ] = getUnits(timeDiff);
if (unit === DAY) {
return { interval: DAY };
}
const leftover = unit - timeDiff % unit;
return {
startTimeout: unit === SECOND ?
0 :
// here we assume that in current dayjs locale time difference is rounded by Math.round function
// so we have to update displayed value whenever time comes over the middle of the unit interval
// since it will be rounded to the upper bound
(leftover < unit / 2 ? leftover + unit / 2 : leftover - unit / 2) + SECOND,
endTimeout: higherUnit - timeDiff + SECOND,
interval: unit,
};
}
export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) {
const [ value, setValue ] = React.useState(dayjs(ts).fromNow());
React.useEffect(() => {
const timeouts: Array<number> = [];
const intervals: Array<number> = [];
const startIncrement = () => {
const { startTimeout, interval, endTimeout } = getUpdateParams(ts);
if (!startTimeout && !endTimeout) {
return;
}
let intervalId: number;
const startTimeoutId = window.setTimeout(() => {
setValue(dayjs(ts).fromNow());
intervalId = window.setInterval(() => {
setValue(dayjs(ts).fromNow());
}, interval);
intervals.push(intervalId);
}, startTimeout);
const endTimeoutId = window.setTimeout(() => {
window.clearInterval(intervalId);
startIncrement();
}, endTimeout);
timeouts.push(startTimeoutId);
timeouts.push(endTimeoutId);
};
isEnabled && startIncrement();
return () => {
timeouts.forEach(window.clearTimeout);
intervals.forEach(window.clearInterval);
};
}, [ isEnabled, ts ]);
return value;
}
const BASE_PATH = '/[network_type]/[network_sub_type]';
module.exports = BASE_PATH;
...@@ -15,18 +15,8 @@ export default function link( ...@@ -15,18 +15,8 @@ export default function link(
return ''; return '';
} }
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) => { const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !refinedUrlParams.network_sub_type) { let paramValue = urlParams?.[paramName];
return '';
}
let paramValue = refinedUrlParams?.[paramName];
if (Array.isArray(paramValue)) { if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could // FIXME we don't have yet params as array, but typescript says that we could
// dunno know how to manage it, fix me if you find an issue // dunno know how to manage it, fix me if you find an issue
...@@ -42,5 +32,5 @@ export default function link( ...@@ -42,5 +32,5 @@ export default function link(
url.searchParams.append(key, value); url.searchParams.append(key, value);
}); });
return url.toString(); return url.pathname;
} }
const BASE_PATH = require('./basePath');
const paths = { const paths = {
network_index: `${ BASE_PATH }`, network_index: `/`,
watchlist: `${ BASE_PATH }/account/watchlist`, watchlist: `/account/watchlist`,
private_tags: `${ BASE_PATH }/account/tag_address`, private_tags: `/account/tag_address`,
public_tags: `${ BASE_PATH }/account/public_tags_request`, public_tags: `/account/public_tags_request`,
api_keys: `${ BASE_PATH }/account/api_key`, api_keys: `/account/api_key`,
custom_abi: `${ BASE_PATH }/account/custom_abi`, custom_abi: `/account/custom_abi`,
profile: `${ BASE_PATH }/auth/profile`, profile: `/auth/profile`,
txs: `${ BASE_PATH }/txs`, txs: `/txs`,
tx: `${ BASE_PATH }/tx/[id]`, tx: `/tx/[id]`,
blocks: `${ BASE_PATH }/blocks`, blocks: `/blocks`,
block: `${ BASE_PATH }/block/[id]`, block: `/block/[id]`,
tokens: `${ BASE_PATH }/tokens`, tokens: `/tokens`,
token_index: `${ BASE_PATH }/token/[hash]`, token_index: `/token/[hash]`,
token_instance_item: `${ BASE_PATH }/token/[hash]/instance/[id]`, token_instance_item: `/token/[hash]/instance/[id]`,
address_index: `${ BASE_PATH }/address/[id]`, address_index: `/address/[id]`,
address_contract_verification: `${ BASE_PATH }/address/[id]/contract_verifications/new`, address_contract_verification: `/address/[id]/contract_verifications/new`,
apps: `${ BASE_PATH }/apps`, apps: `/apps`,
app_index: `${ BASE_PATH }/apps/[id]`, app_index: `/apps/[id]`,
search_results: `${ BASE_PATH }/search-results`, search_results: `/search-results`,
other: `${ BASE_PATH }/search-results`, other: `/search-results`,
// no slash required, it is correct auth: `/auth/auth0`,
auth: `${ BASE_PATH }auth/auth0`,
}; };
module.exports = paths; module.exports = paths;
import type { FeaturedNetwork } from 'types/networks'; import type { FeaturedNetwork, PreDefinedNetwork } from 'types/networks';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg'; import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
...@@ -12,97 +12,99 @@ import poaIcon from 'icons/networks/icons/poa.svg'; ...@@ -12,97 +12,99 @@ import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg'; import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons // predefined network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = { const ICONS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = {
'/xdai/mainnet': gnosisIcon, xdai_mainnet: gnosisIcon,
'/xdai/optimism': optimismIcon, xdai_optimism: optimismIcon,
'/xdai/aox': arbitrumIcon, xdai_aox: arbitrumIcon,
'/eth/mainnet': ethereumIcon, eth_mainnet: ethereumIcon,
'/etc/mainnet': ethereumClassicIcon, etc_mainnet: ethereumClassicIcon,
'/poa/core': poaIcon, poa_core: poaIcon,
'/rsk/mainnet': rskIcon, rsk_mainnet: rskIcon,
'/xdai/testnet': arbitrumIcon, xdai_testnet: arbitrumIcon,
'/poa/sokol': poaSokolIcon, poa_sokol: poaSokolIcon,
'/artis/sigma1': artisIcon, artis_sigma1: artisIcon,
}; };
// for easy .env.example update // for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([ // const FEATURED_NETWORKS = JSON.stringify([
// { // {
// title: 'Gnosis Chain', // title: 'Gnosis Chain',
// basePath: '/xdai/mainnet', // url: 'https://blockscout.com/xdai/mainnet',
// group: 'mainnets', // group: 'mainnets',
// type: 'xdai_mainnet',
// }, // },
// { // {
// title: 'Optimism on Gnosis Chain', // title: 'Optimism on Gnosis Chain',
// basePath: '/xdai/optimism', // url: 'https://blockscout.com/xdai/optimism',
// group: 'mainnets', // group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60', // icon: 'https://www.fillmurray.com/60/60',
// type: 'xdai_optimism',
// }, // },
// { // {
// title: 'Arbitrum on xDai', // title: 'Arbitrum on xDai',
// basePath: '/xdai/aox', // url: 'https://blockscout.com/xdai/aox',
// group: 'mainnets', // group: 'mainnets',
// }, // },
// { // {
// title: 'Ethereum', // title: 'Ethereum',
// basePath: '/eth/mainnet', // url: 'https://blockscout.com/eth/mainnet',
// group: 'mainnets', // group: 'mainnets',
// type: 'eth_mainnet',
// }, // },
// { // {
// title: 'Ethereum Classic', // title: 'Ethereum Classic',
// basePath: '/etx/mainnet', // url: 'https://blockscout.com/etx/mainnet',
// group: 'mainnets', // group: 'mainnets',
// type: 'etc_mainnet',
// }, // },
// { // {
// title: 'POA', // title: 'POA',
// basePath: '/poa/core', // url: 'https://blockscout.com/poa/core',
// group: 'mainnets', // group: 'mainnets',
// type: 'poa_core',
// }, // },
// { // {
// title: 'RSK', // title: 'RSK',
// basePath: '/rsk/mainnet', // url: 'https://blockscout.com/rsk/mainnet',
// group: 'mainnets', // group: 'mainnets',
// type: 'rsk_mainnet',
// }, // },
// { // {
// title: 'Gnosis Chain Testnet', // title: 'Gnosis Chain Testnet',
// basePath: '/xdai/testnet', // url: 'https://blockscout.com/xdai/testnet',
// group: 'testnets', // group: 'testnets',
// type: 'xdai_testnet',
// }, // },
// { // {
// title: 'POA Sokol', // title: 'POA Sokol',
// basePath: '/poa/sokol', // url: 'https://blockscout.com/poa/sokol',
// group: 'testnets', // group: 'testnets',
// type: 'poa_sokol',
// }, // },
// { // {
// title: 'ARTIS Σ1', // title: 'ARTIS Σ1',
// basePath: '/artis/sigma1', // url: 'https://blockscout.com/artis/sigma1',
// group: 'other', // group: 'other',
// type: 'artis_sigma1',
// }, // },
// { // {
// title: 'LUKSO L14', // title: 'LUKSO L14',
// basePath: '/lukso/l14', // url: 'https://blockscout.com/lukso/l14',
// group: 'other', // group: 'other',
// type: 'lukso_l14',
// }, // },
// { // {
// title: 'Astar', // title: 'Astar',
// basePath: '/astar', // url: 'https://blockscout.com/astar',
// group: 'other', // group: 'other',
// type: 'astar',
// }, // },
// ]).replaceAll('"', '\''); // ]).replaceAll('"', '\'');
function parseNetworkConfig() {
try {
return JSON.parse(appConfig.featuredNetworks || '[]');
} catch (error) {
return [];
}
}
const featuredNetworks: Array<FeaturedNetwork> = (() => { const featuredNetworks: Array<FeaturedNetwork> = (() => {
const networksFromConfig: Array<FeaturedNetwork> = parseNetworkConfig(); return appConfig.featuredNetworks.map((network) => ({
return networksFromConfig.map((network) => ({
...network, ...network,
icon: network.icon || ICONS[network.basePath], icon: network.icon || (network.type ? ICONS[network.type] : undefined),
})); }));
})(); })();
......
import appConfig from 'configs/app/config';
export default function getNetworkValidatorTitle() {
return appConfig.network.verificationType === 'validation' ? 'validator' : 'miner';
}
import _compose from 'lodash/fp/compose';
import _mapValues from 'lodash/mapValues';
import type { NetworkExplorer } from 'types/networks';
import appConfig from 'configs/app/config';
// for easy .env update
// const NETWORK_EXPLORERS = JSON.stringify([
// {
// title: 'Anyblock',
// baseUrl: 'https://explorer.anyblock.tools',
// paths: {
// tx: '/ethereum/poa/core/tx',
// },
// },
// ]).replaceAll('"', '\'');
const stripTrailingSlash = (str: string) => str.at(-1) === '/' ? str.slice(0, -1) : str;
const addLeadingSlash = (str: string) => str.at(0) === '/' ? str : '/' + str;
const networkExplorers: Array<NetworkExplorer> = (() => {
return appConfig.network.explorers.map((explorer) => ({
...explorer,
baseUrl: stripTrailingSlash(explorer.baseUrl),
paths: _mapValues(explorer.paths, _compose(stripTrailingSlash, addLeadingSlash)),
}));
})();
export default networkExplorers;
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; 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 featuredNetworks from 'lib/networks/featuredNetworks'; import featuredNetworks from 'lib/networks/featuredNetworks';
export default function useNetworkNavigationItems() { export default function useNetworkNavigationItems() {
const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName];
const router = useRouter();
return React.useMemo(() => { return React.useMemo(() => {
return featuredNetworks.map((network) => { 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 { return {
...network, ...network,
url: url, isActive: network.type ? appConfig.network.type === network.type : false,
isActive: appConfig.network.basePath === network.basePath,
}; };
}); });
}, [ currentRoute, currentRouteName, router.query ]); }, []);
} }
export type PageParams = { export type PageParams = {
network_type: string;
network_sub_type: string;
id: string; id: string;
} }
export type PageParams = { export type PageParams = unknown
network_type: string;
network_sub_type: string;
}
import type { GetServerSideProps, GetServerSidePropsResult } from 'next';
export type Props = {
cookies: string;
}
export const getServerSideProps: GetServerSideProps = async({ req }): Promise<GetServerSidePropsResult<Props>> => {
return {
props: {
cookies: req.headers.cookie || '',
},
};
};
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: 'blocking' };
};
import type { GetStaticProps, GetStaticPropsResult } from 'next';
export const getStaticProps: GetStaticProps = async(context): Promise<GetStaticPropsResult<{ [key: string]: unknown }>> => {
return {
props: {
pageParams: context.params,
},
};
};
export type PageParams = { export type PageParams = {
network_type: string;
network_sub_type: string;
id: string; id: string;
} }
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns';
export default function sortTxs(txs: TransactionsResponse['items'], sorting?: Sort) {
let sortedTxs;
switch (sorting) {
case 'val-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value));
break;
case 'val-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value));
break;
case 'fee-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value));
break;
case 'fee-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value));
break;
default:
sortedTxs = txs;
}
return sortedTxs;
}
...@@ -15,24 +15,13 @@ export function middleware(req: NextRequest) { ...@@ -15,24 +15,13 @@ export function middleware(req: NextRequest) {
return; return;
} }
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
if (appConfig.network.type !== networkType && appConfig.network.subtype !== networkSubtype) {
const url = req.nextUrl.clone();
url.pathname = `/404`;
return NextResponse.rewrite(url);
}
// we don't have any info from router here, so just do straight forward sub-string search (sorry) // we don't have any info from router here, so just do straight forward sub-string search (sorry)
const isAccountRoute = req.nextUrl.pathname.includes('/account/'); const isAccountRoute = req.nextUrl.pathname.includes('/account/');
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
const apiToken = req.cookies.get(NAMES.API_TOKEN); const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (isAccountRoute && !apiToken) { if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) {
const authUrl = link('auth', networkParams); const authUrl = link('auth');
return NextResponse.redirect(authUrl); return NextResponse.redirect(authUrl);
} }
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Home from 'ui/pages/Home';
const HomePage: NextPage = () => {
return (
<>
<Head><title>Home Page</title></Head>
<Home/>
</>
);
};
export default HomePage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { AppWrapper } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme'; import theme from 'theme';
...@@ -30,12 +31,14 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -30,12 +31,14 @@ function MyApp({ Component, pageProps }: AppProps) {
})); }));
return ( return (
<QueryClientProvider client={ queryClient }> <AppWrapper pageProps={ pageProps }>
<ChakraProvider theme={ theme }> <QueryClientProvider client={ queryClient }>
<Component { ...pageProps }/> <Chakra theme={ theme } cookies={ pageProps.cookies }>
</ChakraProvider> <Component { ...pageProps }/>
<ReactQueryDevtools/> </Chakra>
</QueryClientProvider> <ReactQueryDevtools/>
</QueryClientProvider>
</AppWrapper>
); );
} }
......
...@@ -13,10 +13,10 @@ class MyDocument extends Document { ...@@ -13,10 +13,10 @@ class MyDocument extends Document {
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<link rel="icon" sizes="32x32" type="image/png" href="/favicon-32x32.png"/> <link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
<link rel="icon" sizes="16x16" type="image/png"href="/favicon-16x16.png"/> <link rel="icon" sizes="16x16" type="image/png"href="/static/favicon-16x16.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/> <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/>
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"/> <link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#5bbad5"/>
</Head> </Head>
<body> <body>
<ColorModeScript initialColorMode={ theme.config.initialColorMode }/> <ColorModeScript initialColorMode={ theme.config.initialColorMode }/>
......
...@@ -5,16 +5,7 @@ import React from 'react'; ...@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import ApiKeys from 'ui/pages/ApiKeys'; import ApiKeys from 'ui/pages/ApiKeys';
type PageParams = { const ApiKeysPage: NextPage = () => {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const ApiKeysPage: NextPage<Props> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -26,5 +17,4 @@ const ApiKeysPage: NextPage<Props> = () => { ...@@ -26,5 +17,4 @@ const ApiKeysPage: NextPage<Props> = () => {
export default ApiKeysPage; export default ApiKeysPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -5,16 +5,7 @@ import React from 'react'; ...@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import CustomAbi from 'ui/pages/CustomAbi'; import CustomAbi from 'ui/pages/CustomAbi';
type PageParams = { const CustomAbiPage: NextPage = () => {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const CustomAbiPage: NextPage<Props> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -26,5 +17,4 @@ const CustomAbiPage: NextPage<Props> = () => { ...@@ -26,5 +17,4 @@ const CustomAbiPage: NextPage<Props> = () => {
export default CustomAbiPage; export default CustomAbiPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -5,16 +5,7 @@ import React from 'react'; ...@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PublicTags from 'ui/pages/PublicTags'; import PublicTags from 'ui/pages/PublicTags';
type PageParams = { const PublicTagsPage: NextPage = () => {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const PublicTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -26,5 +17,4 @@ const PublicTagsPage: NextPage<Props> = () => { ...@@ -26,5 +17,4 @@ const PublicTagsPage: NextPage<Props> = () => {
export default PublicTagsPage; export default PublicTagsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -5,16 +5,7 @@ import React from 'react'; ...@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PrivateTags from 'ui/pages/PrivateTags'; import PrivateTags from 'ui/pages/PrivateTags';
type PageParams = { const AddressTagsPage: NextPage = () => {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -26,5 +17,4 @@ const AddressTagsPage: NextPage<Props> = () => { ...@@ -26,5 +17,4 @@ const AddressTagsPage: NextPage<Props> = () => {
export default AddressTagsPage; export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -5,16 +5,7 @@ import React from 'react'; ...@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import WatchList from 'ui/pages/Watchlist'; import WatchList from 'ui/pages/Watchlist';
type PageParams = { const WatchListPage: NextPage = () => {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const WatchListPage: NextPage<Props> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -28,5 +19,4 @@ const WatchListPage: NextPage<Props> = () => { ...@@ -28,5 +19,4 @@ const WatchListPage: NextPage<Props> = () => {
export default WatchListPage; export default WatchListPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/blocks/${ req.query.id }/transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => {
return `/v2/config/json-rpc-url`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler'; import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => { const getUrl = (req: NextApiRequest) => {
const searchParams: Record<string, string> = {}; const searchParamsStr = getSearchParams(req);
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 : '' }`; return `/v2/transactions/${ searchParamsStr ? '?' + searchParamsStr : '' }`;
}; };
......
import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
...@@ -5,7 +6,7 @@ import Apps from 'ui/pages/Apps'; ...@@ -5,7 +6,7 @@ import Apps from 'ui/pages/Apps';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage = () => { const AppsPage: NextPage = () => {
return ( return (
<Page> <Page>
<PageTitle text="Apps"/> <PageTitle text="Apps"/>
...@@ -18,5 +19,4 @@ const AppsPage = () => { ...@@ -18,5 +19,4 @@ const AppsPage = () => {
export default AppsPage; export default AppsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'; ...@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json'; import appConfig from 'configs/app/config';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import MarketplaceApp from 'ui/pages/MarketplaceApp'; import MarketplaceApp from 'ui/pages/MarketplaceApp';
...@@ -23,7 +23,7 @@ const AppPage: NextPage = () => { ...@@ -23,7 +23,7 @@ const AppPage: NextPage = () => {
return; return;
} }
const app = marketplaceApps.find((app) => app.id === id); const app = appConfig.marketplaceAppList.find((app) => app.id === id);
setApp(app); setApp(app);
setIsLoading(false); setIsLoading(false);
}, [ id ]); }, [ id ]);
...@@ -47,5 +47,4 @@ const AppPage: NextPage = () => { ...@@ -47,5 +47,4 @@ const AppPage: NextPage = () => {
export default AppPage; export default AppPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -15,5 +15,4 @@ const MyProfilePage: NextPage = () => { ...@@ -15,5 +15,4 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage; export default MyProfilePage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -17,5 +17,4 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -17,5 +17,4 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage; export default BlockPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage'; import BlocksNextPage from 'lib/next/blocks/BlocksNextPage';
type Props = { const BlockPage: NextPage = () => {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = () => {
return ( return (
<BlocksNextPage/> <BlocksNextPage/>
); );
...@@ -17,5 +11,4 @@ const BlockPage: NextPage<Props> = () => { ...@@ -17,5 +11,4 @@ const BlockPage: NextPage<Props> = () => {
export default BlockPage; export default BlockPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
const Home: NextPage = () => { import Home from 'ui/pages/Home';
return null;
const HomePage: NextPage = () => {
return (
<>
<Head><title>Home Page</title></Head>
<Home/>
</>
);
}; };
export default Home; export default HomePage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
...@@ -17,5 +17,4 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -17,5 +17,4 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage; export default TransactionPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -5,16 +5,7 @@ import React from 'react'; ...@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions'; import Transactions from 'ui/pages/Transactions';
type PageParams = { const TxsPage: NextPage = () => {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -24,7 +15,6 @@ const AddressTagsPage: NextPage<Props> = () => { ...@@ -24,7 +15,6 @@ const AddressTagsPage: NextPage<Props> = () => {
); );
}; };
export default AddressTagsPage; export default TxsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -16,10 +16,10 @@ const variantSimple = definePartsStyle((props) => { ...@@ -16,10 +16,10 @@ const variantSimple = definePartsStyle((props) => {
th: { th: {
border: 0, border: 0,
color: mode('gray.600', 'whiteAlpha.700')(props), color: mode('gray.600', 'whiteAlpha.700')(props),
backgroundColor: mode('blackAlpha.100', 'whiteAlpha.200')(props),
...transitionProps, ...transitionProps,
}, },
thead: { thead: {
backgroundColor: mode('blackAlpha.100', 'whiteAlpha.200')(props),
...transitionProps, ...transitionProps,
}, },
td: { td: {
...@@ -73,9 +73,6 @@ const variants = { ...@@ -73,9 +73,6 @@ const variants = {
}; };
const baseStyle = definePartsStyle({ const baseStyle = definePartsStyle({
thead: {
backgroundColor: 'gray.50',
},
th: { th: {
textTransform: 'none', textTransform: 'none',
fontFamily: 'body', fontFamily: 'body',
...@@ -83,6 +80,12 @@ const baseStyle = definePartsStyle({ ...@@ -83,6 +80,12 @@ const baseStyle = definePartsStyle({
overflow: 'hidden', overflow: 'hidden',
color: 'gray.500', color: 'gray.500',
letterSpacing: 'none', letterSpacing: 'none',
_first: {
borderTopLeftRadius: '8px',
},
_last: {
borderTopRightRadius: '8px',
},
}, },
td: { td: {
fontSize: 'md', fontSize: 'md',
...@@ -92,7 +95,7 @@ const baseStyle = definePartsStyle({ ...@@ -92,7 +95,7 @@ const baseStyle = definePartsStyle({
tableLayout: 'fixed', tableLayout: 'fixed',
borderTopLeftRadius: 'base', borderTopLeftRadius: 'base',
borderTopRightRadius: 'base', borderTopRightRadius: 'base',
overflow: 'hidden', overflow: 'unset',
fontVariant: 'normal', fontVariant: 'normal',
}, },
}); });
......
const breakpoints = { const breakpoints = {
// maybe we need them in future // maybe we need them in future
sm: '414px', sm: '415px',
// md: '768px', // md: '768px',
lg: '1000px', lg: '1000px',
xl: '1440px', xl: '1440px',
......
const zIndices = {
hide: -1,
auto: 'auto',
base: 0,
docked: 10,
dropdown: 1000,
sticky: 1100,
sticky1: 1101,
sticky2: 1102,
banner: 1200,
overlay: 1300,
modal: 1400,
popover: 1500,
skipLink: 1600,
toast: 1700,
tooltip: 1800,
};
export default zIndices;
...@@ -7,6 +7,7 @@ import breakpoints from './foundations/breakpoints'; ...@@ -7,6 +7,7 @@ import breakpoints from './foundations/breakpoints';
import colors from './foundations/colors'; import colors from './foundations/colors';
import transition from './foundations/transition'; import transition from './foundations/transition';
import typography from './foundations/typography'; import typography from './foundations/typography';
import zIndices from './foundations/zIndices';
import global from './global'; import global from './global';
const overrides = { const overrides = {
...@@ -20,6 +21,7 @@ const overrides = { ...@@ -20,6 +21,7 @@ const overrides = {
}, },
breakpoints, breakpoints,
transition, transition,
zIndices,
}; };
export default extendTheme(overrides); export default extendTheme(overrides);
export interface AddressTag {
label: string;
display_name: string;
address_hash: string;
}
export interface WatchlistName {
label: string;
display_name: string;
}
export interface AddressParam { export interface AddressParam {
hash: string; hash: string;
implementation_name: string; implementation_name: string;
name: string; name: string | null;
is_contract: boolean; is_contract: boolean;
private_tags: Array<AddressTag> | null;
watchlist_names: Array<WatchlistName> | null;
public_tags: Array<AddressTag> | null;
} }
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward'; import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
export type BlockType = 'block' | 'reorg' | 'uncle'; export type BlockType = 'block' | 'reorg' | 'uncle';
...@@ -37,3 +38,12 @@ export interface BlocksResponse { ...@@ -37,3 +38,12 @@ export interface BlocksResponse {
items_count: number; items_count: number;
}; };
} }
export interface BlockTransactionsResponse {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
} | null;
}
...@@ -10,10 +10,11 @@ export interface InternalTransaction { ...@@ -10,10 +10,11 @@ export interface InternalTransaction {
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
created_contract: AddressParam; created_contract: AddressParam;
value: number; value: string;
index: number; index: number;
block: number; block: number;
timestamp: string; timestamp: string;
gas_limit: string;
} }
export interface InternalTransactionsResponse { export interface InternalTransactionsResponse {
......
export type JsonRpcUrlResponse = {
json_rpc_url: string;
}
export interface Reward { export interface Reward {
reward: number; reward: string;
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward'; type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
} }
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo {
address: string;
type: TokenType;
symbol: string | null;
name: string | null;
decimals: string | null;
holders: string | null;
exchange_rate: string | null;
}
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric } from './tokenInfo';
export type ERC1155TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null;
value: string;
}
export type Erc721TotalPayload = {
token_id: string;
}
export type Erc1155TotalPayload = {
decimals: string | null;
value: string; value: string;
token_id: string; token_id: string;
} }
export type TokenTransfer = ( export type TokenTransfer = (
{ {
token_type: 'ERC-20'; token: TokenInfoGeneric<'ERC-20'>;
total: { total: Erc20TotalPayload;
value: string;
};
} | } |
{ {
token_type: 'ERC-721'; token: TokenInfoGeneric<'ERC-721'>;
total: { total: Erc721TotalPayload;
token_id: string;
};
} | } |
{ {
token_type: 'ERC-1155'; token: TokenInfoGeneric<'ERC-1155'>;
total: ERC1155TotalPayload | Array<ERC1155TotalPayload>; total: Erc1155TotalPayload | Array<Erc1155TotalPayload>;
} }
) & TokenTransferBase ) & TokenTransferBase
interface TokenTransferBase { interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting'; type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
txHash: string; tx_hash: string;
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
token_address: string;
token_symbol: string;
exchange_rate: string;
} }
...@@ -38,6 +38,9 @@ export interface Transaction { ...@@ -38,6 +38,9 @@ export interface Transaction {
token_transfers: Array<TokenTransfer> | null; token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean; token_transfers_overflow: boolean;
exchange_rate: string; exchange_rate: string;
method: string;
tx_types: Array<TransactionType>;
tx_tag: string | null;
} }
export interface TransactionsResponse { export interface TransactionsResponse {
...@@ -46,5 +49,7 @@ export interface TransactionsResponse { ...@@ -46,5 +49,7 @@ export interface TransactionsResponse {
block_number: number; block_number: number;
index: number; index: number;
items_count: number; items_count: number;
}; } | null;
} }
export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer'
export type TTxsFilters = {
filter: 'pending' | 'validated';
type?: Array<TypeFilter>;
method?: Array<MethodFilter>;
}
export type TypeFilter = 'token_transfer' | 'contract_creation' | 'contract_call' | 'coin_transfer' | 'token_creation';
export type MethodFilter = 'approve' | 'transfer' | 'multicall' | 'mint' | 'commit';
export enum QueryKeys {
addressTags = 'address-tags',
apiKeys = 'api-keys',
block = 'block',
blocks = 'blocks',
customAbis = 'custom-abis',
profile = 'profile',
publicTags = 'public-tags',
transactionTags = 'transaction-tags',
watchlist = 'watchlist',
}
...@@ -26,7 +26,6 @@ export type AppItemPreview = { ...@@ -26,7 +26,6 @@ export type AppItemPreview = {
} }
export type AppItemOverview = AppItemPreview & { export type AppItemOverview = AppItemPreview & {
chainIds: Array<string>;
author: string; author: string;
url: string; url: string;
description: string; description: string;
......
export enum QueryKeys {
csrf = 'csrf',
profile = 'profile',
transactions = 'transactions',
tx = 'tx',
txInternals = 'tx-internals',
txLog = 'tx-log',
txRawTrace = 'tx-raw-trace',
blockTxs = 'block-transactions',
}
export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | undefined; export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | '';
...@@ -2,9 +2,23 @@ import type { FunctionComponent, SVGAttributes } from 'react'; ...@@ -2,9 +2,23 @@ import type { FunctionComponent, SVGAttributes } from 'react';
export type NetworkGroup = 'mainnets' | 'testnets' | 'other'; export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export type PreDefinedNetwork = 'xdai_mainnet' | 'xdai_optimism' | 'xdai_aox' | 'eth_mainnet' | 'etc_mainnet' | 'poa_core' |
'rsk_mainnet' | 'xdai_testnet' | 'poa_sokol' | 'artis_sigma1' | 'lukso_l14' | 'astar' | 'shiden' | 'shibuya';
export interface FeaturedNetwork { export interface FeaturedNetwork {
title: string; title: string;
basePath: string; url: string;
group: NetworkGroup; group: NetworkGroup;
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string; icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
type?: PreDefinedNetwork;
} }
export interface NetworkExplorer {
title: string;
baseUrl: string;
paths: {
tx: string;
};
}
export type NetworkVerificationType = 'mining' | 'validation';
import React from 'react';
const ScrollDirectionContext = React.createContext<'up' | 'down' | undefined>(undefined);
export default ScrollDirectionContext;
...@@ -12,6 +12,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; ...@@ -12,6 +12,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
...@@ -47,17 +48,17 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -47,17 +48,17 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const body = { name: data.name }; const body = { name: data.name };
if (!data.token) { if (!data.token) {
return fetch('/api/account/api-keys', { method: 'POST', body }); return fetch('/node-api/account/api-keys', { method: 'POST', body });
} }
return fetch(`/api/account/api-keys/${ data.token }`, { method: 'PUT', body }); return fetch(`/node-api/account/api-keys/${ data.token }`, { method: 'PUT', body });
}; };
const mutation = useMutation(updateApiKey, { const mutation = useMutation(updateApiKey, {
onSuccess: async(data) => { onSuccess: async(data) => {
const response = data as unknown as ApiKey; const response = data as unknown as ApiKey;
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => { queryClient.setQueryData([ QueryKeys.apiKeys ], (prevData: ApiKeys | undefined) => {
const isExisting = prevData && prevData.some((item) => item.api_key === response.api_key); const isExisting = prevData && prevData.some((item) => item.api_key === response.api_key);
if (isExisting) { if (isExisting) {
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -21,26 +20,24 @@ interface Props { ...@@ -21,26 +20,24 @@ interface Props {
const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => { const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
return ( return (
<TableContainer width="100%"> <Table variant="simple" minWidth="600px">
<Table variant="simple" minWidth="600px"> <Thead>
<Thead> <Tr>
<Tr> <Th>{ `API key token (limit ${ limit } keys)` }</Th>
<Th>{ `API key token (limit ${ limit } keys)` }</Th> <Th width="108px"></Th>
<Th width="108px"></Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item) => (
{ data.map((item) => ( <ApiKeyTableItem
<ApiKeyTableItem item={ item }
item={ item } key={ item.api_key }
key={ item.api_key } onDeleteClick={ onDeleteClick }
onDeleteClick={ onDeleteClick } onEditClick={ onEditClick }
onEditClick={ onEditClick } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -18,11 +19,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -18,11 +19,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const fetch = useFetch(); const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' }); return fetch(`/node-api/account/api-keys/${ data.api_key }`, { method: 'DELETE' });
}, [ data.api_key, fetch ]); }, [ data.api_key, fetch ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => { queryClient.setQueryData([ QueryKeys.apiKeys ], (prevData: ApiKeys | undefined) => {
return prevData?.filter((item) => item.api_key !== data.api_key); return prevData?.filter((item) => item.api_key !== data.api_key);
}); });
}, [ data, queryClient ]); }, [ data, queryClient ]);
......
...@@ -7,7 +7,7 @@ import React, { useCallback } from 'react'; ...@@ -7,7 +7,7 @@ import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json'; import appConfig from 'configs/app/config';
import linkIcon from 'icons/link.svg'; import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg'; import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg'; import tgIcon from 'icons/social/telega.svg';
...@@ -43,7 +43,7 @@ const AppModal = ({ ...@@ -43,7 +43,7 @@ const AppModal = ({
twitter, twitter,
logo, logo,
categories, categories,
} = marketplaceApps.find(app => app.id === id) as AppItemOverview; } = appConfig.marketplaceAppList.find(app => app.id === id) as AppItemOverview;
const socialLinks = [ const socialLinks = [
telegram ? { telegram ? {
...@@ -206,7 +206,7 @@ const AppModal = ({ ...@@ -206,7 +206,7 @@ const AppModal = ({
</Link> </Link>
) } ) }
{ socialLinks.length && ( { socialLinks.length > 0 && (
<List <List
marginLeft={{ sm: 'auto' }} marginLeft={{ sm: 'auto' }}
display="grid" display="grid"
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps'; import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import appConfig from 'configs/app/config';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg'; import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem'; import CategoriesMenuItem from './CategoriesMenuItem';
...@@ -20,6 +21,10 @@ type Props = { ...@@ -20,6 +21,10 @@ type Props = {
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => { const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId); const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId);
const actualCategories = appConfig.marketplaceAppList.map(app => app.categories).flat();
const displayedCategories = categoriesList.filter(category => category.id === 'all' ||
category.id === 'favorites' ||
actualCategories.includes(category.id));
return ( return (
<Menu> <Menu>
...@@ -43,7 +48,7 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => { ...@@ -43,7 +48,7 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
</MenuButton> </MenuButton>
<MenuList zIndex={ 3 }> <MenuList zIndex={ 3 }>
{ categoriesList.map((category: MarketplaceCategory) => ( { displayedCategories.map((category: MarketplaceCategory) => (
<CategoriesMenuItem <CategoriesMenuItem
key={ category.id } key={ category.id }
id={ category.id } id={ category.id }
......
...@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react'; ...@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import marketplaceApps from 'data/marketplaceApps.json';
const favoriteAppsLocalStorageKey = 'favoriteApps'; const favoriteAppsLocalStorageKey = 'favoriteApps';
...@@ -79,8 +78,7 @@ export default function useMarketplaceApps() { ...@@ -79,8 +78,7 @@ export default function useMarketplaceApps() {
}, [ filterQuery, category, filterApps ]); }, [ filterQuery, category, filterApps ]);
useEffect(() => { useEffect(() => {
const defaultDisplayedApps = [ ...marketplaceApps ] const defaultDisplayedApps = [ ...appConfig.marketplaceAppList ]
.filter(item => item.chainIds.includes(appConfig.network.id))
.sort((a, b) => a.title.localeCompare(b.title)); .sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps); setDefaultAppList(defaultDisplayedApps);
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-ui/react'; import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
...@@ -17,6 +19,7 @@ import type { ErrorType } from 'lib/hooks/useFetch'; ...@@ -17,6 +19,7 @@ import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton'; import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -34,8 +37,8 @@ const BlockDetails = () => { ...@@ -34,8 +37,8 @@ const BlockDetails = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, Block>( const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, Block>(
[ 'block', router.query.id ], [ QueryKeys.block, router.query.id ],
async() => await fetch(`/api/blocks/${ router.query.id }`), async() => await fetch(`/node-api/blocks/${ router.query.id }`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
...@@ -69,6 +72,8 @@ const BlockDetails = () => { ...@@ -69,6 +72,8 @@ const BlockDetails = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>; const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data); const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
const validatorTitle = getNetworkValidatorTitle();
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<DetailsInfoItem <DetailsInfoItem
...@@ -104,17 +109,17 @@ const BlockDetails = () => { ...@@ -104,17 +109,17 @@ const BlockDetails = () => {
title="Transactions" title="Transactions"
hint="The number of transactions in the block." hint="The number of transactions in the block."
> >
<Link href={ link('block', { id: router.query.id }, { tab: 'transactions' }) }> <Link href={ link('block', { id: router.query.id }, { tab: 'txs' }) }>
{ data.tx_count } transactions { data.tx_count } transactions
</Link> </Link>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Mined by" title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
hint="A block producer who successfully included the block onto the blockchain." hint="A block producer who successfully included the block onto the blockchain."
columnGap={ 1 } columnGap={ 1 }
> >
<AddressLink hash={ data.miner.hash }/> <AddressLink hash={ data.miner.hash }/>
{ data.miner.name && <Text>(Miner: { data.miner.name })</Text> } { data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> }
{ /* api doesn't return the block processing time yet */ } { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ } { /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem> </DetailsInfoItem>
...@@ -122,12 +127,12 @@ const BlockDetails = () => { ...@@ -122,12 +127,12 @@ const BlockDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Block reward" title="Block reward"
hint={ hint={
`For each block, the miner is rewarded with a finite amount of ${ appConfig.network.currency || 'native token' } `For each block, the ${ validatorTitle } is rewarded with a finite amount of ${ appConfig.network.currency.symbol || 'native token' }
on top of the fees paid for all transactions in the block.` on top of the fees paid for all transactions in the block.`
} }
columnGap={ 1 } columnGap={ 1 }
> >
<Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text> <Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
{ (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && ( { (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && (
<Text variant="secondary" whiteSpace="break-spaces">( <Text variant="secondary" whiteSpace="break-spaces">(
<Tooltip label="Static block reward"> <Tooltip label="Static block reward">
...@@ -153,6 +158,19 @@ const BlockDetails = () => { ...@@ -153,6 +158,19 @@ const BlockDetails = () => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.rewards
?.filter(({ type }) => type !== 'Validator Reward' && type !== 'Miner Reward')
.map(({ type, reward }) => (
<DetailsInfoItem
key={ type }
title={ type }
// is this text correct for validators?
hint={ `Amount of distributed reward. ${ capitalize(validatorTitle) }s receive a static block reward + Tx fees + uncle fees.` }
>
{ BigNumber(reward).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</DetailsInfoItem>
))
}
{ sectionGap } { sectionGap }
...@@ -180,7 +198,7 @@ const BlockDetails = () => { ...@@ -180,7 +198,7 @@ const BlockDetails = () => {
title="Base fee per gas" title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion." hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
> >
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency } </Text> <Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text>
<Text variant="secondary" whiteSpace="pre"> <Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei) { space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
</Text> </Text>
...@@ -189,13 +207,13 @@ const BlockDetails = () => { ...@@ -189,13 +207,13 @@ const BlockDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint={ hint={
`Amount of ${ appConfig.network.currency || 'native token' } burned from transactions included in the block. `Amount of ${ appConfig.network.currency.symbol || 'native token' } burned from transactions included in the block.
Equals Block Base Fee per Gas * Gas Used.` Equals Block Base Fee per Gas * Gas Used.`
} }
> >
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text> <Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
{ !txFees.isEqualTo(ZERO) && ( { !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label="Burnt fees / Txn fees * 100%">
<Box> <Box>
...@@ -212,13 +230,13 @@ const BlockDetails = () => { ...@@ -212,13 +230,13 @@ const BlockDetails = () => {
title="Priority fee / Tip" title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion." hint="User-defined tips sent to validator for transaction priority/inclusion."
> >
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency } { BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ /* api doesn't support extra data yet */ } { /* api doesn't support extra data yet */ }
{ /* <DetailsInfoItem { /* <DetailsInfoItem
title="Extra data" title="Extra data"
hint="Any data that can be included by the miner in the block." hint={ `Any data that can be included by the ${ validatorTitle } in the block.` }
> >
<Text whiteSpace="pre">{ data.extra_data } </Text> <Text whiteSpace="pre">{ data.extra_data } </Text>
<Text variant="secondary">(Hex: { data.extra_data })</Text> <Text variant="secondary">(Hex: { data.extra_data })</Text>
...@@ -247,7 +265,7 @@ const BlockDetails = () => { ...@@ -247,7 +265,7 @@ const BlockDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Difficulty" title="Difficulty"
hint="Block difficulty for miner, used to calibrate block generation time." hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time.` }
> >
{ BigNumber(data.difficulty).toFormat() } { BigNumber(data.difficulty).toFormat() }
</DetailsInfoItem> </DetailsInfoItem>
...@@ -293,17 +311,6 @@ const BlockDetails = () => { ...@@ -293,17 +311,6 @@ const BlockDetails = () => {
> >
{ data.nonce } { data.nonce }
</DetailsInfoItem> </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> </Grid>
......
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
const BlockTxs = () => { const BlockTxs = () => {
return <TxsContent showDescription={ false } showSortButton={ false } txs={ [] }/>; const router = useRouter();
return (
<TxsContent
queryName={ QueryKeys.blockTxs }
apiPath={ `/api/blocks/${ router.query.id }/transactions` }
/>
);
}; };
export default BlockTxs; export default BlockTxs;
import { Text } from '@chakra-ui/react';
import React from 'react';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
interface Props {
ts: string;
isEnabled?: boolean;
}
const BlockTimestamp = ({ ts, isEnabled }: Props) => {
const timeAgo = useTimeAgoIncrement(ts, isEnabled);
return <Text variant="secondary" fontWeight={ 400 }>{ timeAgo }</Text>;
};
export default React.memo(BlockTimestamp);
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { BlockType, BlocksResponse } from 'types/api/block'; import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
...@@ -20,8 +21,8 @@ const BlocksContent = ({ type }: Props) => { ...@@ -20,8 +21,8 @@ const BlocksContent = ({ type }: Props) => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ 'blocks', type ], [ QueryKeys.blocks, type ],
async() => await fetch(`/api/blocks${ type ? `?type=${ type }` : '' }`), async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`),
); );
if (isLoading) { if (isLoading) {
...@@ -48,11 +49,12 @@ const BlocksContent = ({ type }: Props) => { ...@@ -48,11 +49,12 @@ const BlocksContent = ({ type }: Props) => {
return ( return (
<> <>
<Text>Total of { data.items[0].height.toLocaleString() } blocks</Text> <Text as="span">Total of { data.items[0].height.toLocaleString() } blocks</Text>
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
<Show above="lg" key="content-desktop"><BlocksTable 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 }}> <Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/> { /* eslint-disable-next-line react/jsx-no-bind */ }
<Pagination page={ 1 } onNextPageClick={ () => {} } onPrevPageClick={ () => {} } resetPage={ () => {} } hasNextPage/>
</Box> </Box>
</> </>
); );
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
...@@ -12,7 +13,10 @@ interface Props { ...@@ -12,7 +13,10 @@ interface Props {
const BlocksList = ({ data }: Props) => { const BlocksList = ({ data }: Props) => {
return ( return (
<Box mt={ 8 }> <Box mt={ 8 }>
{ data.map((item) => <BlocksListItem key={ item.height } data={ item }/>) } <AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) }
</AnimatePresence>
</Box> </Box>
); );
}; };
......
import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react'; import { Flex, Link, Spinner, Text, Box, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward'; import { WEI, ZERO } from 'lib/consts';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
...@@ -18,17 +19,21 @@ import Utilization from 'ui/shared/Utilization'; ...@@ -18,17 +19,21 @@ import Utilization from 'ui/shared/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isPending?: boolean;
enableTimeIncrement?: boolean;
} }
const BlocksListItem = ({ data, isPending }: Props) => { const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const totalReward = data.rewards
const { totalReward, burntFees, txFees } = getBlockReward(data); ?.map(({ reward }) => BigNumber(reward))
.reduce((result, item) => result.plus(item), ZERO) || ZERO;
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
return ( return (
<AccountListItemMobile rowGap={ 3 }> <AccountListItemMobile rowGap={ 3 } key={ String(data.height) }>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm" color="blue.500" emptyColor={ spinnerEmptyColor }/> } { isPending && <Spinner size="sm"/> }
<Link <Link
fontWeight={ 600 } fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) } href={ link('block', { id: String(data.height) }) }
...@@ -36,14 +41,14 @@ const BlocksListItem = ({ data, isPending }: Props) => { ...@@ -36,14 +41,14 @@ const BlocksListItem = ({ data, isPending }: Props) => {
{ data.height } { data.height }
</Link> </Link>
</Flex> </Flex>
<Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text> <Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text> <Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Miner</Text> <Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/> <AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
...@@ -52,22 +57,26 @@ const BlocksListItem = ({ data, isPending }: Props) => { ...@@ -52,22 +57,26 @@ const BlocksListItem = ({ data, isPending }: Props) => {
</Flex> </Flex>
<Box> <Box>
<Text fontWeight={ 500 }>Gas used</Text> <Text fontWeight={ 500 }>Gas used</Text>
<Flex columnGap={ 4 }> <Flex columnGap={ 4 } mt={ 2 }>
<Text variant="secondary">{ BigNumber(data.gas_used || 0).toFormat() }</Text> <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() }/> <Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/> <GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
</Flex> </Flex>
</Box> </Box>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.div(WEI).toFixed() }</Text> <Text variant="secondary">{ totalReward.div(WEI).toFixed() }</Text>
</Flex> </Flex>
<Flex> <Box>
<Text fontWeight={ 500 }>Burnt fees</Text> <Text fontWeight={ 500 }>Burnt fees</Text>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" ml={ 2 }/> <Flex columnGap={ 4 } mt={ 2 }>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text> <Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
</Flex> <Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
</Box>
</AccountListItemMobile> </AccountListItemMobile>
); );
}; };
......
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlocksTableItem from 'ui/blocks/BlocksTableItem'; import BlocksTableItem from 'ui/blocks/BlocksTableItem';
import { default as Thead } from 'ui/shared/TheadSticky';
interface Props { interface Props {
data: Array<Block>; data: Array<Block>;
...@@ -13,24 +17,25 @@ interface Props { ...@@ -13,24 +17,25 @@ interface Props {
const BlocksTable = ({ data }: Props) => { const BlocksTable = ({ data }: Props) => {
return ( return (
<TableContainer width="100%" mt={ 8 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 } mt={ 8 }>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Thead top={ 0 }>
<Thead> <Tr>
<Tr> <Th width="125px">Block</Th>
<Th width="125px">Block</Th> <Th width="120px">Size</Th>
<Th width="120px">Size</Th> <Th width="21%" minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="21%" minW="144px">Miner</Th> <Th width="64px" isNumeric>Txn</Th>
<Th width="64px" isNumeric>Txn</Th> <Th width="35%">Gas used</Th>
<Th width="35%">Gas used</Th> <Th width="22%">Reward { appConfig.network.currency.symbol }</Th>
<Th width="22%">Reward { appConfig.network.currency }</Th> <Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th>
<Th width="22%">Burnt fees { appConfig.network.currency }</Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> <AnimatePresence initial={ false }>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item }/>) } { /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
</Tbody> { data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement/>) }
</Table> </AnimatePresence>
</TableContainer> </Tbody>
</Table>
); );
}; };
......
import { Tr, Td, Text, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Tr, Td, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward'; import { WEI, ZERO } from 'lib/consts';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
...@@ -16,15 +16,27 @@ import Utilization from 'ui/shared/Utilization'; ...@@ -16,15 +16,27 @@ import Utilization from 'ui/shared/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isPending?: boolean;
enableTimeIncrement?: boolean;
} }
const BlocksTableItem = ({ data, isPending }: Props) => { const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const { totalReward, burntFees, txFees } = getBlockReward(data); const totalReward = data.rewards
?.map(({ reward }) => BigNumber(reward))
.reduce((result, item) => result.plus(item), ZERO) || ZERO;
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
return ( return (
<Tr> <Tr
as={ motion.tr }
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
key={ data.height }
>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> } { isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations"> <Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<Link <Link
...@@ -35,7 +47,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => { ...@@ -35,7 +47,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => {
</Link> </Link>
</Tooltip> </Tooltip>
</Flex> </Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td> <Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm"> <Td fontSize="sm">
......
...@@ -13,6 +13,7 @@ import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form'; ...@@ -13,6 +13,7 @@ import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
...@@ -52,10 +53,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -52,10 +53,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi }; const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi };
if (!data.id) { if (!data.id) {
return fetch<CustomAbi, CustomAbiErrors>('/api/account/custom-abis', { method: 'POST', body }); return fetch<CustomAbi, CustomAbiErrors>('/node-api/account/custom-abis', { method: 'POST', body });
} }
return fetch<CustomAbi, CustomAbiErrors>(`/api/account/custom-abis/${ data.id }`, { method: 'PUT', body }); return fetch<CustomAbi, CustomAbiErrors>(`/node-api/account/custom-abis/${ data.id }`, { method: 'PUT', body });
}; };
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
...@@ -63,7 +64,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -63,7 +64,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const mutation = useMutation(customAbiKey, { const mutation = useMutation(customAbiKey, {
onSuccess: (data) => { onSuccess: (data) => {
const response = data as unknown as CustomAbi; const response = data as unknown as CustomAbi;
queryClient.setQueryData([ 'custom-abis' ], (prevData: CustomAbis | undefined) => { queryClient.setQueryData([ QueryKeys.customAbis ], (prevData: CustomAbis | undefined) => {
const isExisting = prevData && prevData.some((item) => item.id === response.id); const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) { if (isExisting) {
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,26 +19,24 @@ interface Props { ...@@ -20,26 +19,24 @@ interface Props {
const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => { const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return ( return (
<TableContainer width="100%"> <Table variant="simple" minWidth="600px">
<Table variant="simple" minWidth="600px"> <Thead>
<Thead> <Tr>
<Tr> <Th>ABI for Smart contract address (0x...)</Th>
<Th>ABI for Smart contract address (0x...)</Th> <Th width="108px"></Th>
<Th width="108px"></Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item) => (
{ data.map((item) => ( <CustomAbiTableItem
<CustomAbiTableItem item={ item }
item={ item } key={ item.id }
key={ item.id } onDeleteClick={ onDeleteClick }
onDeleteClick={ onDeleteClick } onEditClick={ onEditClick }
onEditClick={ onEditClick } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account'; import type { CustomAbi, CustomAbis } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -17,11 +18,11 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -17,11 +18,11 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/custom-abis/${ data.id }`, { method: 'DELETE' }); return fetch(`/node-api/account/custom-abis/${ data.id }`, { method: 'DELETE' });
}, [ data ]); }, [ data ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'custom-abis' ], (prevData: CustomAbis | undefined) => { queryClient.setQueryData([ QueryKeys.customAbis ], (prevData: CustomAbis | undefined) => {
return prevData?.filter((item) => item.id !== data.id); return prevData?.filter((item) => item.id !== data.id);
}); });
}, [ data, queryClient ]); }, [ data, queryClient ]);
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -30,7 +31,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -30,7 +31,7 @@ const ApiKeysPage: React.FC = () => {
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isLoading, isError } = useQuery<unknown, unknown, ApiKeys>([ 'api-keys' ], async() => await fetch('/api/account/api-keys')); const { data, isLoading, isError } = useQuery<unknown, unknown, ApiKeys>([ QueryKeys.apiKeys ], async() => await fetch('/node-api/account/api-keys'));
const onEditClick = useCallback((data: ApiKey) => { const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data); setApiKeyModalData(data);
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
import config from 'configs/app/config'; import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import useFetch from 'lib/hooks/useFetch';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton'; import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu'; import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
import useMarketplaceApps from '../apps/useMarkeplaceApps'; import useMarketplaceApps from '../apps/useMarketplaceApps';
const Apps = () => { const Apps = () => {
const fetch = useFetch();
const { const {
isLoading, isLoading,
category, category,
...@@ -24,6 +30,11 @@ const Apps = () => { ...@@ -24,6 +30,11 @@ const Apps = () => {
handleFavoriteClick, handleFavoriteClick,
} = useMarketplaceApps(); } = useMarketplaceApps();
useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ],
async() => await fetch(`/node-api/config/json-rpc-url`),
);
return ( return (
<> <>
<Box <Box
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account'; import type { CustomAbi, CustomAbis } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -27,7 +28,8 @@ const CustomAbiPage: React.FC = () => { ...@@ -27,7 +28,8 @@ const CustomAbiPage: React.FC = () => {
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isLoading, isError } = useQuery<unknown, unknown, CustomAbis>([ 'custom-abis' ], async() => await fetch('/api/account/custom-abis')); const { data, isLoading, isError } = useQuery<unknown, unknown, CustomAbis>([ QueryKeys.customAbis ], async() =>
await fetch('/node-api/account/custom-abis'));
const onEditClick = useCallback((data: CustomAbi) => { const onEditClick = useCallback((data: CustomAbi) => {
setCustomAbiModalData(data); setCustomAbiModalData(data);
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react'; import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -11,7 +10,6 @@ import Page from 'ui/shared/Page/Page'; ...@@ -11,7 +10,6 @@ import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => { const Home = () => {
const router = useRouter();
const toast = useToast(); const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false); const [ isFormVisible, setFormVisibility ] = React.useState(false);
...@@ -46,7 +44,7 @@ const Home = () => { ...@@ -46,7 +44,7 @@ const Home = () => {
}); });
}, [ toast, token ]); }, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString(); const prodUrl = 'https://blockscout.com/poa/core';
return ( return (
<Page> <Page>
......
import { Box, Center, useColorMode } from '@chakra-ui/react'; import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch';
import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -14,32 +18,51 @@ type Props = { ...@@ -14,32 +18,51 @@ type Props = {
const MarketplaceApp = ({ app, isLoading }: Props) => { const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading); const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const fetch = useFetch();
const ref = useRef<HTMLIFrameElement>(null);
const handleIframeLoad = useCallback(() => { const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false); setIsFrameLoading(false);
}, []); }, []);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' + const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
'allow-pointer-lock allow-popups-to-escape-sandbox ' + [ 'json-rpc-url' ],
'allow-same-origin allow-scripts ' + async() => await fetch(`/node-api/config/json-rpc-url`),
'allow-top-navigation-by-user-activation allow-popups'; { refetchOnMount: false },
);
const allowAttributeValue = 'clipboard-read; clipboard-write;';
useEffect(() => { useEffect(() => {
if (app && !isFrameLoading) { if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: Number(appConfig.network.id) }, app.url); const message = {
blockscoutColorMode: colorMode,
blockscoutRootUrl: link('network_index'),
blockscoutAddressExplorerUrl: link('address_index'),
blockscoutTransactionExplorerUrl: link('tx'),
blockscoutNetworkName: appConfig.network.name,
blockscoutNetworkId: Number(appConfig.network.id),
blockscoutNetworkCurrency: appConfig.network.currency,
blockscoutNetworkRpc: jsonRpcUrlResponse?.json_rpc_url,
};
ref?.current?.contentWindow?.postMessage(message, app.url);
} }
}, [ isFrameLoading, app, colorMode, ref ]); }, [ isFrameLoading, app, colorMode, ref, jsonRpcUrlResponse ]);
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;';
return ( return (
<Page wrapChildren={ false }> <Page wrapChildren={ false }>
<Center <Center
as="main" as="main"
h="100vh" h="100vh"
paddingTop={{ base: '138px', lg: 0 }} pt={{ base: '138px', lg: 0 }}
pb={{ base: 0, lg: 10 }}
> >
{ (isFrameLoading) && ( { (isFrameLoading) && (
<ContentLoader/> <ContentLoader/>
......
...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => { const MyProfile = () => {
const { data, isLoading, isError } = useFetchProfileInfo(); const { data, isLoading, isError, isFetched } = useFetchProfileInfo();
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
...@@ -22,7 +22,7 @@ const MyProfile = () => { ...@@ -22,7 +22,7 @@ const MyProfile = () => {
return ( return (
<VStack maxW="412px" mt={ 12 } gap={ 5 } alignItems="stretch"> <VStack maxW="412px" mt={ 12 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data }/> <UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/>
<FormControl variant="floating" id="name" isRequired size="lg"> <FormControl variant="floating" id="name" isRequired size="lg">
<Input <Input
required required
......
import { Flex, Link, Icon } from '@chakra-ui/react'; import { Flex, Link, Icon, Tag } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import link from 'lib/link/link'; import useFetch from 'lib/hooks/useFetch';
import isBrowser from 'lib/isBrowser';
import networkExplorers from 'lib/networks/networkExplorers';
import ExternalLink from 'ui/shared/ExternalLink'; import ExternalLink from 'ui/shared/ExternalLink';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -25,28 +30,50 @@ const TABS: Array<RoutedTab> = [ ...@@ -25,28 +30,50 @@ const TABS: Array<RoutedTab> = [
]; ];
const TransactionPageContent = () => { const TransactionPageContent = () => {
const router = useRouter();
const fetch = useFetch();
const { data } = useQuery<unknown, unknown, Transaction>(
[ 'tx', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths.tx)
.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
return <ExternalLink key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>;
});
const hasGoBackLink = isBrowser() && window.document.referrer.includes('/txs');
return ( return (
<Page> <Page>
{ /* TODO should be shown only when navigating from txs list */ } { hasGoBackLink && (
<Link mb={ 6 } display="inline-flex" href={ link('txs') }> <Link mb={ 6 } display="inline-flex" href={ window.document.referrer }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/> <Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions Transactions
</Link> </Link>
) }
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}> <Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<PageTitle text="Transaction details"/> <PageTitle text="Transaction details"/>
<Flex { data?.tx_tag && <Tag my={ 2 } ml={ 3 }>{ data.tx_tag }</Tag> }
alignItems="center" { explorersLinks.length > 0 && (
flexWrap="wrap" <Flex
columnGap={ 6 } alignItems="center"
rowGap={ 3 } flexWrap="wrap"
ml={{ base: 'initial', lg: 'auto' }} columnGap={ 6 }
mb={{ base: 6, lg: 'initial' }} rowGap={ 3 }
py={ 2.5 } ml={{ base: 'initial', lg: 'auto' }}
> mb={{ base: 6, lg: 'initial' }}
<ExternalLink title="Open in Tenderly" href="#"/> py={ 2.5 }
<ExternalLink title="Open in Blockchair" href="#"/> >
<ExternalLink title="Open in Etherscan" href="#"/> { explorersLinks }
</Flex> </Flex>
) }
</Flex> </Flex>
<RoutedTabs <RoutedTabs
tabs={ TABS } tabs={ TABS }
......
...@@ -5,21 +5,21 @@ import React from 'react'; ...@@ -5,21 +5,21 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsPending from 'ui/txs/TxsPending'; import TxsTab from 'ui/txs/TxsTab';
import TxsValidated from 'ui/txs/TxsValidated';
const TABS: Array<RoutedTab> = [
{ id: 'validated', title: 'Validated', component: <TxsValidated/> },
{ id: 'pending', title: 'Pending', component: <TxsPending/> },
];
const Transactions = () => { const Transactions = () => {
const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined';
const TABS: Array<RoutedTab> = [
{ id: 'validated', title: verifiedTitle, component: <TxsTab tab="validated"/> },
{ id: 'pending', title: 'Pending', component: <TxsTab tab="pending"/> },
];
return ( return (
<Page> <Page hideMobileHeaderOnScrollDown>
<Box h="100%"> <Box h="100%">
<PageTitle text="Transactions"/> <PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS }/>
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account'; import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -19,7 +20,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; ...@@ -19,7 +20,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const { data, isLoading, isError } = const { data, isLoading, isError } =
useQuery<unknown, unknown, TWatchlist>([ 'watchlist' ], async() => fetch('/api/account/watchlist/get-with-tokens')); useQuery<unknown, unknown, TWatchlist>([ QueryKeys.watchlist ], async() => fetch('/node-api/account/watchlist/get-with-tokens'));
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
......
...@@ -9,6 +9,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; ...@@ -9,6 +9,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account'; import type { AddressTag, AddressTagErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
...@@ -53,10 +54,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -53,10 +54,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { if (isEdit) {
return fetch(`/api/account/private-tags/address/${ data.id }`, { method: 'PUT', body }); return fetch(`/node-api/account/private-tags/address/${ data.id }`, { method: 'PUT', body });
} }
return fetch('/api/account/private-tags/address', { method: 'POST', body }); return fetch('/node-api/account/private-tags/address', { method: 'POST', body });
}, { }, {
onError: (e: ErrorType<AddressTagErrors>) => { onError: (e: ErrorType<AddressTagErrors>) => {
setPending(false); setPending(false);
...@@ -70,7 +71,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -70,7 +71,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
} }
}, },
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries([ 'address-tags' ]).then(() => { queryClient.refetchQueries([ QueryKeys.addressTags ]).then(() => {
onClose(); onClose();
setPending(false); setPending(false);
}); });
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,27 +19,25 @@ interface Props { ...@@ -20,27 +19,25 @@ interface Props {
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return ( return (
<TableContainer width="100%"> <Table variant="simple" minWidth="600px">
<Table variant="simple" minWidth="600px"> <Thead>
<Thead> <Tr>
<Tr> <Th width="60%">Address</Th>
<Th width="60%">Address</Th> <Th width="40%">Private tag</Th>
<Th width="40%">Private tag</Th> <Th width="108px"></Th>
<Th width="108px"></Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item: AddressTag) => (
{ data.map((item: AddressTag) => ( <AddressTagTableItem
<AddressTagTableItem item={ item }
item={ item } key={ item.id }
key={ item.id } onDeleteClick={ onDeleteClick }
onDeleteClick={ onDeleteClick } onEditClick={ onEditClick }
onEditClick={ onEditClick } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account'; import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -22,16 +23,16 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type }) ...@@ -22,16 +23,16 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const fetch = useFetch(); const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' }); return fetch(`/node-api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' });
}, [ fetch, type, id ]); }, [ fetch, type, id ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
if (type === 'address') { if (type === 'address') {
queryClient.setQueryData([ 'address-tags' ], (prevData: AddressTags | undefined) => { queryClient.setQueryData([ QueryKeys.addressTags ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id); return prevData?.filter((item: AddressTag) => item.id !== id);
}); });
} else { } else {
queryClient.setQueryData([ 'transaction-tags' ], (prevData: TransactionTags | undefined) => { queryClient.setQueryData([ QueryKeys.transactionTags ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id); return prevData?.filter((item: TransactionTag) => item.id !== id);
}); });
} }
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account'; import type { AddressTags, AddressTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -18,7 +19,7 @@ import DeletePrivateTagModal from './DeletePrivateTagModal'; ...@@ -18,7 +19,7 @@ import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => { const PrivateAddressTags = () => {
const { data: addressTagsData, isLoading, isError } = const { data: addressTagsData, isLoading, isError } =
useQuery<unknown, unknown, AddressTags>([ 'address-tags' ], async() => fetch('/api/account/private-tags/address'), { refetchOnMount: false }); useQuery<unknown, unknown, AddressTags>([ QueryKeys.addressTags ], async() => fetch('/node-api/account/private-tags/address'), { refetchOnMount: false });
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account'; import type { TransactionTags, TransactionTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -18,7 +19,10 @@ import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; ...@@ -18,7 +19,10 @@ import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => { const PrivateTransactionTags = () => {
const { data: transactionTagsData, isLoading, isError } = const { data: transactionTagsData, isLoading, isError } =
useQuery<unknown, unknown, TransactionTags>([ 'transaction-tags' ], async() => fetch('/api/account/private-tags/transaction'), { refetchOnMount: false }); useQuery<unknown, unknown, TransactionTags>(
[ QueryKeys.transactionTags ],
async() => fetch('/node-api/account/private-tags/transaction'), { refetchOnMount: false },
);
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
......
...@@ -9,6 +9,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; ...@@ -9,6 +9,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account'; import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
...@@ -53,10 +54,10 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -53,10 +54,10 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { if (isEdit) {
return fetch(`/api/account/private-tags/transaction/${ data.id }`, { method: 'PUT', body }); return fetch(`/node-api/account/private-tags/transaction/${ data.id }`, { method: 'PUT', body });
} }
return fetch('/api/account/private-tags/transaction', { method: 'POST', body }); return fetch('/node-api/account/private-tags/transaction', { method: 'POST', body });
}, { }, {
onError: (e: ErrorType<TransactionTagErrors>) => { onError: (e: ErrorType<TransactionTagErrors>) => {
setPending(false); setPending(false);
...@@ -70,7 +71,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -70,7 +71,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
} }
}, },
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries([ 'transaction-tags' ]).then(() => { queryClient.refetchQueries([ QueryKeys.transactionTags ]).then(() => {
onClose(); onClose();
setPending(false); setPending(false);
}); });
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,27 +19,25 @@ interface Props { ...@@ -20,27 +19,25 @@ interface Props {
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return ( return (
<TableContainer width="100%"> <Table variant="simple" minWidth="600px">
<Table variant="simple" minWidth="600px"> <Thead>
<Thead> <Tr>
<Tr> <Th width="75%">Transaction</Th>
<Th width="75%">Transaction</Th> <Th width="25%">Private tag</Th>
<Th width="25%">Private tag</Th> <Th width="108px"></Th>
<Th width="108px"></Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item) => (
{ data.map((item) => ( <TransactionTagTableItem
<TransactionTagTableItem item={ item }
item={ item } key={ item.id }
key={ item.id } onDeleteClick={ onDeleteClick }
onDeleteClick={ onDeleteClick } onEditClick={ onEditClick }
onEditClick={ onEditClick } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -27,12 +28,12 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -27,12 +28,12 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const deleteApiKey = useCallback(() => { const deleteApiKey = useCallback(() => {
const body = { remove_reason: reason }; const body = { remove_reason: reason };
return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body }); return fetch(`/node-api/account/public-tags/${ data.id }`, { method: 'DELETE', body });
}, [ data.id, fetch, reason ]); }, [ data.id, fetch, reason ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
onDeleteSuccess(); onDeleteSuccess();
queryClient.setQueryData([ 'public-tags' ], (prevData: PublicTags | undefined) => { queryClient.setQueryData([ QueryKeys.publicTags ], (prevData: PublicTags | undefined) => {
return prevData?.filter((item) => item.id !== data.id); return prevData?.filter((item) => item.id !== data.id);
}); });
}, [ queryClient, data, onDeleteSuccess ]); }, [ queryClient, data, onDeleteSuccess ]);
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,28 +19,26 @@ interface Props { ...@@ -20,28 +19,26 @@ interface Props {
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
return ( return (
<TableContainer width="100%"> <Table variant="simple" minWidth="600px">
<Table variant="simple" minWidth="600px"> <Thead>
<Thead> <Tr>
<Tr> <Th width="50%">Smart contract / Address (0x...)</Th>
<Th width="50%">Smart contract / Address (0x...)</Th> <Th width="25%">Public tag</Th>
<Th width="25%">Public tag</Th> <Th width="25%">Request status</Th>
<Th width="25%">Request status</Th> <Th width="108px"></Th>
<Th width="108px"></Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item) => (
{ data.map((item) => ( <PublicTagTableItem
<PublicTagTableItem item={ item }
item={ item } key={ item.id }
key={ item.id } onDeleteClick={ onDeleteClick }
onDeleteClick={ onDeleteClick } onEditClick={ onEditClick }
onEditClick={ onEditClick } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -26,7 +27,8 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -26,7 +27,8 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags')); const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ QueryKeys.publicTags ], async() =>
await fetch('/node-api/account/public-tags'));
const onDeleteModalClose = useCallback(() => { const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined); setDeleteModalData(undefined);
......
...@@ -12,6 +12,7 @@ import type { FieldError, Path, SubmitHandler } from 'react-hook-form'; ...@@ -12,6 +12,7 @@ import type { FieldError, Path, SubmitHandler } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account'; import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
...@@ -98,17 +99,17 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -98,17 +99,17 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
}; };
if (!data?.id) { if (!data?.id) {
return fetch<PublicTag, PublicTagErrors>('/api/account/public-tags', { method: 'POST', body }); return fetch<PublicTag, PublicTagErrors>('/node-api/account/public-tags', { method: 'POST', body });
} }
return fetch<PublicTag, PublicTagErrors>(`/api/account/public-tags/${ data.id }`, { method: 'PUT', body }); return fetch<PublicTag, PublicTagErrors>(`/node-api/account/public-tags/${ data.id }`, { method: 'PUT', body });
}; };
const mutation = useMutation(updatePublicTag, { const mutation = useMutation(updatePublicTag, {
onSuccess: async(data) => { onSuccess: async(data) => {
const response = data as unknown as PublicTag; const response = data as unknown as PublicTag;
queryClient.setQueryData([ 'public-tags' ], (prevData: PublicTags | undefined) => { queryClient.setQueryData([ QueryKeys.publicTags ], (prevData: PublicTags | undefined) => {
const isExisting = prevData && prevData.some((item) => item.id === response.id); const isExisting = prevData && prevData.some((item) => item.id === response.id);
if (isExisting) { if (isExisting) {
......
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react'; import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react'; import React from 'react';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
key?: string;
} }
const AccountListItemMobile = ({ children, className }: Props) => { const AccountListItemMobile = ({ children, className, key }: Props) => {
return ( return (
<Flex <Flex
as={ motion.div }
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
key={ key }
rowGap={ 6 } rowGap={ 6 }
alignItems="flex-start" alignItems="flex-start"
flexDirection="column" flexDirection="column"
......
...@@ -2,22 +2,18 @@ import { Box, Text, chakra } from '@chakra-ui/react'; ...@@ -2,22 +2,18 @@ import { Box, Text, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { Unit } from 'types/unit';
import getValueWithUnit from 'lib/getValueWithUnit';
interface Props { interface Props {
value: string; value: string;
unit?: Unit;
currency?: string; currency?: string;
exchangeRate?: string | null; exchangeRate?: string | null;
className?: string; className?: string;
accuracy?: number; accuracy?: number;
accuracyUsd?: number; accuracyUsd?: number;
decimals?: string | null;
} }
const CurrencyValue = ({ value, currency = '', unit, exchangeRate, className, accuracy, accuracyUsd }: Props) => { const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
const valueCurr = getValueWithUnit(value, unit); const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18')));
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat(); const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdContent; let usdContent;
......
...@@ -2,7 +2,12 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,12 @@ import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
...@@ -10,25 +15,36 @@ import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; ...@@ -10,25 +15,36 @@ import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
wrapChildren?: boolean; wrapChildren?: boolean;
hideMobileHeaderOnScrollDown?: boolean;
} }
const Page = ({ children, wrapChildren = true }: Props) => { const Page = ({
children,
wrapChildren = true,
hideMobileHeaderOnScrollDown,
}: Props) => {
const fetch = useFetch(); const fetch = useFetch();
useQuery<unknown, unknown, unknown>([ 'csrf' ], async() => await fetch('/api/account/csrf')); useQuery<unknown, unknown, unknown>([ QueryKeys.csrf ], async() => await fetch('/node-api/account/csrf'), {
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
const directionContext = useScrollDirection();
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
<PageContent>{ children }</PageContent> <PageContent>{ children }</PageContent>
) : children; ) : children;
return ( return (
<Flex w="100%" minH="100vh" alignItems="stretch"> <ScrollDirectionContext.Provider value={ directionContext }>
<NavigationDesktop/> <Flex w="100%" minH="100vh" alignItems="stretch">
<Flex flexDir="column" width="100%"> <NavigationDesktop/>
<Header/> <Flex flexDir="column" width="100%">
{ renderedChildren } <Header hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
{ renderedChildren }
</Flex>
</Flex> </Flex>
</Flex> </ScrollDirectionContext.Provider>
); );
}; };
......
import { Button, Flex, Input, Icon, IconButton } from '@chakra-ui/react'; import { Button, Flex, Icon, IconButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
type Props = { export type Props = {
currentPage: number; page: number;
maxPage?: number; onNextPageClick: () => void;
onPrevPageClick: () => void;
resetPage: () => void;
hasNextPage: boolean;
hasPaginationParams?: boolean;
} }
const MAX_PAGE_DEFAULT = 50; const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams }: Props) => {
const Pagination = ({ currentPage, maxPage }: Props) => { return (
const pageNumber = ( <Flex
<Flex alignItems="center"> fontSize="sm"
alignItems="center"
>
<Button <Button
variant="outline" variant="outline"
colorScheme="gray"
size="sm" size="sm"
isActive onClick={ resetPage }
borderWidth="1px" disabled={ !hasPaginationParams }
fontWeight={ 400 } mr={ 4 }
mr={ 3 }
h={ 8 }
> >
{ currentPage } First
</Button> </Button>
of <IconButton
variant="outline"
onClick={ onPrevPageClick }
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 6 }
disabled={ page === 1 }
/>
<Button <Button
variant="outline" variant="outline"
colorScheme="gray"
size="sm" size="sm"
width={ 8 } isActive
borderWidth="1px" borderWidth="1px"
fontWeight={ 400 } fontWeight={ 400 }
ml={ 3 } h={ 8 }
cursor="unset"
disabled={ hasPaginationParams && page === 1 }
> >
{ maxPage || MAX_PAGE_DEFAULT } { page }
</Button> </Button>
</Flex> <IconButton
); variant="outline"
onClick={ onNextPageClick }
return ( size="sm"
<Flex aria-label="Next page"
fontSize="sm" w="36px"
width={{ base: '100%', lg: 'auto' }} icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
justifyContent={{ base: 'space-between', lg: 'unset' }} ml={ 6 }
alignItems="center" disabled={ !hasNextPage }
> />
<Flex alignItems="center" justifyContent="space-between" w={{ base: '100%', lg: 'auto' }}> { /* not implemented yet */ }
<IconButton { /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
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 }/> Go to <Input w="84px" size="xs" ml={ 2 }/>
</Flex> </Flex> */ }
</Flex> </Flex>
); );
......
...@@ -49,8 +49,11 @@ const RoutedTabs = ({ tabs }: Props) => { ...@@ -49,8 +49,11 @@ const RoutedTabs = ({ tabs }: Props) => {
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; const nextTab = tabs[index];
router.query.tab = nextTab.id; router.push(
router.push(router); { pathname: router.asPath.split('?')[0], query: { tab: nextTab.id } },
undefined,
{ shallow: true },
);
}, [ tabs, router ]); }, [ tabs, router ]);
return ( return (
......
...@@ -14,7 +14,7 @@ import { menuButton } from './utils'; ...@@ -14,7 +14,7 @@ import { menuButton } from './utils';
interface Props { interface Props {
tabs: Array<RoutedTab | MenuButton>; tabs: Array<RoutedTab | MenuButton>;
activeTab: RoutedTab; activeTab?: RoutedTab;
tabsCut: number; tabsCut: number;
isActive: boolean; isActive: boolean;
styles?: StyleProps; styles?: StyleProps;
...@@ -52,7 +52,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe ...@@ -52,7 +52,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
key={ tab.id } key={ tab.id }
variant="ghost" variant="ghost"
onClick={ handleItemClick } onClick={ handleItemClick }
isActive={ activeTab.id === tab.id } isActive={ activeTab ? activeTab.id === tab.id : false }
justifyContent="left" justifyContent="left"
data-index={ index } data-index={ index }
> >
......
...@@ -4,12 +4,12 @@ import React from 'react'; ...@@ -4,12 +4,12 @@ import React from 'react';
import upDownArrow from 'icons/arrows/up-down.svg'; import upDownArrow from 'icons/arrows/up-down.svg';
type Props = { type Props = {
handleSort: () => void; onClick: () => void;
isSortActive: boolean; isActive: boolean;
className?: string; className?: string;
} }
const SortButton = ({ handleSort, isSortActive, className }: Props) => { const SortButton = ({ onClick, isActive, className }: Props) => {
return ( return (
<IconButton <IconButton
icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> } icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> }
...@@ -18,8 +18,9 @@ const SortButton = ({ handleSort, isSortActive, className }: Props) => { ...@@ -18,8 +18,9 @@ const SortButton = ({ handleSort, isSortActive, className }: Props) => {
variant="outline" variant="outline"
colorScheme="gray-dark" colorScheme="gray-dark"
minWidth="36px" minWidth="36px"
onClick={ handleSort } onClick={ onClick }
isActive={ isSortActive } isActive={ isActive }
display="flex"
className={ className } className={ className }
/> />
); );
......
import { Thead, useColorModeValue } from '@chakra-ui/react';
import type { TableHeadProps, PositionProps } from '@chakra-ui/react';
import throttle from 'lodash/throttle';
import React from 'react';
interface Props extends TableHeadProps {
top?: number;
children?: React.ReactNode;
}
const TheadSticky = ({ top, children, ...restProps }: Props) => {
const ref = React.useRef<HTMLTableSectionElement>(null);
const [ isSticky, setIsSticky ] = React.useState(false);
const handleScroll = React.useCallback(() => {
if (Number(ref.current?.getBoundingClientRect().y) <= (top || 0)) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ top ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
}, [ handleScroll ]);
const props = {
...restProps,
position: 'sticky' as PositionProps['position'],
top: `${ top }px` || 0,
backgroundColor: useColorModeValue('white', 'black'),
boxShadow: isSticky ? 'md' : 'none',
zIndex: 1,
};
return (
<Thead { ...props } ref={ ref }>
{ children }
</Thead>
);
};
export default TheadSticky;
...@@ -7,18 +7,18 @@ const EmptyElement = () => null; ...@@ -7,18 +7,18 @@ const EmptyElement = () => null;
interface Props { interface Props {
hash: string; hash: string;
name?: string; name?: string | null;
className?: string; className?: string;
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = ` const logoSrc = appConfig.network.assetsPathname ? `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
${ appConfig.network.assetsPathname || appConfig.network.type } ${ appConfig.network.assetsPathname }
/assets/ /assets/
${ hash } ${ hash }
/logo.png /logo.png
`; ` : undefined;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>; return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>;
}; };
......
...@@ -5,9 +5,9 @@ import link from 'lib/link/link'; ...@@ -5,9 +5,9 @@ import link from 'lib/link/link';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
symbol: string; symbol?: string | null;
hash: string; hash: string;
name: string; name?: string | null;
className?: string; className?: string;
} }
...@@ -20,7 +20,7 @@ const TokenSnippet = ({ symbol, hash, name, className }: Props) => { ...@@ -20,7 +20,7 @@ const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
<Link href={ url } target="_blank"> <Link href={ url } target="_blank">
{ name } { name }
</Link> </Link>
<Text variant="secondary">({ symbol })</Text> { symbol && <Text variant="secondary">({ symbol })</Text> }
</Center> </Center>
); );
}; };
......
import { useColorModeValue, chakra, Image } from '@chakra-ui/react'; import { useColorModeValue, chakra, SkeletonCircle, Image } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Identicon from 'react-identicons'; import Identicon from 'react-identicons';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
const ProfileIcon = chakra(Identicon); const ProfileIcon = chakra(Identicon);
interface Props { interface Props {
size: number; size: number;
data?: UserInfo; data?: UserInfo;
isFetched: boolean;
} }
const UserAvatar = ({ size, data }: Props) => { const UserAvatar = ({ size, data, isFetched }: Props) => {
const appProps = useAppContext();
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies));
const sizeString = `${ size }px`; const sizeString = `${ size }px`;
const bgColor = useColorModeValue('blackAlpha.100', 'white'); const bgColor = useColorModeValue('blackAlpha.100', 'white');
if (hasAuth && !isFetched) {
return <SkeletonCircle h={ sizeString } w={ sizeString }/>;
}
if (data?.avatar) { if (data?.avatar) {
return ( return (
<Image <Image
flexShrink={ 0 } flexShrink={ 0 }
src={ data.avatar } src={ data.avatar }
alt={ `Profile picture of ${ data.name || data.nickname || '' }` } alt={ `Profile picture of ${ data?.name || data?.nickname || '' }` }
w={ sizeString } w={ sizeString }
minW={ sizeString } minW={ sizeString }
h={ sizeString } h={ sizeString }
......
...@@ -25,4 +25,4 @@ const Utilization = ({ className, value, colorScheme = 'green' }: Props) => { ...@@ -25,4 +25,4 @@ const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
); );
}; };
export default chakra(Utilization); export default React.memo(chakra(Utilization));
import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react'; import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react'; import React from 'react';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -7,15 +8,16 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -7,15 +8,16 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
type?: 'address' | 'transaction' | 'token' | 'block'; type?: 'address' | 'transaction' | 'token' | 'block';
alias?: string; alias?: string | null;
className?: string; className?: string;
hash: string; hash: string;
truncation?: 'constant' | 'dynamic'| 'none'; truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string; fontWeight?: string;
id?: string; id?: string;
target?: HTMLAttributeAnchorTarget;
} }
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight }: Props) => { const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target }: Props) => {
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = link('tx', { id: id || hash }); url = link('tx', { id: id || hash });
...@@ -49,7 +51,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -49,7 +51,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
<Link <Link
className={ className } className={ className }
href={ url } href={ url }
target="_blank" target={ target || '_blank' }
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
> >
......
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
...@@ -9,46 +10,53 @@ import SearchBar from 'ui/snippets/searchBar/SearchBar'; ...@@ -9,46 +10,53 @@ import SearchBar from 'ui/snippets/searchBar/SearchBar';
import Burger from './Burger'; import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler'; import ColorModeToggler from './ColorModeToggler';
const Header = () => { const Header = ({ hideOnScrollDown }: {hideOnScrollDown?: boolean}) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
return ( return (
<> <ScrollDirectionContext.Consumer>
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}> { (scrollDirection) => (
<Flex <>
as="header" <Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
position="fixed" <Flex
top={ 0 } as="header"
left={ 0 } position="fixed"
paddingX={ 4 } top={ 0 }
paddingY={ 2 } left={ 0 }
bgColor={ bgColor } paddingX={ 4 }
width="100%" paddingY={ 2 }
alignItems="center" bgColor={ bgColor }
justifyContent="space-between" width="100%"
zIndex="sticky" alignItems="center"
> justifyContent="space-between"
<Burger/> zIndex="sticky2"
<NetworkLogo/> transitionProperty="box-shadow"
<ProfileMenuMobile/> transitionDuration="slow"
</Flex> boxShadow={ !hideOnScrollDown && scrollDirection === 'down' ? 'md' : 'none' }
<SearchBar/> >
</Box> <Burger/>
<HStack <NetworkLogo/>
as="header" <ProfileMenuMobile/>
width="100%" </Flex>
alignItems="center" <SearchBar withShadow={ !hideOnScrollDown }/>
justifyContent="center" </Box><HStack
gap={ 12 } as="header"
display={{ base: 'none', lg: 'flex' }} width="100%"
paddingX={ 12 } alignItems="center"
paddingTop={ 9 } justifyContent="center"
paddingBottom="52px" gap={ 12 }
> display={{ base: 'none', lg: 'flex' }}
<SearchBar/> paddingX={ 12 }
<ColorModeToggler/> paddingTop={ 9 }
<ProfileMenuDesktop/> paddingBottom="52px"
</HStack> >
</> <SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
</>
) }
</ScrollDirectionContext.Consumer>
); );
}; };
......
...@@ -13,51 +13,61 @@ interface Props { ...@@ -13,51 +13,61 @@ interface Props {
text: string; text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>; icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
px?: string | number; px?: string | number;
isNewUi: boolean;
} }
const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => { const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props) => {
const colors = useColors(); const colors = useColors();
const isExpanded = isCollapsed === false; const isExpanded = isCollapsed === false;
const content = (
<Link
{ ...(isNewUi ? {} : { href: url }) }
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 }
bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base"
whiteSpace="nowrap"
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Tooltip
label={ text }
hasArrow={ false }
isDisabled={ !isCollapsed }
placement="right"
variant="nav"
gutter={ 15 }
color={ isActive ? colors.text.active : colors.text.hover }
>
<HStack spacing={ 3 }>
<Icon as={ icon } boxSize="30px"/>
<Text
variant="inherit"
fontSize="sm"
lineHeight="20px"
display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}
>
{ text }
</Text>
</HStack>
</Tooltip>
</Link>
);
return ( return (
<Box as="li" listStyleType="none" w="100%"> <Box as="li" listStyleType="none" w="100%">
<NextLink href={ url } passHref> { /* why not NextLink in all cases? since prev UI and new one are hosting in the same domain and global routing is managed by nginx */ }
<Link { /* we have to hard reload page on every transition between urls from different part of the app */ }
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }} { isNewUi ? (
px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } } <NextLink href={ url } passHref>
py={ 2.5 } { content }
display="flex" </NextLink>
color={ isActive ? colors.text.active : colors.text.default } ) : content }
bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base"
whiteSpace="nowrap"
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Tooltip
label={ text }
hasArrow={ false }
isDisabled={ !isCollapsed }
placement="right"
variant="nav"
gutter={ 15 }
color={ isActive ? colors.text.active : colors.text.hover }
>
<HStack spacing={ 3 }>
<Icon as={ icon } boxSize="30px"/>
<Text
variant="inherit"
fontSize="sm"
lineHeight="20px"
display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}
>
{ text }
</Text>
</HStack>
</Tooltip>
</Link>
</NextLink>
</Box> </Box>
); );
}; };
......
...@@ -3,9 +3,9 @@ import React from 'react'; ...@@ -3,9 +3,9 @@ import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu'; import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
...@@ -14,25 +14,24 @@ import NavFooter from './NavFooter'; ...@@ -14,25 +14,24 @@ import NavFooter from './NavFooter';
import NavLink from './NavLink'; import NavLink from './NavLink';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const appProps = useAppContext();
const cookiesString = appProps.cookies;
const isNavBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED, cookiesString);
let isNavBarCollapsed;
if (isNavBarCollapsedCookie === 'true') {
isNavBarCollapsed = true;
}
if (isNavBarCollapsedCookie === 'false') {
isNavBarCollapsed = false;
}
const isInBrowser = isBrowser(); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
const [ hasAccount, setHasAccount ] = React.useState(false);
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(); const { mainNavItems, accountNavItems } = useNavItems();
React.useEffect(() => { const hasAccount = hasAuth && appConfig.isAccountSupported;
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED); const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(isNavBarCollapsed);
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));
}
}, [ isInBrowser ]);
const handleTogglerClick = React.useCallback(() => { const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag); setCollapsedState((flag) => !flag);
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react'; import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react'; import type { FunctionComponent, SVGAttributes } from 'react';
import type { PreDefinedNetwork } from 'types/networks';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockscoutLogo from 'icons/logo.svg'; import blockscoutLogo from 'icons/logo.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
// predefined network logos // predefined network logos
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = { const LOGOS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = {
'/xdai/mainnet': require('icons/networks/logos/gnosis.svg'), xdai_mainnet: require('icons/networks/logos/gnosis.svg'),
'/eth/mainnet': require('icons/networks/logos/eth.svg'), eth_mainnet: require('icons/networks/logos/eth.svg'),
'/etc/mainnet': require('icons/networks/logos/etc.svg'), etc_mainnet: require('icons/networks/logos/etc.svg'),
'/poa/core': require('icons/networks/logos/poa.svg'), poa_core: require('icons/networks/logos/poa.svg'),
'/rsk/mainnet': require('icons/networks/logos/rsk.svg'), rsk_mainnet: require('icons/networks/logos/rsk.svg'),
'/xdai/testnet': require('icons/networks/logos/gnosis.svg'), xdai_testnet: require('icons/networks/logos/gnosis.svg'),
'/poa/sokol': require('icons/networks/logos/sokol.svg'), poa_sokol: require('icons/networks/logos/sokol.svg'),
'/artis/sigma1': require('icons/networks/logos/artis.svg'), artis_sigma1: require('icons/networks/logos/artis.svg'),
'/lukso/l14': require('icons/networks/logos/lukso.svg'), lukso_l14: require('icons/networks/logos/lukso.svg'),
'/astar': require('icons/networks/logos/astar.svg'), astar: require('icons/networks/logos/astar.svg'),
'/shiden': require('icons/networks/logos/shiden.svg'), shiden: require('icons/networks/logos/shiden.svg'),
'/shibuya': require('icons/networks/logos/shibuya.svg'), shibuya: require('icons/networks/logos/shibuya.svg'),
}; };
interface Props { interface Props {
...@@ -32,7 +33,7 @@ interface Props { ...@@ -32,7 +33,7 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => { const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white'); const logoColor = useColorModeValue('blue.600', 'white');
const href = link('network_index'); const href = link('network_index');
const logo = appConfig.network.logo || LOGOS[appConfig.network.basePath]; const logo = appConfig.network.logo || (appConfig.network.type ? LOGOS[appConfig.network.type] : undefined);
const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' }); const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' });
...@@ -41,46 +42,50 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => { ...@@ -41,46 +42,50 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
if (logo && typeof logo === 'string') { if (logo && typeof logo === 'string') {
logoEl = ( logoEl = (
<Image <Image
h="20px" w="auto"
h="100%"
src={ logo } src={ logo }
alt={ `${ appConfig.network.name } network icon` } alt={ `${ appConfig.network.name } network icon` }
/> />
); );
} else if (typeof logo !== undefined) { } else if (typeof logo !== 'undefined') {
logoEl = ( logoEl = (
<Icon <Icon
as={ logo as FunctionComponent<SVGAttributes<SVGElement>> } as={ logo as FunctionComponent<SVGAttributes<SVGElement>> }
width="auto" width="auto"
height="20px" height="100%"
{ ...getDefaultTransitionProps() } { ...getDefaultTransitionProps() }
style={ style } style={ style }
/> />
); );
} else { } else {
<Icon logoEl = (
as={ blockscoutLogo } <Icon
width="113px" as={ blockscoutLogo }
height="20px" width="auto"
color={ logoColor } height="100%"
{ ...getDefaultTransitionProps() } color={ logoColor }
style={ style } { ...getDefaultTransitionProps() }
/>; style={ style }
/>
);
} }
return ( return (
<NextLink href={ href } passHref> // TODO switch to <NextLink href={ href } passHref> when main page for network will be ready
<Box <Box
as="a" as="a"
width={{ base: '113px', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }} href={ href }
display="inline-flex" width={{ base: 'auto', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }}
overflow="hidden" height="20px"
onClick={ onClick } display="inline-flex"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) } overflow="hidden"
aria-label="Link to main page" onClick={ onClick }
> { ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
{ logoEl } aria-label="Link to main page"
</Box> >
</NextLink> { logoEl }
</Box>
); );
}; };
......
import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react'; import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { FeaturedNetwork } from 'types/networks'; import type { FeaturedNetwork } from 'types/networks';
...@@ -12,7 +11,6 @@ import useColors from './useColors'; ...@@ -12,7 +11,6 @@ import useColors from './useColors';
interface Props extends FeaturedNetwork { interface Props extends FeaturedNetwork {
isActive: boolean; isActive: boolean;
isMobile?: boolean; isMobile?: boolean;
url: string;
} }
const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => { const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
...@@ -31,38 +29,37 @@ const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => { ...@@ -31,38 +29,37 @@ const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
return ( return (
<Box as="li" listStyleType="none"> <Box as="li" listStyleType="none">
<NextLink href={ url } passHref> <Flex
<Flex as="a"
as="a" href={ url }
px={ isMobile ? 3 : 4 } px={ isMobile ? 3 : 4 }
py={ 2 } py={ 2 }
alignItems="center" alignItems="center"
cursor="pointer" cursor="pointer"
pointerEvents={ isActive ? 'none' : 'initial' } pointerEvents={ isActive ? 'none' : 'initial' }
borderRadius="base" borderRadius="base"
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
bgColor={ isActive ? colors.bg.active : colors.bg.default } bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }} _hover={{ color: isActive ? colors.text.active : colors.text.hover }}
>
{ iconEl }
<Text
marginLeft={ 3 }
fontWeight="500"
color="inherit"
fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' }
> >
{ iconEl } { title }
<Text </Text>
marginLeft={ 3 } { isActive && (
fontWeight="500" <Icon
color="inherit" as={ checkIcon }
fontSize={ isMobile ? 'sm' : 'md' } boxSize="24px"
lineHeight={ isMobile ? '20px' : '24px' } marginLeft="auto"
> />
{ title } ) }
</Text> </Flex>
{ isActive && (
<Icon
as={ checkIcon }
boxSize="24px"
marginLeft="auto"
/>
) }
</Flex>
</NextLink>
</Box> </Box>
); );
}; };
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import appConfig from 'configs/app/config';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
...@@ -40,7 +41,7 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => { ...@@ -40,7 +41,7 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => {
</VStack> </VStack>
</Box> </Box>
<Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }> <Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<Button size="sm" width="full" variant="outline">Sign Out</Button> <Button size="sm" width="full" variant="outline" as="a" href={ appConfig.logoutUrl }>Sign Out</Button>
</Box> </Box>
</Box> </Box>
); );
......
...@@ -2,17 +2,26 @@ import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@c ...@@ -2,17 +2,26 @@ import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@c
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import link from 'lib/link/link';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuDesktop = () => { const ProfileMenuDesktop = () => {
const { data } = useFetchProfileInfo(); const { data, isFetched } = useFetchProfileInfo();
const loginUrl = link('auth');
return ( return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy> <Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<PopoverTrigger> <PopoverTrigger>
<Button variant="unstyled" display="inline-flex" height="auto" flexShrink={ 0 }> <Button
<UserAvatar size={ 50 } data={ data }/> variant="unstyled"
display="inline-flex"
height="auto"
flexShrink={ 0 }
as={ data ? undefined : 'a' }
href={ data ? undefined : loginUrl }
>
<UserAvatar size={ 50 } data={ data } isFetched={ isFetched }/>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
{ data && ( { data && (
......
import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure } from '@chakra-ui/react'; import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, Button } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import link from 'lib/link/link';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler'; import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
...@@ -9,38 +10,39 @@ import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; ...@@ -9,38 +10,39 @@ import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => { const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { data } = useFetchProfileInfo(); const { data, isFetched } = useFetchProfileInfo();
const loginUrl = link('auth');
return ( return (
<> <>
<Box padding={ 2 } onClick={ onOpen }> <Box padding={ 2 } onClick={ onOpen }>
<UserAvatar size={ 24 } data={ data }/> <UserAvatar size={ 24 } data={ data } isFetched={ isFetched }/>
</Box> </Box>
{ data && ( <Drawer
<Drawer isOpen={ isOpen }
isOpen={ isOpen } placement="right"
placement="right" onClose={ onClose }
onClose={ onClose } autoFocus={ false }
autoFocus={ false } >
> <DrawerOverlay/>
<DrawerOverlay/> <DrawerContent maxWidth="260px">
<DrawerContent maxWidth="260px"> <DrawerBody p={ 6 }>
<DrawerBody p={ 6 }> <Flex
<Flex justifyContent="space-between"
justifyContent="space-between" alignItems="center"
alignItems="center" mb={ 6 }
mb={ 6 } >
> <ColorModeToggler/>
<ColorModeToggler/> <Box onClick={ onClose }>
<Box onClick={ onClose }> <UserAvatar size={ 24 } data={ data } isFetched={ isFetched }/>
<UserAvatar size={ 24 } data={ data }/> </Box>
</Box> </Flex>
</Flex> { data ? <ProfileMenuContent { ...data }/> : (
<ProfileMenuContent { ...data }/> <Button size="sm" width="full" variant="outline" as="a" href={ loginUrl }>Sign In</Button>
</DrawerBody> ) }
</DrawerContent> </DrawerBody>
</Drawer> </DrawerContent>
) } </Drawer>
</> </>
); );
}; };
......
...@@ -6,7 +6,7 @@ import link from 'lib/link/link'; ...@@ -6,7 +6,7 @@ import link from 'lib/link/link';
import SearchBarDesktop from './SearchBarDesktop'; import SearchBarDesktop from './SearchBarDesktop';
import SearchBarMobile from './SearchBarMobile'; import SearchBarMobile from './SearchBarMobile';
const SearchBar = () => { const SearchBar = ({ withShadow }: {withShadow?: boolean}) => {
const [ value, setValue ] = React.useState(''); const [ value, setValue ] = React.useState('');
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
...@@ -22,7 +22,7 @@ const SearchBar = () => { ...@@ -22,7 +22,7 @@ const SearchBar = () => {
return ( return (
<> <>
<SearchBarDesktop onChange={ handleChange } onSubmit={ handleSubmit }/> <SearchBarDesktop onChange={ handleChange } onSubmit={ handleSubmit }/>
<SearchBarMobile onChange={ handleChange } onSubmit={ handleSubmit }/> <SearchBarMobile onChange={ handleChange } onSubmit={ handleSubmit } withShadow={ withShadow }/>
</> </>
); );
}; };
......
import { InputGroup, Input, InputLeftElement, Icon, 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 throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import isBrowser from 'lib/isBrowser'; import ScrollDirectionContext from 'ui/ScrollDirectionContext';
const SCROLL_DIFF_THRESHOLD = 20; const TOP = 55;
interface Props { interface Props {
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void; onSubmit: (event: FormEvent<HTMLFormElement>) => void;
withShadow?: boolean;
} }
const SearchBarMobile = ({ onChange, onSubmit }: Props) => { const SearchBarMobile = ({ onChange, onSubmit, withShadow }: Props) => {
const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0); const [ isSticky, setIsSticky ] = React.useState(false);
const [ isVisible, setVisibility ] = React.useState(true);
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const bgColor = useColorModeValue('white', 'black');
const handleScroll = React.useCallback(() => { const handleScroll = React.useCallback(() => {
const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight); if (window.pageYOffset !== 0) {
const scrollDiff = currentScrollPosition - prevScrollPosition.current; setIsSticky(true);
} else {
if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) { setIsSticky(false);
setVisibility(scrollDiff > 0 ? false : true);
} }
prevScrollPosition.current = currentScrollPosition;
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
...@@ -46,37 +38,46 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => { ...@@ -46,37 +38,46 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const bgColor = useColorModeValue('white', 'black');
return ( return (
<chakra.form <ScrollDirectionContext.Consumer>
noValidate { (scrollDirection) => (
onSubmit={ onSubmit } <chakra.form
paddingX={ 4 } noValidate
paddingTop={ 1 } onSubmit={ onSubmit }
paddingBottom={ 2 } paddingX={ 4 }
position="fixed" paddingTop={ 1 }
top="56px" paddingBottom={ 2 }
left="0" position="fixed"
zIndex="docked" top={ `${ TOP }px` }
bgColor={ bgColor } left="0"
transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' } zIndex="sticky1"
transitionProperty="transform" bgColor={ bgColor }
transitionDuration="slow" transform={ scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)' }
display={{ base: 'block', lg: 'none' }} transitionProperty="transform,box-shadow"
w="100%" transitionDuration="slow"
> display={{ base: 'block', lg: 'none' }}
<InputGroup size="sm"> w="100%"
<InputLeftElement > boxShadow={ withShadow && scrollDirection !== 'down' && isSticky ? 'md' : 'none' }
<Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/> >
</InputLeftElement> <InputGroup size="sm">
<Input <InputLeftElement >
paddingInlineStart="38px" <Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
placeholder="Search by addresses / ... " </InputLeftElement>
ml="1px" <Input
onChange={ onChange } paddingInlineStart="38px"
borderColor={ inputBorderColor } placeholder="Search by addresses / ... "
/> ml="1px"
</InputGroup> onChange={ onChange }
</chakra.form> borderColor={ inputBorderColor }
/>
</InputGroup>
</chakra.form>
) }
</ScrollDirectionContext.Consumer>
); );
}; };
......
...@@ -3,27 +3,33 @@ import React from 'react'; ...@@ -3,27 +3,33 @@ import React from 'react';
import nftIcon from 'icons/nft_shield.svg'; import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props { interface Props {
value: string; value: string;
tokenId: string; tokenId: string;
hash: string; hash: string;
symbol: string; name?: string | null;
symbol?: string | null;
} }
const NftTokenTransferSnippet = (props: Props) => { const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) => {
const num = props.value === '1' ? '' : props.value; const num = value === '1' ? '' : value;
const url = link('token_instance_item', { hash: props.hash, id: props.tokenId }); const url = link('token_instance_item', { hash: hash, id: tokenId });
return ( return (
<Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap"> <Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
<Text fontWeight={ 500 } as="span">For { num } token ID:</Text> <Text fontWeight={ 500 } as="span">For { num } token ID:</Text>
<Box display="inline-flex" alignItems="center"> <Box display="inline-flex" alignItems="center">
<Icon as={ nftIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ nftIcon } boxSize={ 6 } mr={ 1 }/>
<Link href={ url } fontWeight={ 600 }>{ props.tokenId }</Link> <Link href={ url } fontWeight={ 600 }>{ tokenId }</Link>
</Box> </Box>
<TokenSnippet symbol={ props.symbol } hash={ props.hash } name="Foo"/> { name ? (
<TokenSnippet symbol={ symbol } hash={ hash } name={ name }/>
) : (
<AddressLink hash={ hash } truncation="constant" type="token"/>
) }
</Flex> </Flex>
); );
}; };
......
import { Flex, Icon, Text } from '@chakra-ui/react'; import { Flex, Icon, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer as TTokenTransfer, Erc20TotalPayload, Erc721TotalPayload, Erc1155TotalPayload } from 'types/api/tokenTransfer';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
...@@ -12,43 +12,47 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet'; ...@@ -12,43 +12,47 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer; type Props = TTokenTransfer;
const TokenTransfer = (props: Props) => { const TokenTransfer = ({ token, total, to, from }: Props) => {
const isColumnLayout = props.token_type === 'ERC-1155' && Array.isArray(props.total); const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total);
const tokenSnippet = <TokenSnippet symbol={ props.token_symbol } hash={ props.token_address } name="Foo" ml={ 3 }/>; const tokenSnippet = <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } ml={ 3 }/>;
const content = (() => { const content = (() => {
switch (props.token_type) { switch (token.type) {
case 'ERC-20': case 'ERC-20': {
const payload = total as Erc20TotalPayload;
return ( return (
<Flex> <Flex>
<Text fontWeight={ 500 } as="span">For:{ space } <Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ props.total.value } unit="ether" exchangeRate={ props.exchange_rate } fontWeight={ 600 }/> <CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/>
</Text> </Text>
{ tokenSnippet } { tokenSnippet }
</Flex> </Flex>
); );
}
case 'ERC-721': { case 'ERC-721': {
const payload = total as Erc721TotalPayload;
return ( return (
<NftTokenTransferSnippet <NftTokenTransferSnippet
tokenId={ props.total.token_id } tokenId={ payload.token_id }
value="1" value="1"
hash={ props.token_address } hash={ token.address }
symbol={ props.token_symbol } symbol={ token.symbol }
/> />
); );
} }
case 'ERC-1155': { case 'ERC-1155': {
const items = Array.isArray(props.total) ? props.total : [ props.total ]; const payload = total as Erc1155TotalPayload | Array<Erc1155TotalPayload>;
const items = Array.isArray(payload) ? payload : [ payload ];
return items.map((item) => ( return items.map((item) => (
<NftTokenTransferSnippet <NftTokenTransferSnippet
key={ item.token_id } key={ item.token_id }
tokenId={ item.token_id } tokenId={ item.token_id }
value={ item.value } value={ item.value }
hash={ props.token_address } hash={ token.address }
symbol={ props.token_symbol } symbol={ token.symbol }
/> />
)); ));
} }
...@@ -64,9 +68,9 @@ const TokenTransfer = (props: Props) => { ...@@ -64,9 +68,9 @@ const TokenTransfer = (props: Props) => {
flexDir={ isColumnLayout ? 'column' : 'row' } flexDir={ isColumnLayout ? 'column' : 'row' }
> >
<Flex alignItems="center"> <Flex alignItems="center">
<AddressLink fontWeight="500" hash={ props.from.hash } truncation="constant"/> <AddressLink fontWeight="500" hash={ from.hash } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink fontWeight="500" hash={ props.to.hash } truncation="constant"/> <AddressLink fontWeight="500" hash={ to.hash } truncation="constant"/>
</Flex> </Flex>
<Flex flexDir="column" rowGap={ 5 }> <Flex flexDir="column" rowGap={ 5 }>
{ content } { content }
......
...@@ -10,9 +10,9 @@ interface Props { ...@@ -10,9 +10,9 @@ interface Props {
} }
function getItemsNum(items: Array<TTokenTransfer>) { function getItemsNum(items: Array<TTokenTransfer>) {
const nonErc1155items = items.filter((item) => item.token_type !== 'ERC-1155').length; const nonErc1155items = items.filter((item) => item.token.type !== 'ERC-1155').length;
const erc1155items = items const erc1155items = items
.filter((item) => item.token_type === 'ERC-1155') .filter((item) => item.token.type === 'ERC-1155')
.map((item) => { .map((item) => {
if (Array.isArray(item.total)) { if (Array.isArray(item.total)) {
return item.total.length; return item.total.length;
......
import { Grid, GridItem, Text, Box, Icon, Link, Spinner } from '@chakra-ui/react'; import { Grid, GridItem, Text, Box, Icon, Link, Spinner, Tag, Flex, Tooltip, chakra } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -6,13 +6,14 @@ import React from 'react'; ...@@ -6,13 +6,14 @@ import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg';
import { WEI, WEI_IN_GWEI } from 'lib/consts'; 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 dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration'; import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
...@@ -47,8 +48,8 @@ const TxDetails = () => { ...@@ -47,8 +48,8 @@ const TxDetails = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Transaction>( const { data, isLoading, isError } = useQuery<unknown, unknown, Transaction>(
[ 'tx', router.query.id ], [ QueryKeys.tx, router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }`), async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
...@@ -72,6 +73,18 @@ const TxDetails = () => { ...@@ -72,6 +73,18 @@ const TxDetails = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const addressFromTags = [
...data.from.private_tags || [],
...data.from.public_tags || [],
...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const addressToTags = [
...data.to.private_tags || [],
...data.to.public_tags || [],
...data.to.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
<DetailsInfoItem <DetailsInfoItem
...@@ -131,36 +144,51 @@ const TxDetails = () => { ...@@ -131,36 +144,51 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="From" title="From"
hint="Address (external or contract) sending the transaction." hint="Address (external or contract) sending the transaction."
columnGap={ 3 }
> >
<Address> <Address>
<AddressIcon hash={ data.from.hash }/> <AddressIcon hash={ data.from.hash }/>
<AddressLink ml={ 2 } hash={ data.from.hash } alias={ data.from.name }/> <AddressLink ml={ 2 } hash={ data.from.hash }/>
<CopyToClipboard text={ data.from.hash }/> <CopyToClipboard text={ data.from.hash }/>
</Address> </Address>
{ data.from.name && <Text>{ data.from.name }</Text> }
{ addressFromTags.length > 0 && (
<Flex columnGap={ 3 }>
{ addressFromTags }
</Flex>
) }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title={ data.to.is_contract ? 'Interacted with contract' : 'To' } title={ data.to.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction." hint="Address (external or contract) receiving the transaction."
flexWrap={{ base: 'wrap', lg: 'nowrap' }} flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 }
> >
<Address mr={ 3 }> <Address>
<AddressIcon hash={ data.to.hash }/> <AddressIcon hash={ data.to.hash }/>
<AddressLink ml={ 2 } hash={ data.to.hash } alias={ data.to.name }/> <AddressLink ml={ 2 } hash={ data.to.hash }/>
<CopyToClipboard text={ data.to.hash }/> <CopyToClipboard text={ data.to.hash }/>
</Address> </Address>
{ /* todo_tom Nikita should add to api later */ } { data.to.name && <Text>{ data.to.name }</Text> }
{ /* <Tag colorScheme="orange" variant="solid" flexShrink={ 0 }>SANA</Tag> */ } { data.to.is_contract && data.result === 'success' && (
{ /* <Tooltip label="Contract execution completed"> <Tooltip label="Contract execution completed">
<chakra.span display="inline-flex"> <chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500" cursor="pointer"/> <Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
</chakra.span> </chakra.span>
</Tooltip> */ } </Tooltip>
{ /* <Tooltip label="Error occured during contract execution"> ) }
<chakra.span display="inline-flex"> { data.to.is_contract && Boolean(data.status) && data.result !== 'success' && (
<Icon as={ errorIcon } boxSize={ 4 } ml={ 2 } color="red.500" cursor="pointer"/> <Tooltip label="Error occured during contract execution">
</chakra.span> <chakra.span display="inline-flex">
</Tooltip> */ } <Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
{ /* <TokenSnippet symbol="UP" name="User Pay" hash="0xA17ed5dFc62D0a3E74D69a0503AE9FdA65d9f212" ml={ 3 }/> */ } </chakra.span>
</Tooltip>
) }
{ addressToTags.length > 0 && (
<Flex columnGap={ 3 }>
{ addressToTags }
</Flex>
) }
</DetailsInfoItem> </DetailsInfoItem>
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => { { TOKEN_TRANSFERS.map(({ title, hint, type }) => {
const items = data.token_transfers?.filter((token) => token.type === type) || []; const items = data.token_transfers?.filter((token) => token.type === type) || [];
...@@ -184,7 +212,7 @@ const TxDetails = () => { ...@@ -184,7 +212,7 @@ const TxDetails = () => {
title="Value" title="Value"
hint="Value sent in the native token (and USD) if applicable." hint="Value sent in the native token (and USD) if applicable."
> >
<CurrencyValue value={ data.value } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/> <CurrencyValue value={ data.value } currency={ appConfig.network.currency.symbol } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transaction fee" title="Transaction fee"
...@@ -192,7 +220,7 @@ const TxDetails = () => { ...@@ -192,7 +220,7 @@ const TxDetails = () => {
> >
<CurrencyValue <CurrencyValue
value={ data.fee.value } value={ data.fee.value }
currency={ appConfig.network.currency } currency={ appConfig.network.currency.symbol }
exchangeRate={ data.exchange_rate } exchangeRate={ data.exchange_rate }
flexWrap="wrap" flexWrap="wrap"
/> />
...@@ -201,7 +229,7 @@ const TxDetails = () => { ...@@ -201,7 +229,7 @@ const TxDetails = () => {
title="Gas price" title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
> >
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency }</Text> <Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text> <Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
...@@ -244,12 +272,12 @@ const TxDetails = () => { ...@@ -244,12 +272,12 @@ const TxDetails = () => {
{ data.tx_burnt_fee && ( { data.tx_burnt_fee && (
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` } hint={ `Amount of ${ appConfig.network.currency.symbol } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
> >
<Icon as={ flameIcon } mr={ 1 } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } mr={ 1 } boxSize={ 5 } color="gray.500"/>
<CurrencyValue <CurrencyValue
value={ String(data.tx_burnt_fee) } value={ String(data.tx_burnt_fee) }
currency={ appConfig.network.currency } currency={ appConfig.network.currency.symbol }
exchangeRate={ data.exchange_rate } exchangeRate={ data.exchange_rate }
flexWrap="wrap" flexWrap="wrap"
/> />
......
...@@ -4,6 +4,7 @@ import { useRouter } from 'next/router'; ...@@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -42,16 +43,15 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT ...@@ -42,16 +43,15 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
return a.value === b.value ? 0 : result; return a.value === b.value ? 0 : result;
} }
// no gas limit in api yet case 'gas-limit-desc': {
// case 'gas-limit-desc': { const result = a.gas_limit > b.gas_limit ? -1 : 1;
// const result = a.gasLimit > b.gasLimit ? -1 : 1; return a.gas_limit === b.gas_limit ? 0 : result;
// return a.gasLimit === b.gasLimit ? 0 : result; }
// }
// case 'gas-limit-asc': { case 'gas-limit-asc': {
// const result = a.gasLimit > b.gasLimit ? 1 : -1; const result = a.gas_limit > b.gas_limit ? 1 : -1;
// return a.gasLimit === b.gasLimit ? 0 : result; return a.gas_limit === b.gas_limit ? 0 : result;
// } }
default: default:
return 0; return 0;
...@@ -73,8 +73,8 @@ const TxInternals = () => { ...@@ -73,8 +73,8 @@ const TxInternals = () => {
const [ searchTerm, setSearchTerm ] = React.useState<string>(''); const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>(); const [ sort, setSort ] = React.useState<Sort>();
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ 'tx-internals', router.query.id ], [ QueryKeys.txInternals, router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/internal-transactions`), async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
......
...@@ -4,6 +4,7 @@ import { useRouter } from 'next/router'; ...@@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { LogsResponse } from 'types/api/log'; import type { LogsResponse } from 'types/api/log';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -15,8 +16,8 @@ const TxLogs = () => { ...@@ -15,8 +16,8 @@ const TxLogs = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ 'tx-log', router.query.id ], [ QueryKeys.txLog, router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/logs`), async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
......
...@@ -4,6 +4,7 @@ import { useRouter } from 'next/router'; ...@@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -14,8 +15,8 @@ const TxRawTrace = () => { ...@@ -14,8 +15,8 @@ const TxRawTrace = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
[ 'tx-raw-trace', router.query.id ], [ QueryKeys.txRawTrace, router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/raw-trace`), async() => await fetch(`/node-api/transactions/${ router.query.id }/raw-trace`),
{ {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react'; import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
...@@ -14,7 +15,7 @@ import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; ...@@ -14,7 +15,7 @@ import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction; type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) => { const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: gasLimit }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return ( return (
...@@ -35,14 +36,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) = ...@@ -35,14 +36,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) =
</Address> </Address>
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency }</Text> <Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text> <Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack> </HStack>
{ /* no gas limit in api yet */ } <HStack spacing={ 3 }>
{ /* <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text> <Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text> <Text fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Text>
</HStack> */ } </HStack>
</AccountListItemMobile> </AccountListItemMobile>
); );
}; };
......
...@@ -10,7 +10,7 @@ const TxInternalsSkeletonDesktop = () => { ...@@ -10,7 +10,7 @@ const TxInternalsSkeletonDesktop = () => {
<Skeleton w="78px"/> <Skeleton w="78px"/>
<Skeleton w="360px"/> <Skeleton w="360px"/>
</Flex> </Flex>
<SkeletonTable columns={ [ '28%', '28%', '24px', '28%', '16%' ] }/> <SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
</> </>
); );
}; };
......
...@@ -38,6 +38,10 @@ const TxInternalsSkeletonMobile = () => { ...@@ -38,6 +38,10 @@ const TxInternalsSkeletonMobile = () => {
<Skeleton w="70px" mr={ 2 }/> <Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/> <Skeleton w="30px"/>
</Flex> </Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
</Flex> </Flex>
)) } )) }
</Box> </Box>
......
import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th, Link, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import { default as Thead } from 'ui/shared/TheadSticky';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem'; import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils'; import type { Sort, SortField } from 'ui/tx/internals/utils';
...@@ -18,36 +19,33 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => { ...@@ -18,36 +19,33 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
<TableContainer width="100%" mt={ 6 }> <Table variant="simple" size="sm" mt={ 6 }>
<Table variant="simple" size="sm"> <Thead top={ 0 }>
<Thead> <Tr>
<Tr> <Th width="28%">Type</Th>
<Th width="28%">Type</Th> <Th width="20%">From</Th>
<Th width="28%">From</Th> <Th width="24px" px={ 0 }/>
<Th width="24px" px={ 0 }/> <Th width="20%">To</Th>
<Th width="28%">To</Th> <Th width="16%" isNumeric>
<Th width="16%" isNumeric> <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }> { sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> } Value { appConfig.network.currency.symbol }
Value { appConfig.network.currency } </Link>
</Link> </Th>
</Th> <Th width="16%" isNumeric>
{ /* no gas limit in api yet */ } <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ /* <Th width="16%" isNumeric> { sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }> Gas limit { appConfig.network.currency.symbol }
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> } </Link>
Gas limit </Th>
</Link> </Tr>
</Th> */ } </Thead>
</Tr> <Tbody>
</Thead> { data.map((item) => (
<Tbody> <TxInternalsTableItem key={ item.transaction_hash } { ...item }/>
{ data.map((item) => ( )) }
<TxInternalsTableItem key={ item.transaction_hash } { ...item }/> </Tbody>
)) } </Table>
</Tbody>
</Table>
</TableContainer>
); );
}; };
......
import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react'; import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
...@@ -12,7 +13,7 @@ import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; ...@@ -12,7 +13,7 @@ import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction type Props = InternalTransaction
const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) => { const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: gasLimit }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return ( return (
...@@ -43,10 +44,9 @@ const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) = ...@@ -43,10 +44,9 @@ const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) =
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ value } { value }
</Td> </Td>
{ /* no gas limit in api yet */ } <Td isNumeric verticalAlign="middle">
{ /* <Td isNumeric verticalAlign='middle'> { BigNumber(gasLimit).toFormat() }
{ gasLimit.toLocaleString('en') } </Td>
</Td> */ }
</Tr> </Tr>
); );
}; };
......
import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react'; import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type ArrayElement from 'types/utils/ArrayElement';
...@@ -6,6 +7,7 @@ import type ArrayElement from 'types/utils/ArrayElement'; ...@@ -6,6 +7,7 @@ import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { data } from 'data/txState'; import type { data } from 'data/txState';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
...@@ -60,11 +62,11 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -60,11 +62,11 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
) } ) }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }> <Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box> <Box>
<Text as="span">Miner </Text> <Text as="span">{ capitalize(getNetworkValidatorTitle()) }</Text>
<Link>{ miner }</Link> <Link>{ miner }</Link>
</Box> </Box>
<Box> <Box>
<Text as="span">Before { appConfig.network.currency } </Text> <Text as="span">Before { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ before.balance }</Text> <Text as="span" variant="secondary">{ before.balance }</Text>
</Box> </Box>
{ typeof before.nonce !== 'undefined' && ( { typeof before.nonce !== 'undefined' && (
...@@ -74,7 +76,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -74,7 +76,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
</Box> </Box>
) } ) }
<Box> <Box>
<Text as="span">After { appConfig.network.currency } </Text> <Text as="span">After { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ after.balance }</Text> <Text as="span" variant="secondary">{ after.balance }</Text>
</Box> </Box>
{ typeof after.nonce !== 'undefined' && ( { typeof after.nonce !== 'undefined' && (
...@@ -83,7 +85,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -83,7 +85,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text> <Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box> </Box>
) } ) }
<Text>State difference { appConfig.network.currency }</Text> <Text>State difference { appConfig.network.currency.symbol }</Text>
<Stat> <Stat>
{ diff } { diff }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/> <StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
......
import { import {
Table, Table,
Thead,
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { data } from 'data/txState'; import { data } from 'data/txState';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import { default as Thead } from 'ui/shared/TheadSticky';
import TxStateTableItem from 'ui/tx/state/TxStateTableItem'; import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => { const TxStateTable = () => {
return ( return (
<TableContainer width="100%" mt={ 6 }> <Table variant="simple" minWidth="950px" size="sm" w="auto" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm" w="auto"> <Thead top={ 0 }>
<Thead> <Tr>
<Tr> <Th width="92px">Storage</Th>
<Th width="92px">Storage</Th> <Th width="146px">Address</Th>
<Th width="146px">Address</Th> <Th width="120px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="120px">Miner</Th> <Th width="33%" isNumeric>{ `After ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency }` }</Th> <Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency }` }</Th> <Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency }` }</Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -34,7 +34,7 @@ const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => { ...@@ -34,7 +34,7 @@ const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => {
<Flex> <Flex>
<CurrencyValue <CurrencyValue
value={ tx.fee.value } value={ tx.fee.value }
currency={ appConfig.network.currency } currency={ appConfig.network.currency.symbol }
exchangeRate={ tx.exchange_rate } exchangeRate={ tx.exchange_rate }
accuracyUsd={ 2 } accuracyUsd={ 2 }
/> />
......
import { Tag } from '@chakra-ui/react'; import { Tag } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TransactionType } from 'types/api/transaction';
export interface Props { export interface Props {
type: 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall'; type: TransactionType;
} }
const TxStatus = ({ type }: Props) => { const TxStatus = ({ type }: Props) => {
...@@ -10,24 +12,24 @@ const TxStatus = ({ type }: Props) => { ...@@ -10,24 +12,24 @@ const TxStatus = ({ type }: Props) => {
let colorScheme; let colorScheme;
switch (type) { switch (type) {
case 'contract-call': case 'contract_call':
label = 'Contract call'; label = 'Contract call';
colorScheme = 'blue'; colorScheme = 'blue';
break; break;
case 'transaction': case 'contract_creation':
label = 'Transaction'; label = 'Contract creation';
colorScheme = 'purple'; colorScheme = 'purple';
break; break;
case 'token-transfer': case 'token_transfer':
label = 'Token transfer'; label = 'Token transfer';
colorScheme = 'orange'; colorScheme = 'orange';
break; break;
case 'internal-tx': case 'token_creation':
label = 'Internal txn'; label = 'Token creation';
colorScheme = 'cyan'; colorScheme = 'cyan';
break; break;
case 'multicall': case 'coin_transfer':
label = 'Multicall'; label = 'Coin transfer';
colorScheme = 'teal'; colorScheme = 'teal';
break; break;
} }
......
import { Box, HStack, Show } from '@chakra-ui/react'; import { Alert, Box, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useState, useCallback } from 'react';
import type { TransactionsResponse } from 'types/api/transaction'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns'; import * as cookies from 'lib/cookies';
import FilterButton from 'ui/shared/FilterButton'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton';
import TxsListItem from './TxsListItem'; import TxsHeader from './TxsHeader';
import TxsTable from './TxsTable'; import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort';
import useQueryWithPages from './useQueryWithPages';
type Props = { type Props = {
txs: TransactionsResponse['items']; queryName: QueryKeys;
showDescription?: boolean; showDescription?: boolean;
showSortButton?: boolean; stateFilter?: TTxsFilters['filter'];
apiPath: string;
} }
const TxsContent = ({ showSortButton = true, showDescription = true, txs }: Props) => { const TxsContent = ({
const [ sorting, setSorting ] = useState<Sort>(); queryName,
const [ sortedTxs, setSortedTxs ] = useState(txs); showDescription,
stateFilter,
apiPath,
}: Props) => {
const [ sorting, setSorting ] = useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort || '');
// const [ filters, setFilters ] = useState<Partial<TTxsFilters>>({ type: [], method: [] });
// sorting should be preserved with pagination!
const sort = useCallback((field: 'val' | 'fee') => () => { const sort = useCallback((field: 'val' | 'fee') => () => {
if (field === 'val') { setSorting((prevVal) => {
setSorting((prevVal => { let newVal: Sort = '';
if (field === 'val') {
if (prevVal === 'val-asc') { if (prevVal === 'val-asc') {
return undefined; newVal = '';
} else if (prevVal === 'val-desc') {
newVal = 'val-asc';
} else {
newVal = 'val-desc';
} }
if (prevVal === 'val-desc') { }
return 'val-asc'; if (field === 'fee') {
}
return 'val-desc';
}));
}
if (field === 'fee') {
setSorting((prevVal => {
if (prevVal === 'fee-asc') { if (prevVal === 'fee-asc') {
return undefined; newVal = '';
} } else if (prevVal === 'fee-desc') {
if (prevVal === 'fee-desc') { newVal = 'fee-asc';
return 'fee-asc'; } else {
newVal = 'fee-desc';
} }
return 'fee-desc'; }
})); cookies.set(cookies.NAMES.TXS_SORT, newVal);
} return newVal;
}, []); });
}, [ ]);
const {
data,
isLoading,
isError,
pagination,
} = useQueryWithPages(apiPath, queryName, stateFilter && { filter: stateFilter });
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
if (isError) {
return <DataFetchAlert/>;
}
const txs = data?.items;
if (!isLoading && !txs) {
return <Alert>There are no transactions.</Alert>;
}
let content = (
<>
<Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show>
<Show above="lg" ssr={ false }><TxsSkeletonDesktop/></Show>
</>
);
if (!isLoading && txs) {
content = <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
}
useEffect(() => { const paginationProps = {
switch (sorting) { ...pagination,
case 'val-desc': hasNextPage: data?.next_page_params !== undefined && data?.next_page_params !== null && Object.keys(data?.next_page_params).length > 0,
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 ( return (
<> <>
{ showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> } { showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
<HStack mb={ 6 }> <TxsHeader sorting={ sorting } setSorting={ setSorting } paginationProps={ paginationProps }/>
{ /* TODO */ } { content }
<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>
</> </>
); );
}; };
......
import {
Button,
Checkbox,
CheckboxGroup,
Grid,
Link,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Text,
useColorModeValue,
useDisclosure,
Flex,
} from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import type { TTxsFilters, TypeFilter, MethodFilter } from 'types/api/txsFilters';
import FilterButton from 'ui/shared/FilterButton';
interface Props {
appliedFiltersNum?: number;
filters: Partial<TTxsFilters>;
onFiltersChange: (val: Partial<TTxsFilters>) => void;
}
const TYPE_OPTIONS = [
{ title: 'Token transfer', id: 'token_transfer' },
{ title: 'Contract Creation', id: 'contract_creation' },
{ title: 'Contract Call', id: 'contract_call' },
{ title: 'Coin Transfer', id: 'coin_transfer' },
{ title: 'Token Creation', id: 'token_creation' },
];
const METHOD_OPTIONS = [
{ title: 'Approve', id: 'approve' },
{ title: 'Transfer', id: 'transfer' },
{ title: 'Multicall', id: 'multicall' },
{ title: 'Mint', id: 'mint' },
{ title: 'Commit', id: 'commit' },
];
// TODO: i think we need to reload page after applying filters,
// because we need to reset pagination, clear query caches, reconnect websocket...
// also mobile version of filters is not implemented
const TxsFilters = ({ onFiltersChange, filters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const [ typeFilter, setTypeFilter ] = useState<Array<TypeFilter>>(filters.type || []);
const [ methodFilter, setMethodFilter ] = useState<Array<MethodFilter>>(filters.method || []);
const onTypeFilterChange = useCallback((val: Array<TypeFilter>) => {
setTypeFilter(val);
}, []);
const onMethodFilterChange = useCallback((val: Array<MethodFilter>) => {
setMethodFilter(val);
}, []);
const onFilterReset = useCallback(() => {
setTypeFilter([]);
setMethodFilter([]);
onFiltersChange({ type: [], method: [] });
onClose();
}, [ onClose, onFiltersChange ]);
const onFilterApply = useCallback(() => {
onFiltersChange({ type: typeFilter, method: methodFilter } as Partial<TTxsFilters>);
onClose();
}, [ onClose, onFiltersChange, typeFilter, methodFilter ]);
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
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 }>
<Text variant="secondary" fontWeight="600" fontSize="sm">Type</Text>
<Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
<CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ typeFilter }>
{ TYPE_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</Grid>
<Text variant="secondary" fontWeight="600" fontSize="sm">Method</Text>
<Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
<CheckboxGroup size="lg" onChange={ onMethodFilterChange } defaultValue={ methodFilter }>
{ METHOD_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</Grid>
<Flex alignItems="center" justifyContent="space-between">
<Link fontSize="sm" onClick={ onFilterReset }>Reset filters</Link>
<Button variant="outline" size="sm" onClick={ onFilterApply }>Apply</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TxsFilters);
import { HStack, Flex, useColorModeValue } from '@chakra-ui/react';
import throttle from 'lodash/throttle';
import React, { useCallback } from 'react';
import type { Sort } from 'types/client/txs-sort';
import useIsMobile from 'lib/hooks/useIsMobile';
// import FilterInput from 'ui/shared/FilterInput';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TxsSorting from 'ui/txs/TxsSorting';
// import TxsFilters from './TxsFilters';
type Props = {
sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
paginationProps: PaginationProps;
}
const TOP_UP = 106;
const TOP_DOWN = 0;
const TxsHeader = ({ sorting, setSorting, paginationProps }: Props) => {
const [ isSticky, setIsSticky ] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
const isMobile = useIsMobile(false);
const handleScroll = useCallback(() => {
if (
Number(ref.current?.getBoundingClientRect().y) < TOP_UP + 5
) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const bgColor = useColorModeValue('white', 'black');
return (
<ScrollDirectionContext.Consumer>
{ (scrollDirection) => (
<Flex
backgroundColor={ bgColor }
mt={ -6 }
pt={ 6 }
pb={ 6 }
mx={{ base: -4, lg: 0 }}
px={{ base: 4, lg: 0 }}
justifyContent="space-between"
width={{ base: '100vw', lg: 'unset' }}
position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow"
transitionDuration="slow"
zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
ref={ ref }
>
<HStack>
{ /* api is not implemented */ }
{ /* <TxsFilters
filters={ filters }
onFiltersChange={ setFilters }
appliedFiltersNum={ 0 }
/> */ }
{ isMobile && (
<TxsSorting
isActive={ Boolean(sorting) }
setSorting={ setSorting }
sorting={ sorting }
/>
) }
{ /* api is not implemented */ }
{ /* <FilterInput
// eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} }
maxW="360px"
size="xs"
placeholder="Search by addresses, hash, method..."
/> */ }
</HStack>
<Pagination { ...paginationProps }/>
</Flex>
) }
</ScrollDirectionContext.Consumer>
);
};
export default TxsHeader;
...@@ -39,9 +39,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -39,9 +39,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}> <Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
<Flex justifyContent="space-between" mt={ 4 }> <Flex justifyContent="space-between" mt={ 4 }>
<HStack> <HStack>
{ /* TODO: we don't recieve type from api */ } { tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack> </HStack>
<TxAdditionalInfoButton onClick={ onOpen }/> <TxAdditionalInfoButton onClick={ onOpen }/>
...@@ -60,6 +58,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -60,6 +58,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
target="_self"
/> />
</Address> </Address>
</Flex> </Flex>
...@@ -67,7 +66,6 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -67,7 +66,6 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
</Flex> </Flex>
<Flex mt={ 3 }> <Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text> <Text as="span" whiteSpace="pre">Method </Text>
{ /* TODO: we don't recieve method from api */ }
<Text <Text
as="span" as="span"
variant="secondary" variant="secondary"
...@@ -75,8 +73,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -75,8 +73,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow="ellipsis" textOverflow="ellipsis"
> >
{ /* { tx.method } */ } { tx.method }
CommitHash
</Text> </Text>
</Flex> </Flex>
{ tx.block !== null && ( { tx.block !== null && (
...@@ -112,11 +109,11 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -112,11 +109,11 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
</Address> </Address>
</Flex> </Flex>
<Box mt={ 2 }> <Box mt={ 2 }>
<Text as="span">Value { appConfig.network.currency } </Text> <Text as="span">Value { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text>
</Box> </Box>
<Box mt={ 2 } mb={ 3 }> <Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { appConfig.network.currency } </Text> <Text as="span">Fee { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text>
</Box> </Box>
</Box> </Box>
......
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 { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = ({ isPending }: {isPending?: boolean}) => { const TxsInternalsSkeletonDesktop = () => {
return ( return (
<> <Box mb={ 8 }>
{ !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%' ] }/> <SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
</> </Box>
); );
}; };
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react'; import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const TxInternalsSkeletonMobile = ({ isPending }: {isPending?: boolean}) => { const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<> <Box>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> } { Array.from(Array(2)).map((item, index) => (
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }> <Flex
<Skeleton w="36px" flexShrink={ 0 }/> key={ index }
<Skeleton w="36px" flexShrink={ 0 }/> flexDirection="column"
<Skeleton w="100%"/> paddingBottom={ 3 }
</Flex> paddingTop={ 4 }
<Box> borderTopWidth="1px"
{ Array.from(Array(2)).map((item, index) => ( borderColor={ borderColor }
<Flex _last={{
key={ index } borderBottomWidth: '1px',
flexDirection="column" }}
paddingBottom={ 3 } >
paddingTop={ 4 } <Flex h={ 6 }>
borderTopWidth="1px" <Skeleton w="100px" mr={ 2 } h={ 6 }/>
borderColor={ borderColor } <Skeleton w="100px" h={ 6 }/>
_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> </Flex>
)) } <Skeleton w="100%" h="30px" mt={ 3 }/>
</Box> <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>
); );
}; };
......
import {
chakra,
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import SortButton from 'ui/shared/SortButton';
interface Props {
isActive: boolean;
sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
}
const SORT_OPTIONS = [
{ title: 'Default', id: '' },
{ title: 'Value ascending', id: 'val-asc' },
{ title: 'Value descending', id: 'val-desc' },
{ title: 'Fee ascending', id: 'fee-asc' },
{ title: 'Fee descending', id: 'fee-desc' },
];
const TxsSorting = ({ isActive, sorting, setSorting }: Props) => {
const { isOpen, onToggle } = useDisclosure();
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
setSorting((prevVal: Sort) => {
let newVal: Sort = '';
if (val !== prevVal) {
newVal = val as Sort;
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ setSorting ]);
return (
<Menu>
<MenuButton>
<SortButton
isActive={ isOpen || isActive }
onClick={ onToggle }
/>
</MenuButton>
<MenuList minWidth="240px">
<MenuOptionGroup value={ sorting } title="Sort by" type="radio" onChange={ setSortingFromMenu }>
{ SORT_OPTIONS.map((option) => (
<MenuItemOption
key={ option.id }
value={ option.id }
>
{ option.title }
</MenuItemOption>
)) }
</MenuOptionGroup>
</MenuList>
</Menu>
);
};
export default chakra(TxsSorting);
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from './TxsContent';
type Props = {
tab: 'validated' | 'pending';
}
const TxsTab = ({ tab }: Props) => {
return (
<TxsContent
queryName={ QueryKeys.transactions }
showDescription={ tab === 'validated' }
stateFilter={ tab }
apiPath="/api/transactions"
/>
);
};
export default TxsTab;
import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra-ui/react'; import { Link, Table, Tbody, Tr, Th, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -6,55 +6,54 @@ import type { Sort } from 'types/client/txs-sort'; ...@@ -6,55 +6,54 @@ import type { Sort } from 'types/client/txs-sort';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import TheadSticky from 'ui/shared/TheadSticky';
import TxsTableItem from './TxsTableItem'; import TxsTableItem from './TxsTableItem';
type Props = { type Props = {
txs: Array<Transaction>; txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void; sort: (field: 'val' | 'fee') => () => void;
sorting: Sort; sorting?: Sort;
} }
const TxsTable = ({ txs, sort, sorting }: Props) => { const TxsTable = ({ txs, sort, sorting }: Props) => {
return ( return (
<TableContainer width="100%" mt={ 6 }> <Table variant="simple" minWidth="810px" size="xs">
<Table variant="simple" minWidth="810px" size="xs"> <TheadSticky top={ 80 }>
<Thead> <Tr>
<Tr> <Th width="54px"></Th>
<Th width="54px"></Th> <Th width="20%">Type</Th>
<Th width="20%">Type</Th> <Th width="18%">Txn hash</Th>
<Th width="18%">Txn hash</Th> <Th width="15%">Method</Th>
<Th width="15%">Method</Th> <Th width="11%">Block</Th>
<Th width="11%">Block</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '128px', base: '66px' }}>From</Th> <Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '36px', base: '0' }}></Th> <Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th> <Th width="18%" isNumeric>
<Th width="18%" isNumeric> <Link onClick={ sort('val') } display="flex" justifyContent="end">
<Link onClick={ sort('val') } display="flex" justifyContent="end"> { sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> } { `Value ${ appConfig.network.currency.symbol }` }
{ `Value ${ appConfig.network.currency }` } </Link>
</Link> </Th>
</Th> <Th width="18%" isNumeric pr={ 5 }>
<Th width="18%" isNumeric pr={ 5 }> <Link onClick={ sort('fee') } display="flex" justifyContent="end">
<Link onClick={ sort('fee') } display="flex" justifyContent="end"> { sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> } { `Fee ${ appConfig.network.currency.symbol }` }
{ `Fee ${ appConfig.network.currency }` } </Link>
</Link> </Th>
</Th> </Tr>
</Tr> </TheadSticky>
</Thead> <Tbody>
<Tbody> { txs.map((item) => (
{ txs.map((item) => ( <TxsTableItem
<TxsTableItem key={ item.hash }
key={ item.hash } tx={ item }
tx={ item } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
...@@ -12,7 +12,6 @@ import { ...@@ -12,7 +12,6 @@ import {
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
PopoverBody, PopoverBody,
Portal,
useColorModeValue, useColorModeValue,
Show, Show,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
...@@ -40,7 +39,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -40,7 +39,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
<Tooltip label={ tx.from.implementation_name }> <Tooltip label={ tx.from.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.from.hash }/></Box> <Box display="flex"><AddressIcon hash={ tx.from.hash }/></Box>
</Tooltip> </Tooltip>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 }/> <AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address> </Address>
); );
...@@ -49,7 +48,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -49,7 +48,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
<Tooltip label={ tx.to.implementation_name }> <Tooltip label={ tx.to.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box> <Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box>
</Tooltip> </Tooltip>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 }/> <AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address> </Address>
); );
...@@ -57,28 +56,24 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -57,28 +56,24 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
return ( return (
<Tr> <Tr>
<Td pl={ 4 }> <Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 }> <Popover placement="right-start" openDelay={ 300 } isLazy>
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/> <TxAdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger> </PopoverTrigger>
<Portal> <PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }> <PopoverBody>
<PopoverBody> <TxAdditionalInfo tx={ tx }/>
<TxAdditionalInfo tx={ tx }/> </PopoverBody>
</PopoverBody> </PopoverContent>
</PopoverContent>
</Portal>
</> </>
) } ) }
</Popover> </Popover>
</Td> </Td>
<Td> <Td>
<VStack alignItems="start"> <VStack alignItems="start">
{ /* TODO: we don't recieve type from api */ } { tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack> </VStack>
</Td> </Td>
...@@ -89,33 +84,25 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -89,33 +84,25 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
hash={ tx.hash } hash={ tx.hash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
target="_self"
/> />
</Address> </Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> <Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
{ /* TODO: we don't recieve method from api */ } <TruncatedTextTooltip label={ tx.method }>
{ /* <TruncatedTextTooltip label={ tx.method }>
<Tag <Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' } colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
> >
{ tx.method } { tx.method }
</Tag> </Tag>
</TruncatedTextTooltip> */ }
<TruncatedTextTooltip label="CommitHash">
<Tag
colorScheme="gray"
>
CommitHash
</Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Td> </Td>
<Td> <Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> } { tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td> </Td>
{ /* TODO: fix "show" problem */ } <Show above="xl" ssr={ false }>
<Show above="xl">
<Td> <Td>
{ addressFrom } { addressFrom }
</Td> </Td>
...@@ -126,7 +113,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -126,7 +113,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
{ addressTo } { addressTo }
</Td> </Td>
</Show> </Show>
<Show below="xl"> <Show below="xl" ssr={ false }>
<Td colSpan={ 3 }> <Td colSpan={ 3 }>
<Box> <Box>
{ addressFrom } { addressFrom }
......
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;
import { Box, Show } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import sortTxs from 'lib/tx/sortTxs';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
sorting?: Sort;
sort: (field: 'val' | 'fee') => () => void;
}
const TxsWithSort = ({
txs,
sorting,
sort,
}: Props) => {
const [ sortedTxs, setSortedTxs ] = useState<TransactionsResponse['items']>(sortTxs(txs, sorting));
useEffect(() => {
setSortedTxs(sortTxs(txs, sorting));
}, [ sorting, txs ]);
return (
<>
<Show below="lg" ssr={ false }><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Show above="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
</>
);
};
export default TxsWithSort;
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { TransactionsResponse } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
const PAGINATION_FIELDS = [ 'block_number', 'index', 'items_count' ];
export default function useQueryWithPages(apiPath: string, queryName: QueryKeys, filters?: TTxsFilters) {
const queryClient = useQueryClient();
const router = useRouter();
const [ page, setPage ] = React.useState(1);
const currPageParams = pick(router.query, PAGINATION_FIELDS);
const [ pageParams, setPageParams ] = React.useState<Array<Partial<TransactionsResponse['next_page_params']>>>([ {} ]);
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, TransactionsResponse>(
[ queryName, { page, filters } ],
async() => {
const params: Array<string> = [];
Object.entries({ ...filters, ...currPageParams }).forEach(([ key, val ]) => {
if (Array.isArray(val)) {
val.length && params.push(`${ key }=${ val.join(',') }`);
} else if (val) {
params.push(`${ key }=${ val }`);
}
});
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
},
{ staleTime: Infinity },
);
const onNextPageClick = useCallback(() => {
if (!data?.next_page_params) {
// we hide next page button if no next_page_params
return;
}
// api adds filters into next-page-params now
// later filters will be removed from response
const nextPageParams = pick(data.next_page_params, PAGINATION_FIELDS);
if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]);
}
const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev + 1);
});
}, [ data, page, pageParams, router ]);
const onPrevPageClick = useCallback(() => {
// returning to the first page
// we dont have pagination params for the first page
let nextPageQuery: typeof router.query;
if (page === 2) {
queryClient.clear();
nextPageQuery = omit(router.query, PAGINATION_FIELDS);
} else {
const nextPageParams = { ...pageParams[page - 2] };
nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
}
router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev - 1);
});
}, [ router, page, pageParams, queryClient ]);
const resetPage = useCallback(() => {
queryClient.clear();
router.push({ pathname: router.pathname, query: omit(router.query, PAGINATION_FIELDS) }, undefined, { shallow: true }).then(() => {
animateScroll.scrollToTop({ duration: 0 });
setPage(1);
setPageParams([ {} ]);
});
}, [ router, queryClient ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0;
const pagination = {
page,
onNextPageClick,
onPrevPageClick,
hasPaginationParams,
resetPage,
};
return { data, isError, isLoading, pagination };
}
...@@ -11,6 +11,7 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -11,6 +11,7 @@ import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account'; import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
...@@ -96,17 +97,17 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -96,17 +97,17 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}; };
if (data) { if (data) {
// edit address // edit address
return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body }); return fetch<TWatchlistItem, WatchlistErrors>(`/node-api/account/watchlist/${ data.id }`, { method: 'PUT', body });
} else { } else {
// add address // add address
return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body }); return fetch<TWatchlistItem, WatchlistErrors>('/node-api/account/watchlist', { method: 'POST', body });
} }
} }
const { mutate } = useMutation(updateWatchlist, { const { mutate } = useMutation(updateWatchlist, {
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries([ 'watchlist' ]).then(() => { queryClient.refetchQueries([ QueryKeys.watchlist ]).then(() => {
onClose(); onClose();
setPending(false); setPending(false);
}); });
......
...@@ -8,7 +8,7 @@ import CheckboxInput from 'ui/shared/CheckboxInput'; ...@@ -8,7 +8,7 @@ import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network? // does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const; const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ appConfig.network.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ]; const NOTIFICATIONS_NAMES = [ appConfig.network.currency.symbol, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
type Props<Inputs extends FieldValues> = { type Props<Inputs extends FieldValues> = {
control: Control<Inputs>; control: Control<Inputs>;
......
...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account'; import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -20,11 +21,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -20,11 +21,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const fetch = useFetch(); const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' }); return fetch(`/node-api/account/watchlist/${ data?.id }`, { method: 'DELETE' });
}, [ data?.id, fetch ]); }, [ data?.id, fetch ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'watchlist' ], (prevData: TWatchlist | undefined) => { queryClient.setQueryData([ QueryKeys.watchlist ], (prevData: TWatchlist | undefined) => {
return prevData?.filter((item) => item.id !== data.id); return prevData?.filter((item) => item.id !== data.id);
}); });
}, [ data, queryClient ]); }, [ data, queryClient ]);
......
...@@ -25,7 +25,7 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -25,7 +25,7 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
{ appConfig.network.nativeTokenAddress && { appConfig.network.nativeTokenAddress &&
<TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> } <TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ appConfig.network.currency } balance:${ nbsp }` + nativeBalance }</Text> <Text color={ mainTextColor }>{ `${ appConfig.network.currency.symbol } balance:${ nbsp }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text> <Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack> </HStack>
{ item.tokens_count && ( { item.tokens_count && (
......
...@@ -27,7 +27,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -27,7 +27,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const { mutate } = useMutation(() => { const { mutate } = useMutation(() => {
const data = { ...item, notification_methods: { email: !notificationEnabled } }; const data = { ...item, notification_methods: { email: !notificationEnabled } };
return fetch(`/api/account/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) }); return fetch(`/node-api/account/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) });
}, { }, {
onError: () => { onError: () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
......
...@@ -52,7 +52,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -52,7 +52,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
setSwitchDisabled(true); setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } }; const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState); setNotificationEnabled(prevState => !prevState);
return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body }); return fetch(`/node-api/account/watchlist/${ item.id }`, { method: 'PUT', body });
}, { }, {
onError: () => { onError: () => {
showToast(); showToast();
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
Tbody, Tbody,
Tr, Tr,
Th, Th,
TableContainer,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,28 +19,26 @@ interface Props { ...@@ -20,28 +19,26 @@ interface Props {
const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => { const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return ( return (
<TableContainer width="100%"> <Table variant="simple" minWidth="600px">
<Table variant="simple" minWidth="600px"> <Thead>
<Thead> <Tr>
<Tr> <Th width="70%">Address</Th>
<Th width="70%">Address</Th> <Th width="30%">Private tag</Th>
<Th width="30%">Private tag</Th> <Th width="160px">Email notification</Th>
<Th width="160px">Email notification</Th> <Th width="108px"></Th>
<Th width="108px"></Th> </Tr>
</Tr> </Thead>
</Thead> <Tbody>
<Tbody> { data.map((item) => (
{ data.map((item) => ( <WatchlistTableItem
<WatchlistTableItem item={ item }
item={ item } key={ item.address_hash }
key={ item.address_hash } onDeleteClick={ onDeleteClick }
onDeleteClick={ onDeleteClick } onEditClick={ onEditClick }
onEditClick={ onEditClick } />
/> )) }
)) } </Tbody>
</Tbody> </Table>
</Table>
</TableContainer>
); );
}; };
......
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