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
NEXT_PUBLIC_BLOCKSCOUT_VERSION=APP_NEXT_NEXT_PUBLIC_BLOCKSCOUT_VERSION
NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK
NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=APP_NEXT_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE
NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME
NEXT_PUBLIC_NETWORK_SHORT_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_SHORT_NAME
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=APP_NEXT_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME
NEXT_PUBLIC_NETWORK_TYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_TYPE
NEXT_PUBLIC_NETWORK_SUBTYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_SUBTYPE
NEXT_PUBLIC_NETWORK_ID=APP_NEXT_NEXT_PUBLIC_NETWORK_ID
NEXT_PUBLIC_NETWORK_CURRENCY=APP_NEXT_NEXT_PUBLIC_NETWORK_CURRENCY
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=APP_NEXT_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=APP_NEXT_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED
NEXT_PUBLIC_FEATURED_NETWORKS=APP_NEXT_NEXT_PUBLIC_FEATURED_NETWORKS
NEXT_PUBLIC_APP_PROTOCOL=APP_NEXT_NEXT_PUBLIC_APP_PROTOCOL
NEXT_PUBLIC_APP_HOST=APP_NEXT_NEXT_PUBLIC_APP_HOST
NEXT_PUBLIC_APP_PORT=APP_NEXT_NEXT_PUBLIC_APP_PORT
NEXT_PUBLIC_API_ENDPOINT=APP_NEXT_NEXT_PUBLIC_API_ENDPOINT
NEXT_PUBLIC_API_BASE_PATH=APP_NEXT_NEXT_PUBLIC_API_BASE_PATH
# app config
NEXT_PUBLIC_APP_ENV=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_ENV__
NEXT_PUBLIC_APP_INSTANCE=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_INSTANCE__
NEXT_PUBLIC_APP_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PROTOCOL__
NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__
NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__
# network config
NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__
NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME__
NEXT_PUBLIC_NETWORK_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TYPE__
NEXT_PUBLIC_NETWORK_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_ID__
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_NAME__
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL__
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS__
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS__
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED__
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE
# ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__
NEXT_PUBLIC_FOOTER_GITHUB_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_GITHUB_LINK__
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:
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
deploy_and_tests:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
with:
valuesDir: deploy/values/e2e
appName: e2e-front
appNamespace: e2e-front-$GITHUB_SHA_SHORT
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
blockscoutIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT
frontendIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT
gethIngressHost: e2e-geth-$GITHUB_SHA_SHORT
scVerifierIngressHost: e2e-sc-verifier-$GITHUB_SHA_SHORT
secrets: inherit
name: Deploy review environment
on:
# push:
pull_request:
# push:
# branches-ignore:
# - 'main'
workflow_dispatch:
env:
......@@ -66,10 +68,10 @@ jobs:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values/review
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
......
......@@ -56,10 +56,10 @@ jobs:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values/review
env_vars: VALUES_DIR=deploy/values/main,APP_NAME=bs-stack
appNamespace: front-main
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:main
gethIngressHost: geth
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
......@@ -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))
- to spin up local dev server
- for predefined networks configs (see full available list in `package.json`) you can just run `yarn dev:<app_name>`
- for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev`
- for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev`
- navigate to the host from logs output
## Components visual testing
......@@ -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_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
| NEXT_PUBLIC_NETWORK_TYPE | `string` | Network type (used as first part of the base path) | `xdai` |
| NEXT_PUBLIC_NETWORK_SUBTYPE | `string` | Network subtype (used as second part of the base path) | `mainnet` |
| NEXT_PUBLIC_NETWORK_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_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_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_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### UI configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#featured-network-configuration-properties) | Configuration of featured networks that will be shown in the network menu | `[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]` |
| NEXT_PUBLIC_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_FOOTER_GITHUB_LINK | `string` *(optional)* | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_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_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
......@@ -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_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_ENV | `string` *(optional)* | Current app env (e.g development, review or production). Used for Sentry.io configuration | `production` |
### API configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_API_ENDPOINT | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API endpoint base URL in this variable | `https://blockscout.com` |
| NEXT_PUBLIC_API_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` |
......@@ -89,13 +94,71 @@ The app instance could be customized by passing following variables to NodeJS en
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| 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'` |
| 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
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` |
\ No newline at end of file
| 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 Sentry.io app | `<secret>` |
| 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 */
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 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 = [
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_APP_PORT ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '',
].join('');
process.env.NEXT_PUBLIC_VERCEL_URL || appHost,
appPort && ':' + appPort,
].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({
env,
isDev,
network: {
type: process.env.NEXT_PUBLIC_NETWORK_TYPE,
subtype: process.env.NEXT_PUBLIC_NETWORK_SUBTYPE,
logo: process.env.NEXT_PUBLIC_NETWORK_LOGO,
name: process.env.NEXT_PUBLIC_NETWORK_NAME,
id: process.env.NEXT_PUBLIC_NETWORK_ID,
shortName: process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME,
currency: process.env.NEXT_PUBLIC_NETWORK_CURRENCY,
assetsPathname: process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME,
nativeTokenAddress: process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS,
basePath: '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/'),
type: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TYPE) as PreDefinedNetwork | undefined,
logo: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_LOGO),
name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_NAME),
id: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ID),
shortName: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME),
currency: {
name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_NAME),
symbol: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL),
decimals: Number(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS)) || DEFAULT_CURRENCY_DECIMALS,
},
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: {
github: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK,
twitter: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK,
telegram: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK,
staking: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK,
github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK),
twitter: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK),
telegram: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK),
staking: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK),
},
featuredNetworks: process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"'),
blockScoutVersion: process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION,
isAccountSupported: process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?.replaceAll('\'', '"') === 'true',
marketplaceSubmitForm: process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM,
protocol: process.env.NEXT_PUBLIC_APP_PROTOCOL,
host: process.env.NEXT_PUBLIC_APP_HOST,
port: process.env.NEXT_PUBLIC_APP_PORT,
featuredNetworks: parseEnvJson<Array<FeaturedNetwork>>(getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS)) || [],
blockScoutVersion: getEnvValue(process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION),
isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
marketplaceAppList: parseEnvJson<Array<AppItemOverview>>(getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_APP_LIST)) || [],
marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
protocol: appSchema,
host: appHost,
port: appPort,
baseUrl,
logoutUrl,
api: {
endpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || 'https://blockscout.com',
basePath: process.env.NEXT_PUBLIC_API_BASE_PATH || '',
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
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
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
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_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
# marketplace config
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
# api config
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
\ No newline at end of file
NEXT_PUBLIC_API_HOST=blockscout.com
# nav and footer config
# ui config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]
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_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa
NEXT_PUBLIC_NETWORK_TYPE=poa
NEXT_PUBLIC_NETWORK_SUBTYPE=core
NEXT_PUBLIC_NETWORK_TYPE=poa_core
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_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
NEXT_PUBLIC_API_BASE_PATH=/poa/core
const BASE_PATH = require('../../lib/link/basePath.js');
const PATHS = require('../../lib/link/paths.js');
const PATHS = require('../../lib/link/paths');
const oldUrls = [
{
......@@ -41,17 +40,10 @@ const oldUrls = [
];
async function redirects() {
const homePagePath = '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/');
return [
{
source: '/',
destination: homePagePath,
permanent: false,
},
...oldUrls.map(item => {
const source = BASE_PATH.replaceAll('[', ':').replaceAll(']', '') + item.oldPath;
const destination = item.newPath.replaceAll('[', ':').replaceAll(']', '');
const source = item.oldPath;
const destination = item.newPath;
return { source, destination, permanent: false };
}),
];
......
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 [
{ source: '/astar/:slug*', destination: '/astar/mainnet/:slug*' },
{ source: '/shiden/:slug*', destination: '/shiden/mainnet/:slug*' },
];
{ source: '/node-api/:slug*', destination: '/api/:slug*' },
].filter(Boolean);
}
module.exports = rewrites;
......@@ -2,7 +2,7 @@ import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
export const config: Sentry.BrowserOptions = {
environment: process.env.VERCEL_ENV || process.env.NODE_ENV,
environment: process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_APP_ENV || process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ],
......
[
{
"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 'data/marketplaceApps.json' {
import type { AppItemOverview } from './types/client/apps';
const value: Array<AppItemOverview>;
export default value;
}
......@@ -15,9 +15,9 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str]
_default: ENC[AES256_GCM,data:v6LJLRwibjc6QgF1t6QNzy/FfL/i2G0mD2X6oxf0o4Q6A8cwovE03Wd5nXWTSyWrN95rJm0IQY7bjU3fXpDw0p/u4vu8dMpfM2bZMMOalXP4pA==,iv:VtNoyCEsesWn0PyWRXzLgbMcUa+voKa1sjsUY26RKnw=,tag:t02c9NVe4aGGo5feOP1OWQ==,type:str]
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:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY:
......@@ -65,21 +65,21 @@ geth:
frontend:
environment:
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:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:6MLOmBMJoB+dYoW8L4JYslO3F5tSFzhrkI+6rGo1a51s9sXZa9c=,iv:126unfUWSieigaq4Zne8321tSYoNy3EHk/qwEodqgH8=,tag:6BlrNkck5yANO2rqECjkuQ==,type:str]
NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-14T12:09:11Z"
mac: ENC[AES256_GCM,data:CnD+9hsC9ZyVhZPo+DXZfPH8svMuk50llaAm3JxgOlzhbJ4yp969WxLhZSORvj520b9geBPLZRU7ujLGiHKhrNzAK438LI2QttKQDt3WSbPwkIGDh/zuA201+gpT73awUNfMKCoHVjq4iQ6ty4KP/NCw1ZMcS/c1WVuRYE9RTl8=,iv:rl8eKiXwrBDjns2hiwJ6f28XyuhjH2soHeR1MBBu2Ig=,tag:vu5jGEPMkvmcl7m8huWl7g==,type:str]
lastmodified: "2022-11-02T07:59:42Z"
mac: ENC[AES256_GCM,data:OrV/dUWOtL23UFQLeIsKsGluTmse42d/4sgFMDs3UXdACsZu8twMt29Y/WaPHyq8Tpn5iYzhBLU6SCUmHxEhBNVzKBd5uCUbav1faS/zW6fSd9bEP7rmbUjaJGHliBkG3T4VCSZn53jR/OMNbSynIxZ0kRpVHr+RTcalaH7dLQ8=,iv:J3gXjFgFyZoPqL+VEnjkuKzA9UIIyK3UsvPWacBZKsY=,tag:/zY1jVjIm5BsDNJlfsg5uw==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
global:
env: e2e
env: main
# enable Blockscout deploy
blockscout:
app: blockscout
enabled: true
image:
_default: blockscout/blockscout:latest
......@@ -10,17 +11,28 @@ blockscout:
docker:
port: 80
targetPort: 4000
# init container
init:
enabled: true
image:
_default: blockscout/blockscout:latest
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
annotations: {}
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
#
tls:
enabled: true
path:
# - "/poa/sokol(/|$)(.*)"
- "/"
# probes
livenessProbe:
enabled: true
......@@ -41,7 +53,7 @@ blockscout:
_default: "2"
# enable service to connect to RDS
rds:
enable: false
enabled: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
......@@ -81,15 +93,27 @@ blockscout:
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: true
_default: 'true'
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_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:
enabled: true
......@@ -153,7 +177,7 @@ geth:
ingress:
enabled: true
host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com
_default: node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
......@@ -180,7 +204,6 @@ scVerifier:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
......@@ -247,6 +270,7 @@ scVerifier:
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
app: blockscout
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
......@@ -257,12 +281,21 @@ frontend:
targetPort: 3000
ingress:
enabled: true
# annotations:
# - 'nginx.ingress.kubernetes.io/use-regex: "true"'
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
path:
# - "/(apps|auth/profile|account)"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/account"
resources:
limits:
memory:
......@@ -279,20 +312,16 @@ frontend:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_APP_PROTOCOL:
_default: http
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE:
_default: local
_default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
......@@ -304,18 +333,30 @@ frontend:
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
_default: poa_core
NEXT_PUBLIC_NETWORK_ID:
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
_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_HOST:
_default: blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_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:
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:MkvI0EZaWmsd8tDMU+qiLLZ2anCl00PW3u3Og9GXNtnZ+a7CUyY6rdFBOfE3fSOh4haoNuBxMCyxsAhHk/x0MNRNVqKjqT9I3VBAfzUzM+Ft4g==,iv:RuHXRpEAYKlm2UwN3s6AofiGiSmuRkVyO531rnUIklc=,tag:L3NnykZU4WFOOfPGDCdMUg==,type:str]
_default: ENC[AES256_GCM,data:dQMvW68eW5EokGgekC1HsozsMAq+eqWwqfocgF6QR9+1VA1z5efNbMjTuWJqaSzXq/yWwfAogP8GJSzpS8T7jKATOeVxZ71I4LmlFO7jR4wMKw==,iv:UA0DcnH7z6y5X9C9Qk3WvRrgI3LT/UvH4Fl7gb765/0=,tag:cn78k5f+L8ed0WV3mDKykg==,type:str]
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:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY:
......@@ -70,16 +70,16 @@ frontend:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:KVzLMvMQc6f37c+OWu7vpAdDYwKI048XVY7EVijS2zz6jRRFacg=,iv:LeWSYZeaPdBsOxcGcca8L1Rp3ilsR+R13icX8Q/VUBA=,tag:Bj6NTTqb3R8oxAprZ+7CVQ==,type:str]
NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-14T12:08:39Z"
mac: ENC[AES256_GCM,data:esHv2aUvW4lMmoD5yRWu4OJEpOMkCa7TOyPS0HDkQL25g4TOdE+AfVZBE5wLeL1rePaId4bHnX0sF2Tov0d8xhCH3mv6+Vvmgi+75Oqu8+logQ4LyZSI0yIcvmdGVHhaO6u3u1qwYXHrityIVmiXQdBck5oq67uyT+jtSh1pXpc=,iv:YNWhRxSY0WLRm+wbVURpfxU2K67MAB5dpSILSMy9oCE=,tag:TsCcsGmMG4id5ITR5LrDjg==,type:str]
lastmodified: "2022-11-01T14:58:11Z"
mac: ENC[AES256_GCM,data:3AK4GRnUnAcQrdJ9JrdhSFqMYmYhE2RGiP+NPvO+mqBGDH26pRjN3lkNhHi/uObEQWiQZJLzLEOhSPl0/oDVYRTSGEeEiIlViEm/S5PuD57uFx6ogS9Iz88G/3hnc3HpTAIg2+NVwE7wF1/NK75WlivB1pUGk7OrazhZ+Fhyn5k=,iv:94YFEq/dmS07CqsKFZ2NAKMj3LqqyUB4+2XtHI3UiLg=,tag:4uti1G9eRoL2AwF0n7avdQ==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
global:
env: e2e
env: review
# enable Blockscout deploy
blockscout:
app: blockscout
enabled: true
image:
_default: blockscout/blockscout:latest
......@@ -10,17 +11,28 @@ blockscout:
docker:
port: 80
targetPort: 4000
# init container
init:
enabled: true
image:
_default: blockscout/blockscout:latest
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
annotations: {}
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
#
tls:
enabled: true
path:
# - "/poa/sokol(/|$)(.*)"
- "/"
# probes
livenessProbe:
enabled: true
......@@ -41,7 +53,7 @@ blockscout:
_default: "2"
# enable service to connect to RDS
rds:
enable: false
enabled: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
......@@ -81,15 +93,27 @@ blockscout:
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: true
_default: 'true'
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_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:
enabled: true
......@@ -153,7 +177,7 @@ geth:
ingress:
enabled: true
host:
_default: asdfg-node.test.blockscout.aws-k8s.blockscout.com
_default: node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
......@@ -180,7 +204,6 @@ scVerifier:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
......@@ -247,6 +270,7 @@ scVerifier:
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
app: blockscout
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
......@@ -257,12 +281,21 @@ frontend:
targetPort: 3000
ingress:
enabled: true
# annotations:
# - 'nginx.ingress.kubernetes.io/use-regex: "true"'
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
testnet: asdfg-frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
path:
# - "/(apps|auth/profile|account)"
- "/apps"
- "/_next"
- "/node-api"
- "/static"
- "/auth/profile"
- "/account"
resources:
limits:
memory:
......@@ -279,16 +312,16 @@ frontend:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE:
_default: review
_default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
......@@ -298,20 +331,32 @@ frontend:
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: sokol
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: sokol
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core
NEXT_PUBLIC_NETWORK_ID:
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
_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_HOST:
_default: blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_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 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);
export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
export const ZERO = new BigNumber(0);
export const SECOND = 1_000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
......@@ -6,11 +6,12 @@ import isBrowser from './isBrowser';
export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed',
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()) {
return undefined;
return serverCookie ? getFromCookieString(serverCookie, name) : undefined;
}
return Cookies.get(name);
}
......@@ -20,3 +21,7 @@ export function set(name: string, value: string, attributes: Types.CookieAttribu
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 featuredNetworks from 'lib/networks/featuredNetworks';
import getMarketplaceApps from '../getMarketplaceApps';
const KEY_WORDS = {
BLOB: 'blob:',
DATA: 'data:',
......@@ -29,11 +27,22 @@ function getNetworksExternalAssets() {
}
function getMarketplaceAppsOrigins() {
return getMarketplaceApps().map(({ url }) => url);
return appConfig.marketplaceAppList.map(({ url }) => url);
}
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() {
......@@ -53,6 +62,8 @@ function makePolicyMap() {
// client error monitoring
'sentry.io', '*.sentry.io',
appConfig.api.socket,
],
'script-src': [
......@@ -92,11 +103,17 @@ function makePolicyMap() {
// github avatars
'avatars.githubusercontent.com',
// other github assets (e.g trustwallet token icons)
'raw.githubusercontent.com',
// auth0 assets
's.gravatar.com',
// network assets
...networkExternalAssets.map((url) => url.host),
// marketplace apps logos
...getMarketplaceAppsLogosOrigins(),
...getMarketplaceAppsLogosOrigins().map((url) => url.host),
],
'font-src': [
......@@ -134,7 +151,8 @@ function getCspPolicy() {
return;
}
return [ key, value.join(' ') ].join(' ');
const uniqueValues = unique(value);
return [ key, uniqueValues.join(' ') ].join(' ');
})
.filter(Boolean)
.join(';');
......
import data from 'data/marketplaceApps.json';
export default function getMarketplaceApps() {
return data;
}
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
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';
interface Error {
......@@ -14,14 +16,12 @@ interface Error {
export default function useFetchProfileInfo() {
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>([ 'profile' ], async() => {
return fetch(url.toString(), { credentials: 'include' });
return useQuery<unknown, Error, UserInfo>([ QueryKeys.profile ], async() => {
const url = new URL(`${ appConfig.api.basePath }/api/account/v1/user/info`, appConfig.api.endpoint);
return fetch(url.toString(), { credentials: appConfig.isDev ? 'include' : 'same-origin' });
}, {
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';
export default function useIsMobile() {
return useBreakpointValue({ base: true, lg: false });
export default function useIsMobile(ssr = true) {
return useBreakpointValue({ base: true, lg: false }, { ssr });
}
import React, { useMemo } from 'react';
import React from 'react';
import appConfig from 'configs/app/config';
import marketplaceApps from 'data/marketplaceApps.json';
import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg';
......@@ -18,20 +17,17 @@ import useCurrentRoute from 'lib/link/useCurrentRoute';
import notEmpty from 'lib/notEmpty';
export default function useNavItems() {
const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainIds.includes(appConfig.network.id)),
[ ])
.length > 0;
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0;
const currentRoute = useCurrentRoute()();
return React.useMemo(() => {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
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'
// 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
......@@ -39,14 +35,14 @@ export default function useNavItems() {
].filter(notEmpty);
const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
{ text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' },
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' },
{ 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'), isNewUi: true },
{ 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', isNewUi: true },
{ 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 };
}, [ 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(
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) => {
if (paramName === 'network_sub_type' && !refinedUrlParams.network_sub_type) {
return '';
}
let paramValue = refinedUrlParams?.[paramName];
let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dunno know how to manage it, fix me if you find an issue
......@@ -42,5 +32,5 @@ export default function link(
url.searchParams.append(key, value);
});
return url.toString();
return url.pathname;
}
const BASE_PATH = require('./basePath');
const paths = {
network_index: `${ BASE_PATH }`,
watchlist: `${ BASE_PATH }/account/watchlist`,
private_tags: `${ BASE_PATH }/account/tag_address`,
public_tags: `${ BASE_PATH }/account/public_tags_request`,
api_keys: `${ BASE_PATH }/account/api_key`,
custom_abi: `${ BASE_PATH }/account/custom_abi`,
profile: `${ BASE_PATH }/auth/profile`,
txs: `${ BASE_PATH }/txs`,
tx: `${ BASE_PATH }/tx/[id]`,
blocks: `${ BASE_PATH }/blocks`,
block: `${ BASE_PATH }/block/[id]`,
tokens: `${ BASE_PATH }/tokens`,
token_index: `${ BASE_PATH }/token/[hash]`,
token_instance_item: `${ BASE_PATH }/token/[hash]/instance/[id]`,
address_index: `${ BASE_PATH }/address/[id]`,
address_contract_verification: `${ BASE_PATH }/address/[id]/contract_verifications/new`,
apps: `${ BASE_PATH }/apps`,
app_index: `${ BASE_PATH }/apps/[id]`,
search_results: `${ BASE_PATH }/search-results`,
other: `${ BASE_PATH }/search-results`,
// no slash required, it is correct
auth: `${ BASE_PATH }auth/auth0`,
network_index: `/`,
watchlist: `/account/watchlist`,
private_tags: `/account/tag_address`,
public_tags: `/account/public_tags_request`,
api_keys: `/account/api_key`,
custom_abi: `/account/custom_abi`,
profile: `/auth/profile`,
txs: `/txs`,
tx: `/tx/[id]`,
blocks: `/blocks`,
block: `/block/[id]`,
tokens: `/tokens`,
token_index: `/token/[hash]`,
token_instance_item: `/token/[hash]/instance/[id]`,
address_index: `/address/[id]`,
address_contract_verification: `/address/[id]/contract_verifications/new`,
apps: `/apps`,
app_index: `/apps/[id]`,
search_results: `/search-results`,
other: `/search-results`,
auth: `/auth/auth0`,
};
module.exports = paths;
import type { FeaturedNetwork } from 'types/networks';
import type { FeaturedNetwork, PreDefinedNetwork } from 'types/networks';
import appConfig from 'configs/app/config';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
......@@ -12,97 +12,99 @@ import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': gnosisIcon,
'/xdai/optimism': optimismIcon,
'/xdai/aox': arbitrumIcon,
'/eth/mainnet': ethereumIcon,
'/etc/mainnet': ethereumClassicIcon,
'/poa/core': poaIcon,
'/rsk/mainnet': rskIcon,
'/xdai/testnet': arbitrumIcon,
'/poa/sokol': poaSokolIcon,
'/artis/sigma1': artisIcon,
const ICONS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = {
xdai_mainnet: gnosisIcon,
xdai_optimism: optimismIcon,
xdai_aox: arbitrumIcon,
eth_mainnet: ethereumIcon,
etc_mainnet: ethereumClassicIcon,
poa_core: poaIcon,
rsk_mainnet: rskIcon,
xdai_testnet: arbitrumIcon,
poa_sokol: poaSokolIcon,
artis_sigma1: artisIcon,
};
// for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([
// {
// title: 'Gnosis Chain',
// basePath: '/xdai/mainnet',
// url: 'https://blockscout.com/xdai/mainnet',
// group: 'mainnets',
// type: 'xdai_mainnet',
// },
// {
// title: 'Optimism on Gnosis Chain',
// basePath: '/xdai/optimism',
// url: 'https://blockscout.com/xdai/optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// type: 'xdai_optimism',
// },
// {
// title: 'Arbitrum on xDai',
// basePath: '/xdai/aox',
// url: 'https://blockscout.com/xdai/aox',
// group: 'mainnets',
// },
// {
// title: 'Ethereum',
// basePath: '/eth/mainnet',
// url: 'https://blockscout.com/eth/mainnet',
// group: 'mainnets',
// type: 'eth_mainnet',
// },
// {
// title: 'Ethereum Classic',
// basePath: '/etx/mainnet',
// url: 'https://blockscout.com/etx/mainnet',
// group: 'mainnets',
// type: 'etc_mainnet',
// },
// {
// title: 'POA',
// basePath: '/poa/core',
// url: 'https://blockscout.com/poa/core',
// group: 'mainnets',
// type: 'poa_core',
// },
// {
// title: 'RSK',
// basePath: '/rsk/mainnet',
// url: 'https://blockscout.com/rsk/mainnet',
// group: 'mainnets',
// type: 'rsk_mainnet',
// },
// {
// title: 'Gnosis Chain Testnet',
// basePath: '/xdai/testnet',
// url: 'https://blockscout.com/xdai/testnet',
// group: 'testnets',
// type: 'xdai_testnet',
// },
// {
// title: 'POA Sokol',
// basePath: '/poa/sokol',
// url: 'https://blockscout.com/poa/sokol',
// group: 'testnets',
// type: 'poa_sokol',
// },
// {
// title: 'ARTIS Σ1',
// basePath: '/artis/sigma1',
// url: 'https://blockscout.com/artis/sigma1',
// group: 'other',
// type: 'artis_sigma1',
// },
// {
// title: 'LUKSO L14',
// basePath: '/lukso/l14',
// url: 'https://blockscout.com/lukso/l14',
// group: 'other',
// type: 'lukso_l14',
// },
// {
// title: 'Astar',
// basePath: '/astar',
// url: 'https://blockscout.com/astar',
// group: 'other',
// type: 'astar',
// },
// ]).replaceAll('"', '\'');
function parseNetworkConfig() {
try {
return JSON.parse(appConfig.featuredNetworks || '[]');
} catch (error) {
return [];
}
}
const featuredNetworks: Array<FeaturedNetwork> = (() => {
const networksFromConfig: Array<FeaturedNetwork> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
return appConfig.featuredNetworks.map((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 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';
export default function useNetworkNavigationItems() {
const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName];
const router = useRouter();
return React.useMemo(() => {
return featuredNetworks.map((network) => {
const routeName = 'crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation ? currentRouteName : 'network_index';
const [ , networkType, networkSubtype ] = network.basePath.split('/');
const url = link(routeName, { ...router.query, network_type: networkType, network_sub_type: networkSubtype });
return {
...network,
url: url,
isActive: appConfig.network.basePath === network.basePath,
isActive: network.type ? appConfig.network.type === network.type : false,
};
});
}, [ currentRoute, currentRouteName, router.query ]);
}, []);
}
export type PageParams = {
network_type: string;
network_sub_type: string;
id: string;
}
export type PageParams = {
network_type: string;
network_sub_type: string;
}
export type PageParams = unknown
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 = {
network_type: string;
network_sub_type: 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) {
return;
}
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
if (appConfig.network.type !== networkType && appConfig.network.subtype !== networkSubtype) {
const url = req.nextUrl.clone();
url.pathname = `/404`;
return NextResponse.rewrite(url);
}
// we don't have any info from router here, so just do straight forward sub-string search (sorry)
const isAccountRoute = req.nextUrl.pathname.includes('/account/');
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (isAccountRoute && !apiToken) {
const authUrl = link('auth', networkParams);
if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) {
const authUrl = link('auth');
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 { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import { AppWrapper } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme';
......@@ -30,12 +31,14 @@ function MyApp({ Component, pageProps }: AppProps) {
}));
return (
<QueryClientProvider client={ queryClient }>
<ChakraProvider theme={ theme }>
<Component { ...pageProps }/>
</ChakraProvider>
<ReactQueryDevtools/>
</QueryClientProvider>
<AppWrapper pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<Chakra theme={ theme } cookies={ pageProps.cookies }>
<Component { ...pageProps }/>
</Chakra>
<ReactQueryDevtools/>
</QueryClientProvider>
</AppWrapper>
);
}
......
......@@ -13,10 +13,10 @@ class MyDocument extends Document {
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="icon" sizes="32x32" type="image/png" href="/favicon-32x32.png"/>
<link rel="icon" sizes="16x16" type="image/png"href="/favicon-16x16.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/>
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"/>
<link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
<link rel="icon" sizes="16x16" type="image/png"href="/static/favicon-16x16.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/>
<link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#5bbad5"/>
</Head>
<body>
<ColorModeScript initialColorMode={ theme.config.initialColorMode }/>
......
......@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import ApiKeys from 'ui/pages/ApiKeys';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const ApiKeysPage: NextPage<Props> = () => {
const ApiKeysPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
......@@ -26,5 +17,4 @@ const ApiKeysPage: NextPage<Props> = () => {
export default ApiKeysPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import CustomAbi from 'ui/pages/CustomAbi';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const CustomAbiPage: NextPage<Props> = () => {
const CustomAbiPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
......@@ -26,5 +17,4 @@ const CustomAbiPage: NextPage<Props> = () => {
export default CustomAbiPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PublicTags from 'ui/pages/PublicTags';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const PublicTagsPage: NextPage<Props> = () => {
const PublicTagsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
......@@ -26,5 +17,4 @@ const PublicTagsPage: NextPage<Props> = () => {
export default PublicTagsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PrivateTags from 'ui/pages/PrivateTags';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = () => {
const AddressTagsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
......@@ -26,5 +17,4 @@ const AddressTagsPage: NextPage<Props> = () => {
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import WatchList from 'ui/pages/Watchlist';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const WatchListPage: NextPage<Props> = () => {
const WatchListPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
......@@ -28,5 +19,4 @@ const WatchListPage: NextPage<Props> = () => {
export default WatchListPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
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 getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
const searchParamsStr = new URLSearchParams(searchParams).toString();
const searchParamsStr = getSearchParams(req);
return `/v2/transactions/${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
......@@ -5,7 +6,7 @@ import Apps from 'ui/pages/Apps';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage = () => {
const AppsPage: NextPage = () => {
return (
<Page>
<PageTitle text="Apps"/>
......@@ -18,5 +19,4 @@ const AppsPage = () => {
export default AppsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react';
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 EmptySearchResult from 'ui/apps/EmptySearchResult';
import MarketplaceApp from 'ui/pages/MarketplaceApp';
......@@ -23,7 +23,7 @@ const AppPage: NextPage = () => {
return;
}
const app = marketplaceApps.find((app) => app.id === id);
const app = appConfig.marketplaceAppList.find((app) => app.id === id);
setApp(app);
setIsLoading(false);
}, [ id ]);
......@@ -47,5 +47,4 @@ const AppPage: NextPage = () => {
export default AppPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -15,5 +15,4 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -17,5 +17,4 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextPage } from 'next';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage';
type Props = {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = () => {
const BlockPage: NextPage = () => {
return (
<BlocksNextPage/>
);
......@@ -17,5 +11,4 @@ const BlockPage: NextPage<Props> = () => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
const Home: NextPage = () => {
return null;
import Home from 'ui/pages/Home';
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) => {
export default TransactionPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -5,16 +5,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = () => {
const TxsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
......@@ -24,7 +15,6 @@ const AddressTagsPage: NextPage<Props> = () => {
);
};
export default AddressTagsPage;
export default TxsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -16,10 +16,10 @@ const variantSimple = definePartsStyle((props) => {
th: {
border: 0,
color: mode('gray.600', 'whiteAlpha.700')(props),
backgroundColor: mode('blackAlpha.100', 'whiteAlpha.200')(props),
...transitionProps,
},
thead: {
backgroundColor: mode('blackAlpha.100', 'whiteAlpha.200')(props),
...transitionProps,
},
td: {
......@@ -73,9 +73,6 @@ const variants = {
};
const baseStyle = definePartsStyle({
thead: {
backgroundColor: 'gray.50',
},
th: {
textTransform: 'none',
fontFamily: 'body',
......@@ -83,6 +80,12 @@ const baseStyle = definePartsStyle({
overflow: 'hidden',
color: 'gray.500',
letterSpacing: 'none',
_first: {
borderTopLeftRadius: '8px',
},
_last: {
borderTopRightRadius: '8px',
},
},
td: {
fontSize: 'md',
......@@ -92,7 +95,7 @@ const baseStyle = definePartsStyle({
tableLayout: 'fixed',
borderTopLeftRadius: 'base',
borderTopRightRadius: 'base',
overflow: 'hidden',
overflow: 'unset',
fontVariant: 'normal',
},
});
......
const breakpoints = {
// maybe we need them in future
sm: '414px',
sm: '415px',
// md: '768px',
lg: '1000px',
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';
import colors from './foundations/colors';
import transition from './foundations/transition';
import typography from './foundations/typography';
import zIndices from './foundations/zIndices';
import global from './global';
const overrides = {
......@@ -20,6 +21,7 @@ const overrides = {
},
breakpoints,
transition,
zIndices,
};
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 {
hash: string;
implementation_name: string;
name: string;
name: string | null;
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 { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
export type BlockType = 'block' | 'reorg' | 'uncle';
......@@ -37,3 +38,12 @@ export interface BlocksResponse {
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 {
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: number;
value: string;
index: number;
block: number;
timestamp: string;
gas_limit: string;
}
export interface InternalTransactionsResponse {
......
export type JsonRpcUrlResponse = {
json_rpc_url: string;
}
export interface Reward {
reward: number;
reward: string;
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 { 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;
token_id: string;
}
export type TokenTransfer = (
{
token_type: 'ERC-20';
total: {
value: string;
};
token: TokenInfoGeneric<'ERC-20'>;
total: Erc20TotalPayload;
} |
{
token_type: 'ERC-721';
total: {
token_id: string;
};
token: TokenInfoGeneric<'ERC-721'>;
total: Erc721TotalPayload;
} |
{
token_type: 'ERC-1155';
total: ERC1155TotalPayload | Array<ERC1155TotalPayload>;
token: TokenInfoGeneric<'ERC-1155'>;
total: Erc1155TotalPayload | Array<Erc1155TotalPayload>;
}
) & TokenTransferBase
interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
txHash: string;
tx_hash: string;
from: AddressParam;
to: AddressParam;
token_address: string;
token_symbol: string;
exchange_rate: string;
}
......@@ -38,6 +38,9 @@ export interface Transaction {
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string;
method: string;
tx_types: Array<TransactionType>;
tx_tag: string | null;
}
export interface TransactionsResponse {
......@@ -46,5 +49,7 @@ export interface TransactionsResponse {
block_number: number;
index: 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 = {
}
export type AppItemOverview = AppItemPreview & {
chainIds: Array<string>;
author: string;
url: 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';
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 {
title: string;
basePath: string;
url: string;
group: NetworkGroup;
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';
import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
......@@ -47,17 +48,17 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const body = { name: data.name };
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, {
onSuccess: async(data) => {
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);
if (isExisting) {
......
......@@ -4,7 +4,6 @@ import {
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
......@@ -21,26 +20,24 @@ interface Props {
const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th>{ `API key token (limit ${ limit } keys)` }</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ApiKeyTableItem
item={ item }
key={ item.api_key }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th>{ `API key token (limit ${ limit } keys)` }</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ApiKeyTableItem
item={ item }
key={ item.api_key }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
);
};
......
......@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal';
......@@ -18,11 +19,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const fetch = useFetch();
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 ]);
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);
});
}, [ data, queryClient ]);
......
......@@ -7,7 +7,7 @@ import React, { useCallback } from 'react';
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 ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg';
......@@ -43,7 +43,7 @@ const AppModal = ({
twitter,
logo,
categories,
} = marketplaceApps.find(app => app.id === id) as AppItemOverview;
} = appConfig.marketplaceAppList.find(app => app.id === id) as AppItemOverview;
const socialLinks = [
telegram ? {
......@@ -206,7 +206,7 @@ const AppModal = ({
</Link>
) }
{ socialLinks.length && (
{ socialLinks.length > 0 && (
<List
marginLeft={{ sm: 'auto' }}
display="grid"
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import appConfig from 'configs/app/config';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem';
......@@ -20,6 +21,10 @@ type Props = {
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
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 (
<Menu>
......@@ -43,7 +48,7 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
</MenuButton>
<MenuList zIndex={ 3 }>
{ categoriesList.map((category: MarketplaceCategory) => (
{ displayedCategories.map((category: MarketplaceCategory) => (
<CategoriesMenuItem
key={ category.id }
id={ category.id }
......
......@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import appConfig from 'configs/app/config';
import marketplaceApps from 'data/marketplaceApps.json';
const favoriteAppsLocalStorageKey = 'favoriteApps';
......@@ -79,8 +78,7 @@ export default function useMarketplaceApps() {
}, [ filterQuery, category, filterApps ]);
useEffect(() => {
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainIds.includes(appConfig.network.id))
const defaultDisplayedApps = [ ...appConfig.marketplaceAppList ]
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import type { Block } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg';
......@@ -17,6 +19,7 @@ import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -34,8 +37,8 @@ const BlockDetails = () => {
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ErrorType<{ status: number }>, Block>(
[ 'block', router.query.id ],
async() => await fetch(`/api/blocks/${ router.query.id }`),
[ QueryKeys.block, router.query.id ],
async() => await fetch(`/node-api/blocks/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
......@@ -69,6 +72,8 @@ const BlockDetails = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
const validatorTitle = getNetworkValidatorTitle();
return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<DetailsInfoItem
......@@ -104,17 +109,17 @@ const BlockDetails = () => {
title="Transactions"
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
</Link>
</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."
columnGap={ 1 }
>
<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 */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
......@@ -122,12 +127,12 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Block reward"
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.`
}
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)) && (
<Text variant="secondary" whiteSpace="break-spaces">(
<Tooltip label="Static block reward">
......@@ -153,6 +158,19 @@ const BlockDetails = () => {
) }
</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 }
......@@ -180,7 +198,7 @@ const BlockDetails = () => {
title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency } </Text>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text>
<Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
</Text>
......@@ -189,13 +207,13 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Burnt fees"
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.`
}
>
<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) && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
......@@ -212,13 +230,13 @@ const BlockDetails = () => {
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency }
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</DetailsInfoItem>
) }
{ /* api doesn't support extra data yet */ }
{ /* <DetailsInfoItem
title="Extra data"
hint="Any data that can be included by the miner in the block."
hint={ `Any data that can be included by the ${ validatorTitle } in the block.` }
>
<Text whiteSpace="pre">{ data.extra_data } </Text>
<Text variant="secondary">(Hex: { data.extra_data })</Text>
......@@ -247,7 +265,7 @@ const BlockDetails = () => {
<DetailsInfoItem
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() }
</DetailsInfoItem>
......@@ -293,17 +311,6 @@ const BlockDetails = () => {
>
{ data.nonce }
</DetailsInfoItem>
{ data.rewards
?.filter(({ type }) => type !== 'Validator Reward' && type !== 'Miner Reward')
.map(({ type, reward }) => (
<DetailsInfoItem
key={ type }
title={ type }
hint="Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees."
>
{ BigNumber(reward).dividedBy(WEI).toFixed() } { appConfig.network.currency }
</DetailsInfoItem>
)) }
</>
) }
</Grid>
......
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from 'ui/txs/TxsContent';
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;
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';
import React from 'react';
import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import BlocksList from 'ui/blocks/BlocksList';
......@@ -20,8 +21,8 @@ const BlocksContent = ({ type }: Props) => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ 'blocks', type ],
async() => await fetch(`/api/blocks${ type ? `?type=${ type }` : '' }`),
[ QueryKeys.blocks, type ],
async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`),
);
if (isLoading) {
......@@ -48,11 +49,12 @@ const BlocksContent = ({ type }: Props) => {
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 above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show>
<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>
</>
);
......
import { Box } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { Block } from 'types/api/block';
......@@ -12,7 +13,10 @@ interface Props {
const BlocksList = ({ data }: Props) => {
return (
<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>
);
};
......
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 capitalize from 'lodash/capitalize';
import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import { WEI, ZERO } from 'lib/consts';
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 AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
......@@ -18,17 +19,21 @@ import Utilization from 'ui/shared/Utilization';
interface Props {
data: Block;
isPending?: boolean;
enableTimeIncrement?: boolean;
}
const BlocksListItem = ({ data, isPending }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const { totalReward, burntFees, txFees } = getBlockReward(data);
const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
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 (
<AccountListItemMobile rowGap={ 3 }>
<AccountListItemMobile rowGap={ 3 } key={ String(data.height) }>
<Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm" color="blue.500" emptyColor={ spinnerEmptyColor }/> }
{ isPending && <Spinner size="sm"/> }
<Link
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
......@@ -36,14 +41,14 @@ const BlocksListItem = ({ data, isPending }: Props) => {
{ data.height }
</Link>
</Flex>
<Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Miner</Text>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/>
</Flex>
<Flex columnGap={ 2 }>
......@@ -52,22 +57,26 @@ const BlocksListItem = ({ data, isPending }: Props) => {
</Flex>
<Box>
<Text fontWeight={ 500 }>Gas used</Text>
<Flex columnGap={ 4 }>
<Flex columnGap={ 4 } mt={ 2 }>
<Text variant="secondary">{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
</Flex>
</Box>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.div(WEI).toFixed() }</Text>
</Flex>
<Flex>
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" ml={ 2 }/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
</Box>
</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 type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlocksTableItem from 'ui/blocks/BlocksTableItem';
import { default as Thead } from 'ui/shared/TheadSticky';
interface Props {
data: Array<Block>;
......@@ -13,24 +17,25 @@ interface Props {
const BlocksTable = ({ data }: Props) => {
return (
<TableContainer width="100%" mt={ 8 }>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead>
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size</Th>
<Th width="21%" minW="144px">Miner</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency }</Th>
<Th width="22%">Burnt fees { appConfig.network.currency }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item }/>) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 } mt={ 8 }>
<Thead top={ 0 }>
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size</Th>
<Th width="21%" minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency.symbol }</Th>
<Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
<Tbody>
<AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement/>) }
</AnimatePresence>
</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 { motion } from 'framer-motion';
import React from 'react';
import type { Block } from 'types/api/block';
import flameIcon from 'icons/flame.svg';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import { WEI, ZERO } from 'lib/consts';
import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization';
......@@ -16,15 +16,27 @@ import Utilization from 'ui/shared/Utilization';
interface Props {
data: Block;
isPending?: boolean;
enableTimeIncrement?: boolean;
}
const BlocksTableItem = ({ data, isPending }: Props) => {
const { totalReward, burntFees, txFees } = getBlockReward(data);
const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
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 (
<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">
<Flex columnGap={ 2 } alignItems="center">
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<Link
......@@ -35,7 +47,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => {
</Link>
</Tooltip>
</Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm">
......
......@@ -13,6 +13,7 @@ import type { ControllerRenderProps, SubmitHandler } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
......@@ -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 };
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');
......@@ -63,7 +64,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const mutation = useMutation(customAbiKey, {
onSuccess: (data) => {
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);
if (isExisting) {
......
......@@ -4,7 +4,6 @@ import {
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
......@@ -20,26 +19,24 @@ interface Props {
const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th>ABI for Smart contract address (0x...)</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<CustomAbiTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th>ABI for Smart contract address (0x...)</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<CustomAbiTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
);
};
......
......@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import DeleteModal from 'ui/shared/DeleteModal';
......@@ -17,11 +18,11 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
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 ]);
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);
});
}, [ data, queryClient ]);
......
......@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -30,7 +31,7 @@ const ApiKeysPage: React.FC = () => {
const [ apiKeyModalData, setApiKeyModalData ] = 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) => {
setApiKeyModalData(data);
......
import { Box, Icon, Link } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg';
import useFetch from 'lib/hooks/useFetch';
import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput';
import useMarketplaceApps from '../apps/useMarkeplaceApps';
import useMarketplaceApps from '../apps/useMarketplaceApps';
const Apps = () => {
const fetch = useFetch();
const {
isLoading,
category,
......@@ -24,6 +30,11 @@ const Apps = () => {
handleFavoriteClick,
} = useMarketplaceApps();
useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ],
async() => await fetch(`/node-api/config/json-rpc-url`),
);
return (
<>
<Box
......
......@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -27,7 +28,8 @@ const CustomAbiPage: React.FC = () => {
const [ customAbiModalData, setCustomAbiModalData ] = 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) => {
setCustomAbiModalData(data);
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import * as Sentry from '@sentry/react';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react';
......@@ -11,7 +10,6 @@ import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => {
const router = useRouter();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
......@@ -46,7 +44,7 @@ const Home = () => {
});
}, [ 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 (
<Page>
......
import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
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 appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch';
import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
......@@ -14,32 +18,51 @@ type Props = {
const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode();
const fetch = useFetch();
const ref = useRef<HTMLIFrameElement>(null);
const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false);
}, []);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' +
'allow-top-navigation-by-user-activation allow-popups';
const allowAttributeValue = 'clipboard-read; clipboard-write;';
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ],
async() => await fetch(`/node-api/config/json-rpc-url`),
{ refetchOnMount: false },
);
useEffect(() => {
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 (
<Page wrapChildren={ false }>
<Center
as="main"
h="100vh"
paddingTop={{ base: '138px', lg: 0 }}
pt={{ base: '138px', lg: 0 }}
pb={{ base: 0, lg: 10 }}
>
{ (isFrameLoading) && (
<ContentLoader/>
......
......@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => {
const { data, isLoading, isError } = useFetchProfileInfo();
const { data, isLoading, isError, isFetched } = useFetchProfileInfo();
const content = (() => {
if (isLoading) {
......@@ -22,7 +22,7 @@ const MyProfile = () => {
return (
<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">
<Input
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 type { Transaction } from 'types/api/transaction';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
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 Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -25,28 +30,50 @@ const TABS: Array<RoutedTab> = [
];
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 (
<Page>
{ /* TODO should be shown only when navigating from txs list */ }
<Link mb={ 6 } display="inline-flex" href={ link('txs') }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
{ hasGoBackLink && (
<Link mb={ 6 } display="inline-flex" href={ window.document.referrer }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
) }
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<PageTitle text="Transaction details"/>
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
mb={{ base: 6, lg: 'initial' }}
py={ 2.5 }
>
<ExternalLink title="Open in Tenderly" href="#"/>
<ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/>
</Flex>
{ data?.tx_tag && <Tag my={ 2 } ml={ 3 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && (
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
mb={{ base: 6, lg: 'initial' }}
py={ 2.5 }
>
{ explorersLinks }
</Flex>
) }
</Flex>
<RoutedTabs
tabs={ TABS }
......
......@@ -5,21 +5,21 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsPending from 'ui/txs/TxsPending';
import TxsValidated from 'ui/txs/TxsValidated';
const TABS: Array<RoutedTab> = [
{ id: 'validated', title: 'Validated', component: <TxsValidated/> },
{ id: 'pending', title: 'Pending', component: <TxsPending/> },
];
import TxsTab from 'ui/txs/TxsTab';
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 (
<Page>
<Page hideMobileHeaderOnScrollDown>
<Box h="100%">
<PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS }/>
......
......@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -19,7 +20,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => {
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 deleteModalProps = useDisclosure();
......
......@@ -9,6 +9,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
......@@ -53,10 +54,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const isEdit = data?.id;
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>) => {
setPending(false);
......@@ -70,7 +71,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}
},
onSuccess: () => {
queryClient.refetchQueries([ 'address-tags' ]).then(() => {
queryClient.refetchQueries([ QueryKeys.addressTags ]).then(() => {
onClose();
setPending(false);
});
......
......@@ -4,7 +4,6 @@ import {
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
......@@ -20,27 +19,25 @@ interface Props {
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item: AddressTag) => (
<AddressTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item: AddressTag) => (
<AddressTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
);
};
......
......@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal';
......@@ -22,16 +23,16 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const fetch = useFetch();
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 ]);
const onSuccess = useCallback(async() => {
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);
});
} else {
queryClient.setQueryData([ 'transaction-tags' ], (prevData: TransactionTags | undefined) => {
queryClient.setQueryData([ QueryKeys.transactionTags ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
});
}
......
......@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -18,7 +19,7 @@ import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => {
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 deleteModalProps = useDisclosure();
......
......@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -18,7 +19,10 @@ import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => {
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 deleteModalProps = useDisclosure();
......
......@@ -9,6 +9,7 @@ import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
......@@ -53,10 +54,10 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const isEdit = data?.id;
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>) => {
setPending(false);
......@@ -70,7 +71,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}
},
onSuccess: () => {
queryClient.refetchQueries([ 'transaction-tags' ]).then(() => {
queryClient.refetchQueries([ QueryKeys.transactionTags ]).then(() => {
onClose();
setPending(false);
});
......
......@@ -4,7 +4,6 @@ import {
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
......@@ -20,27 +19,25 @@ interface Props {
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="75%">Transaction</Th>
<Th width="25%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TransactionTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="75%">Transaction</Th>
<Th width="25%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TransactionTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
);
};
......
......@@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal';
......@@ -27,12 +28,12 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const deleteApiKey = useCallback(() => {
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 ]);
const onSuccess = useCallback(async() => {
onDeleteSuccess();
queryClient.setQueryData([ 'public-tags' ], (prevData: PublicTags | undefined) => {
queryClient.setQueryData([ QueryKeys.publicTags ], (prevData: PublicTags | undefined) => {
return prevData?.filter((item) => item.id !== data.id);
});
}, [ queryClient, data, onDeleteSuccess ]);
......
......@@ -4,7 +4,6 @@ import {
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
......@@ -20,28 +19,26 @@ interface Props {
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="50%">Smart contract / Address (0x...)</Th>
<Th width="25%">Public tag</Th>
<Th width="25%">Request status</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<PublicTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="50%">Smart contract / Address (0x...)</Th>
<Th width="25%">Public tag</Th>
<Th width="25%">Request status</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<PublicTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
);
};
......
......@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -26,7 +27,8 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const isMobile = useIsMobile();
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(() => {
setDeleteModalData(undefined);
......
......@@ -12,6 +12,7 @@ import type { FieldError, Path, SubmitHandler } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
......@@ -98,17 +99,17 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
};
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, {
onSuccess: async(data) => {
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);
if (isExisting) {
......
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
key?: string;
}
const AccountListItemMobile = ({ children, className }: Props) => {
const AccountListItemMobile = ({ children, className, key }: Props) => {
return (
<Flex
as={ motion.div }
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
key={ key }
rowGap={ 6 }
alignItems="flex-start"
flexDirection="column"
......
......@@ -2,22 +2,18 @@ import { Box, Text, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Unit } from 'types/unit';
import getValueWithUnit from 'lib/getValueWithUnit';
interface Props {
value: string;
unit?: Unit;
currency?: string;
exchangeRate?: string | null;
className?: string;
accuracy?: number;
accuracyUsd?: number;
decimals?: string | null;
}
const CurrencyValue = ({ value, currency = '', unit, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
const valueCurr = getValueWithUnit(value, unit);
const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18')));
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdContent;
......
......@@ -2,7 +2,12 @@ import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies';
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 Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
......@@ -10,25 +15,36 @@ import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
interface Props {
children: React.ReactNode;
wrapChildren?: boolean;
hideMobileHeaderOnScrollDown?: boolean;
}
const Page = ({ children, wrapChildren = true }: Props) => {
const Page = ({
children,
wrapChildren = true,
hideMobileHeaderOnScrollDown,
}: Props) => {
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 ? (
<PageContent>{ children }</PageContent>
) : children;
return (
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header/>
{ renderedChildren }
<ScrollDirectionContext.Provider value={ directionContext }>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
{ renderedChildren }
</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 arrowIcon from 'icons/arrows/east-mini.svg';
type Props = {
currentPage: number;
maxPage?: number;
export type Props = {
page: 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) => {
const pageNumber = (
<Flex alignItems="center">
return (
<Flex
fontSize="sm"
alignItems="center"
>
<Button
variant="outline"
colorScheme="gray"
size="sm"
isActive
borderWidth="1px"
fontWeight={ 400 }
mr={ 3 }
h={ 8 }
onClick={ resetPage }
disabled={ !hasPaginationParams }
mr={ 4 }
>
{ currentPage }
First
</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
variant="outline"
colorScheme="gray"
size="sm"
width={ 8 }
isActive
borderWidth="1px"
fontWeight={ 400 }
ml={ 3 }
h={ 8 }
cursor="unset"
disabled={ hasPaginationParams && page === 1 }
>
{ maxPage || MAX_PAGE_DEFAULT }
{ page }
</Button>
</Flex>
);
return (
<Flex
fontSize="sm"
width={{ base: '100%', lg: 'auto' }}
justifyContent={{ base: 'space-between', lg: 'unset' }}
alignItems="center"
>
<Flex alignItems="center" justifyContent="space-between" w={{ base: '100%', lg: 'auto' }}>
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 8 }
/>
{ pageNumber }
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 8 }
/>
</Flex>
<Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
<IconButton
variant="outline"
onClick={ onNextPageClick }
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 6 }
disabled={ !hasNextPage }
/>
{ /* not implemented yet */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
Go to <Input w="84px" size="xs" ml={ 2 }/>
</Flex>
</Flex> */ }
</Flex>
);
......
......@@ -49,8 +49,11 @@ const RoutedTabs = ({ tabs }: Props) => {
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
router.query.tab = nextTab.id;
router.push(router);
router.push(
{ pathname: router.asPath.split('?')[0], query: { tab: nextTab.id } },
undefined,
{ shallow: true },
);
}, [ tabs, router ]);
return (
......
......@@ -14,7 +14,7 @@ import { menuButton } from './utils';
interface Props {
tabs: Array<RoutedTab | MenuButton>;
activeTab: RoutedTab;
activeTab?: RoutedTab;
tabsCut: number;
isActive: boolean;
styles?: StyleProps;
......@@ -52,7 +52,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
key={ tab.id }
variant="ghost"
onClick={ handleItemClick }
isActive={ activeTab.id === tab.id }
isActive={ activeTab ? activeTab.id === tab.id : false }
justifyContent="left"
data-index={ index }
>
......
......@@ -4,12 +4,12 @@ import React from 'react';
import upDownArrow from 'icons/arrows/up-down.svg';
type Props = {
handleSort: () => void;
isSortActive: boolean;
onClick: () => void;
isActive: boolean;
className?: string;
}
const SortButton = ({ handleSort, isSortActive, className }: Props) => {
const SortButton = ({ onClick, isActive, className }: Props) => {
return (
<IconButton
icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> }
......@@ -18,8 +18,9 @@ const SortButton = ({ handleSort, isSortActive, className }: Props) => {
variant="outline"
colorScheme="gray-dark"
minWidth="36px"
onClick={ handleSort }
isActive={ isSortActive }
onClick={ onClick }
isActive={ isActive }
display="flex"
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;
interface Props {
hash: string;
name?: string;
name?: string | null;
className?: string;
}
const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = `
const logoSrc = appConfig.network.assetsPathname ? `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
${ appConfig.network.assetsPathname || appConfig.network.type }
${ appConfig.network.assetsPathname }
/assets/
${ hash }
/logo.png
`;
` : undefined;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>;
};
......
......@@ -5,9 +5,9 @@ import link from 'lib/link/link';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
symbol: string;
symbol?: string | null;
hash: string;
name: string;
name?: string | null;
className?: string;
}
......@@ -20,7 +20,7 @@ const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
<Link href={ url } target="_blank">
{ name }
</Link>
<Text variant="secondary">({ symbol })</Text>
{ symbol && <Text variant="secondary">({ symbol })</Text> }
</Center>
);
};
......
import { useColorModeValue, chakra, Image } from '@chakra-ui/react';
import { useColorModeValue, chakra, SkeletonCircle, Image } from '@chakra-ui/react';
import React from 'react';
import Identicon from 'react-identicons';
import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
const ProfileIcon = chakra(Identicon);
interface Props {
size: number;
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 bgColor = useColorModeValue('blackAlpha.100', 'white');
if (hasAuth && !isFetched) {
return <SkeletonCircle h={ sizeString } w={ sizeString }/>;
}
if (data?.avatar) {
return (
<Image
flexShrink={ 0 }
src={ data.avatar }
alt={ `Profile picture of ${ data.name || data.nickname || '' }` }
alt={ `Profile picture of ${ data?.name || data?.nickname || '' }` }
w={ sizeString }
minW={ sizeString }
h={ sizeString }
......
......@@ -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 type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react';
import link from 'lib/link/link';
......@@ -7,15 +8,16 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token' | 'block';
alias?: string;
alias?: string | null;
className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: 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;
if (type === 'transaction') {
url = link('tx', { id: id || hash });
......@@ -49,7 +51,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
<Link
className={ className }
href={ url }
target="_blank"
target={ target || '_blank' }
overflow="hidden"
whiteSpace="nowrap"
>
......
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
......@@ -9,46 +10,53 @@ import SearchBar from 'ui/snippets/searchBar/SearchBar';
import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
const Header = () => {
const Header = ({ hideOnScrollDown }: {hideOnScrollDown?: boolean}) => {
const bgColor = useColorModeValue('white', 'black');
return (
<>
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
<Flex
as="header"
position="fixed"
top={ 0 }
left={ 0 }
paddingX={ 4 }
paddingY={ 2 }
bgColor={ bgColor }
width="100%"
alignItems="center"
justifyContent="space-between"
zIndex="sticky"
>
<Burger/>
<NetworkLogo/>
<ProfileMenuMobile/>
</Flex>
<SearchBar/>
</Box>
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
</>
<ScrollDirectionContext.Consumer>
{ (scrollDirection) => (
<>
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
<Flex
as="header"
position="fixed"
top={ 0 }
left={ 0 }
paddingX={ 4 }
paddingY={ 2 }
bgColor={ bgColor }
width="100%"
alignItems="center"
justifyContent="space-between"
zIndex="sticky2"
transitionProperty="box-shadow"
transitionDuration="slow"
boxShadow={ !hideOnScrollDown && scrollDirection === 'down' ? 'md' : 'none' }
>
<Burger/>
<NetworkLogo/>
<ProfileMenuMobile/>
</Flex>
<SearchBar withShadow={ !hideOnScrollDown }/>
</Box><HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
</>
) }
</ScrollDirectionContext.Consumer>
);
};
......
......@@ -13,51 +13,61 @@ interface Props {
text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
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 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 (
<Box as="li" listStyleType="none" w="100%">
<NextLink href={ url } passHref>
<Link
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>
</NextLink>
{ /* 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 */ }
{ /* we have to hard reload page on every transition between urls from different part of the app */ }
{ isNewUi ? (
<NextLink href={ url } passHref>
{ content }
</NextLink>
) : content }
</Box>
);
};
......
......@@ -3,9 +3,9 @@ import React from 'react';
import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
......@@ -14,25 +14,24 @@ import NavFooter from './NavFooter';
import NavLink from './NavLink';
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 [ hasAccount, setHasAccount ] = React.useState(false);
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>();
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
const { mainNavItems, accountNavItems } = useNavItems();
React.useEffect(() => {
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
if (isInBrowser) {
if (navBarCollapsedCookie === 'true') {
setCollapsedState(true);
}
if (navBarCollapsedCookie === 'false') {
setCollapsedState(false);
}
setHasAccount(Boolean(appConfig.isAccountSupported && isAuth && isInBrowser));
}
}, [ isInBrowser ]);
const hasAccount = hasAuth && appConfig.isAccountSupported;
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(isNavBarCollapsed);
const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag);
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react';
import type { PreDefinedNetwork } from 'types/networks';
import appConfig from 'configs/app/config';
import blockscoutLogo from 'icons/logo.svg';
import link from 'lib/link/link';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
// predefined network logos
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'/eth/mainnet': require('icons/networks/logos/eth.svg'),
'/etc/mainnet': require('icons/networks/logos/etc.svg'),
'/poa/core': require('icons/networks/logos/poa.svg'),
'/rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'/xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'/poa/sokol': require('icons/networks/logos/sokol.svg'),
'/artis/sigma1': require('icons/networks/logos/artis.svg'),
'/lukso/l14': require('icons/networks/logos/lukso.svg'),
'/astar': require('icons/networks/logos/astar.svg'),
'/shiden': require('icons/networks/logos/shiden.svg'),
'/shibuya': require('icons/networks/logos/shibuya.svg'),
const LOGOS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = {
xdai_mainnet: require('icons/networks/logos/gnosis.svg'),
eth_mainnet: require('icons/networks/logos/eth.svg'),
etc_mainnet: require('icons/networks/logos/etc.svg'),
poa_core: require('icons/networks/logos/poa.svg'),
rsk_mainnet: require('icons/networks/logos/rsk.svg'),
xdai_testnet: require('icons/networks/logos/gnosis.svg'),
poa_sokol: require('icons/networks/logos/sokol.svg'),
artis_sigma1: require('icons/networks/logos/artis.svg'),
lukso_l14: require('icons/networks/logos/lukso.svg'),
astar: require('icons/networks/logos/astar.svg'),
shiden: require('icons/networks/logos/shiden.svg'),
shibuya: require('icons/networks/logos/shibuya.svg'),
};
interface Props {
......@@ -32,7 +33,7 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white');
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)' });
......@@ -41,46 +42,50 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
if (logo && typeof logo === 'string') {
logoEl = (
<Image
h="20px"
w="auto"
h="100%"
src={ logo }
alt={ `${ appConfig.network.name } network icon` }
/>
);
} else if (typeof logo !== undefined) {
} else if (typeof logo !== 'undefined') {
logoEl = (
<Icon
as={ logo as FunctionComponent<SVGAttributes<SVGElement>> }
width="auto"
height="20px"
height="100%"
{ ...getDefaultTransitionProps() }
style={ style }
/>
);
} else {
<Icon
as={ blockscoutLogo }
width="113px"
height="20px"
color={ logoColor }
{ ...getDefaultTransitionProps() }
style={ style }
/>;
logoEl = (
<Icon
as={ blockscoutLogo }
width="auto"
height="100%"
color={ logoColor }
{ ...getDefaultTransitionProps() }
style={ style }
/>
);
}
return (
<NextLink href={ href } passHref>
<Box
as="a"
width={{ base: '113px', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }}
display="inline-flex"
overflow="hidden"
onClick={ onClick }
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
aria-label="Link to main page"
>
{ logoEl }
</Box>
</NextLink>
// TODO switch to <NextLink href={ href } passHref> when main page for network will be ready
<Box
as="a"
href={ href }
width={{ base: 'auto', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }}
height="20px"
display="inline-flex"
overflow="hidden"
onClick={ onClick }
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
aria-label="Link to main page"
>
{ logoEl }
</Box>
);
};
......
import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { FeaturedNetwork } from 'types/networks';
......@@ -12,7 +11,6 @@ import useColors from './useColors';
interface Props extends FeaturedNetwork {
isActive: boolean;
isMobile?: boolean;
url: string;
}
const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
......@@ -31,38 +29,37 @@ const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
return (
<Box as="li" listStyleType="none">
<NextLink href={ url } passHref>
<Flex
as="a"
px={ isMobile ? 3 : 4 }
py={ 2 }
alignItems="center"
cursor="pointer"
pointerEvents={ isActive ? 'none' : 'initial' }
borderRadius="base"
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 }}
<Flex
as="a"
href={ url }
px={ isMobile ? 3 : 4 }
py={ 2 }
alignItems="center"
cursor="pointer"
pointerEvents={ isActive ? 'none' : 'initial' }
borderRadius="base"
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 }}
>
{ iconEl }
<Text
marginLeft={ 3 }
fontWeight="500"
color="inherit"
fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' }
>
{ iconEl }
<Text
marginLeft={ 3 }
fontWeight="500"
color="inherit"
fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' }
>
{ title }
</Text>
{ isActive && (
<Icon
as={ checkIcon }
boxSize="24px"
marginLeft="auto"
/>
) }
</Flex>
</NextLink>
{ title }
</Text>
{ isActive && (
<Icon
as={ checkIcon }
boxSize="24px"
marginLeft="auto"
/>
) }
</Flex>
</Box>
);
};
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { UserInfo } from 'types/api/account';
import appConfig from 'configs/app/config';
import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/snippets/navigation/NavLink';
......@@ -40,7 +41,7 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => {
</VStack>
</Box>
<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>
);
......
......@@ -2,17 +2,26 @@ import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@c
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import link from 'lib/link/link';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuDesktop = () => {
const { data } = useFetchProfileInfo();
const { data, isFetched } = useFetchProfileInfo();
const loginUrl = link('auth');
return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<PopoverTrigger>
<Button variant="unstyled" display="inline-flex" height="auto" flexShrink={ 0 }>
<UserAvatar size={ 50 } data={ data }/>
<Button
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>
</PopoverTrigger>
{ 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 useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import link from 'lib/link/link';
import UserAvatar from 'ui/shared/UserAvatar';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
......@@ -9,38 +10,39 @@ import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { data } = useFetchProfileInfo();
const { data, isFetched } = useFetchProfileInfo();
const loginUrl = link('auth');
return (
<>
<Box padding={ 2 } onClick={ onOpen }>
<UserAvatar size={ 24 } data={ data }/>
<UserAvatar size={ 24 } data={ data } isFetched={ isFetched }/>
</Box>
{ data && (
<Drawer
isOpen={ isOpen }
placement="right"
onClose={ onClose }
autoFocus={ false }
>
<DrawerOverlay/>
<DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }>
<Flex
justifyContent="space-between"
alignItems="center"
mb={ 6 }
>
<ColorModeToggler/>
<Box onClick={ onClose }>
<UserAvatar size={ 24 } data={ data }/>
</Box>
</Flex>
<ProfileMenuContent { ...data }/>
</DrawerBody>
</DrawerContent>
</Drawer>
) }
<Drawer
isOpen={ isOpen }
placement="right"
onClose={ onClose }
autoFocus={ false }
>
<DrawerOverlay/>
<DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }>
<Flex
justifyContent="space-between"
alignItems="center"
mb={ 6 }
>
<ColorModeToggler/>
<Box onClick={ onClose }>
<UserAvatar size={ 24 } data={ data } isFetched={ isFetched }/>
</Box>
</Flex>
{ data ? <ProfileMenuContent { ...data }/> : (
<Button size="sm" width="full" variant="outline" as="a" href={ loginUrl }>Sign In</Button>
) }
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
};
......
......@@ -6,7 +6,7 @@ import link from 'lib/link/link';
import SearchBarDesktop from './SearchBarDesktop';
import SearchBarMobile from './SearchBarMobile';
const SearchBar = () => {
const SearchBar = ({ withShadow }: {withShadow?: boolean}) => {
const [ value, setValue ] = React.useState('');
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
......@@ -22,7 +22,7 @@ const SearchBar = () => {
return (
<>
<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 clamp from 'lodash/clamp';
import throttle from 'lodash/throttle';
import React from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg';
import isBrowser from 'lib/isBrowser';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
const SCROLL_DIFF_THRESHOLD = 20;
const TOP = 55;
interface Props {
onChange: (event: ChangeEvent<HTMLInputElement>) => 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 [ 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 [ isSticky, setIsSticky ] = React.useState(false);
const handleScroll = React.useCallback(() => {
const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight);
const scrollDiff = currentScrollPosition - prevScrollPosition.current;
if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) {
setVisibility(scrollDiff > 0 ? false : true);
if (window.pageYOffset !== 0) {
setIsSticky(true);
} else {
setIsSticky(false);
}
prevScrollPosition.current = currentScrollPosition;
}, []);
React.useEffect(() => {
......@@ -46,37 +38,46 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => {
// 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 (
<chakra.form
noValidate
onSubmit={ onSubmit }
paddingX={ 4 }
paddingTop={ 1 }
paddingBottom={ 2 }
position="fixed"
top="56px"
left="0"
zIndex="docked"
bgColor={ bgColor }
transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' }
transitionProperty="transform"
transitionDuration="slow"
display={{ base: 'block', lg: 'none' }}
w="100%"
>
<InputGroup size="sm">
<InputLeftElement >
<Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
placeholder="Search by addresses / ... "
ml="1px"
onChange={ onChange }
borderColor={ inputBorderColor }
/>
</InputGroup>
</chakra.form>
<ScrollDirectionContext.Consumer>
{ (scrollDirection) => (
<chakra.form
noValidate
onSubmit={ onSubmit }
paddingX={ 4 }
paddingTop={ 1 }
paddingBottom={ 2 }
position="fixed"
top={ `${ TOP }px` }
left="0"
zIndex="sticky1"
bgColor={ bgColor }
transform={ scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)' }
transitionProperty="transform,box-shadow"
transitionDuration="slow"
display={{ base: 'block', lg: 'none' }}
w="100%"
boxShadow={ withShadow && scrollDirection !== 'down' && isSticky ? 'md' : 'none' }
>
<InputGroup size="sm">
<InputLeftElement >
<Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
placeholder="Search by addresses / ... "
ml="1px"
onChange={ onChange }
borderColor={ inputBorderColor }
/>
</InputGroup>
</chakra.form>
) }
</ScrollDirectionContext.Consumer>
);
};
......
......@@ -3,27 +3,33 @@ import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props {
value: string;
tokenId: string;
hash: string;
symbol: string;
name?: string | null;
symbol?: string | null;
}
const NftTokenTransferSnippet = (props: Props) => {
const num = props.value === '1' ? '' : props.value;
const url = link('token_instance_item', { hash: props.hash, id: props.tokenId });
const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) => {
const num = value === '1' ? '' : value;
const url = link('token_instance_item', { hash: hash, id: tokenId });
return (
<Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
<Text fontWeight={ 500 } as="span">For { num } token ID:</Text>
<Box display="inline-flex" alignItems="center">
<Icon as={ nftIcon } boxSize={ 6 } mr={ 1 }/>
<Link href={ url } fontWeight={ 600 }>{ props.tokenId }</Link>
<Link href={ url } fontWeight={ 600 }>{ tokenId }</Link>
</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>
);
};
......
import { Flex, Icon, Text } from '@chakra-ui/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 { space } from 'lib/html-entities';
......@@ -12,43 +12,47 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
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 tokenSnippet = <TokenSnippet symbol={ props.token_symbol } hash={ props.token_address } name="Foo" ml={ 3 }/>;
const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total);
const tokenSnippet = <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } ml={ 3 }/>;
const content = (() => {
switch (props.token_type) {
case 'ERC-20':
switch (token.type) {
case 'ERC-20': {
const payload = total as Erc20TotalPayload;
return (
<Flex>
<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>
{ tokenSnippet }
</Flex>
);
}
case 'ERC-721': {
const payload = total as Erc721TotalPayload;
return (
<NftTokenTransferSnippet
tokenId={ props.total.token_id }
tokenId={ payload.token_id }
value="1"
hash={ props.token_address }
symbol={ props.token_symbol }
hash={ token.address }
symbol={ token.symbol }
/>
);
}
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) => (
<NftTokenTransferSnippet
key={ item.token_id }
tokenId={ item.token_id }
value={ item.value }
hash={ props.token_address }
symbol={ props.token_symbol }
hash={ token.address }
symbol={ token.symbol }
/>
));
}
......@@ -64,9 +68,9 @@ const TokenTransfer = (props: Props) => {
flexDir={ isColumnLayout ? 'column' : 'row' }
>
<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"/>
<AddressLink fontWeight="500" hash={ props.to.hash } truncation="constant"/>
<AddressLink fontWeight="500" hash={ to.hash } truncation="constant"/>
</Flex>
<Flex flexDir="column" rowGap={ 5 }>
{ content }
......
......@@ -10,9 +10,9 @@ interface Props {
}
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
.filter((item) => item.token_type === 'ERC-1155')
.filter((item) => item.token.type === 'ERC-1155')
.map((item) => {
if (Array.isArray(item.total)) {
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 BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
......@@ -6,13 +6,14 @@ import React from 'react';
import { scroller, Element } from 'react-scroll';
import type { Transaction } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.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 errorIcon from 'icons/status/error.svg';
// import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
......@@ -47,8 +48,8 @@ const TxDetails = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Transaction>(
[ 'tx', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }`),
[ QueryKeys.tx, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
......@@ -72,6 +73,18 @@ const TxDetails = () => {
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 (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
<DetailsInfoItem
......@@ -131,36 +144,51 @@ const TxDetails = () => {
<DetailsInfoItem
title="From"
hint="Address (external or contract) sending the transaction."
columnGap={ 3 }
>
<Address>
<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 }/>
</Address>
{ data.from.name && <Text>{ data.from.name }</Text> }
{ addressFromTags.length > 0 && (
<Flex columnGap={ 3 }>
{ addressFromTags }
</Flex>
) }
</DetailsInfoItem>
<DetailsInfoItem
title={ data.to.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction."
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 }
>
<Address mr={ 3 }>
<Address>
<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 }/>
</Address>
{ /* todo_tom Nikita should add to api later */ }
{ /* <Tag colorScheme="orange" variant="solid" flexShrink={ 0 }>SANA</Tag> */ }
{ /* <Tooltip label="Contract execution completed">
<chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500" cursor="pointer"/>
</chakra.span>
</Tooltip> */ }
{ /* <Tooltip label="Error occured during contract execution">
<chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } ml={ 2 } color="red.500" cursor="pointer"/>
</chakra.span>
</Tooltip> */ }
{ /* <TokenSnippet symbol="UP" name="User Pay" hash="0xA17ed5dFc62D0a3E74D69a0503AE9FdA65d9f212" ml={ 3 }/> */ }
{ data.to.name && <Text>{ data.to.name }</Text> }
{ data.to.is_contract && data.result === 'success' && (
<Tooltip label="Contract execution completed">
<chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
) }
{ data.to.is_contract && Boolean(data.status) && data.result !== 'success' && (
<Tooltip label="Error occured during contract execution">
<chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
) }
{ addressToTags.length > 0 && (
<Flex columnGap={ 3 }>
{ addressToTags }
</Flex>
) }
</DetailsInfoItem>
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => {
const items = data.token_transfers?.filter((token) => token.type === type) || [];
......@@ -184,7 +212,7 @@ const TxDetails = () => {
title="Value"
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
title="Transaction fee"
......@@ -192,7 +220,7 @@ const TxDetails = () => {
>
<CurrencyValue
value={ data.fee.value }
currency={ appConfig.network.currency }
currency={ appConfig.network.currency.symbol }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
......@@ -201,7 +229,7 @@ const TxDetails = () => {
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
>
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { 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>
</DetailsInfoItem>
<DetailsInfoItem
......@@ -244,12 +272,12 @@ const TxDetails = () => {
{ data.tx_burnt_fee && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
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"/>
<CurrencyValue
value={ String(data.tx_burnt_fee) }
currency={ appConfig.network.currency }
currency={ appConfig.network.currency.symbol }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
/>
......
......@@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -42,16 +43,15 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
return a.value === b.value ? 0 : result;
}
// no gas limit in api yet
// case 'gas-limit-desc': {
// const result = a.gasLimit > b.gasLimit ? -1 : 1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
case 'gas-limit-desc': {
const result = a.gas_limit > b.gas_limit ? -1 : 1;
return a.gas_limit === b.gas_limit ? 0 : result;
}
// case 'gas-limit-asc': {
// const result = a.gasLimit > b.gasLimit ? 1 : -1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
case 'gas-limit-asc': {
const result = a.gas_limit > b.gas_limit ? 1 : -1;
return a.gas_limit === b.gas_limit ? 0 : result;
}
default:
return 0;
......@@ -73,8 +73,8 @@ const TxInternals = () => {
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ 'tx-internals', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/internal-transactions`),
[ QueryKeys.txInternals, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`),
{
enabled: Boolean(router.query.id),
},
......
......@@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { LogsResponse } from 'types/api/log';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -15,8 +16,8 @@ const TxLogs = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ 'tx-log', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/logs`),
[ QueryKeys.txLog, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`),
{
enabled: Boolean(router.query.id),
},
......
......@@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -14,8 +15,8 @@ const TxRawTrace = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
[ 'tx-raw-trace', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/raw-trace`),
[ QueryKeys.txRawTrace, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/raw-trace`),
{
enabled: Boolean(router.query.id),
},
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
......@@ -14,7 +15,7 @@ import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
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;
return (
......@@ -35,14 +36,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) =
</Address>
</Box>
<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>
</HStack>
{ /* no gas limit in api yet */ }
{ /* <HStack spacing={ 3 }>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text>
</HStack> */ }
<Text fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Text>
</HStack>
</AccountListItemMobile>
);
};
......
......@@ -10,7 +10,7 @@ const TxInternalsSkeletonDesktop = () => {
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '28%', '24px', '28%', '16%' ] }/>
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
</>
);
};
......
......@@ -38,6 +38,10 @@ const TxInternalsSkeletonMobile = () => {
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
</Flex>
)) }
</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 type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/east.svg';
import { default as Thead } from 'ui/shared/TheadSticky';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils';
......@@ -18,36 +19,33 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th width="28%">Type</Th>
<Th width="28%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="28%">To</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { appConfig.network.currency }
</Link>
</Th>
{ /* no gas limit in api yet */ }
{ /* <Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit
</Link>
</Th> */ }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem key={ item.transaction_hash } { ...item }/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" size="sm" mt={ 6 }>
<Thead top={ 0 }>
<Tr>
<Th width="28%">Type</Th>
<Th width="20%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { appConfig.network.currency.symbol }
</Link>
</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit { appConfig.network.currency.symbol }
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem key={ item.transaction_hash } { ...item }/>
)) }
</Tbody>
</Table>
);
};
......
import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
......@@ -12,7 +13,7 @@ import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
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;
return (
......@@ -43,10 +44,9 @@ const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) =
<Td isNumeric verticalAlign="middle">
{ value }
</Td>
{ /* no gas limit in api yet */ }
{ /* <Td isNumeric verticalAlign='middle'>
{ gasLimit.toLocaleString('en') }
</Td> */ }
<Td isNumeric verticalAlign="middle">
{ BigNumber(gasLimit).toFormat() }
</Td>
</Tr>
);
};
......
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 type ArrayElement from 'types/utils/ArrayElement';
......@@ -6,6 +7,7 @@ import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
import type { data } from 'data/txState';
import { nbsp } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -60,11 +62,11 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
) }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box>
<Text as="span">Miner </Text>
<Text as="span">{ capitalize(getNetworkValidatorTitle()) }</Text>
<Link>{ miner }</Link>
</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>
</Box>
{ typeof before.nonce !== 'undefined' && (
......@@ -74,7 +76,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
</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>
</Box>
{ typeof after.nonce !== 'undefined' && (
......@@ -83,7 +85,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box>
) }
<Text>State difference { appConfig.network.currency }</Text>
<Text>State difference { appConfig.network.currency.symbol }</Text>
<Stat>
{ diff }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react';
import appConfig from 'configs/app/config';
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';
const TxStateTable = () => {
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm" w="auto">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="950px" size="sm" w="auto" mt={ 6 }>
<Thead top={ 0 }>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency.symbol }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
);
};
......
......@@ -34,7 +34,7 @@ const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => {
<Flex>
<CurrencyValue
value={ tx.fee.value }
currency={ appConfig.network.currency }
currency={ appConfig.network.currency.symbol }
exchangeRate={ tx.exchange_rate }
accuracyUsd={ 2 }
/>
......
import { Tag } from '@chakra-ui/react';
import React from 'react';
import type { TransactionType } from 'types/api/transaction';
export interface Props {
type: 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
type: TransactionType;
}
const TxStatus = ({ type }: Props) => {
......@@ -10,24 +12,24 @@ const TxStatus = ({ type }: Props) => {
let colorScheme;
switch (type) {
case 'contract-call':
case 'contract_call':
label = 'Contract call';
colorScheme = 'blue';
break;
case 'transaction':
label = 'Transaction';
case 'contract_creation':
label = 'Contract creation';
colorScheme = 'purple';
break;
case 'token-transfer':
case 'token_transfer':
label = 'Token transfer';
colorScheme = 'orange';
break;
case 'internal-tx':
label = 'Internal txn';
case 'token_creation':
label = 'Token creation';
colorScheme = 'cyan';
break;
case 'multicall':
label = 'Multicall';
case 'coin_transfer':
label = 'Coin transfer';
colorScheme = 'teal';
break;
}
......
import { Box, HStack, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Box, Show } from '@chakra-ui/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 compareBns from 'lib/bigint/compareBns';
import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton';
import * as cookies from 'lib/cookies';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
import TxsHeader from './TxsHeader';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort';
import useQueryWithPages from './useQueryWithPages';
type Props = {
txs: TransactionsResponse['items'];
queryName: QueryKeys;
showDescription?: boolean;
showSortButton?: boolean;
stateFilter?: TTxsFilters['filter'];
apiPath: string;
}
const TxsContent = ({ showSortButton = true, showDescription = true, txs }: Props) => {
const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
const TxsContent = ({
queryName,
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') => () => {
if (field === 'val') {
setSorting((prevVal => {
setSorting((prevVal) => {
let newVal: Sort = '';
if (field === 'val') {
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';
}
return 'val-desc';
}));
}
if (field === 'fee') {
setSorting((prevVal => {
}
if (field === 'fee') {
if (prevVal === 'fee-asc') {
return undefined;
}
if (prevVal === 'fee-desc') {
return 'fee-asc';
newVal = '';
} else if (prevVal === 'fee-desc') {
newVal = '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(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value)));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value)));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value)));
break;
default:
setSortedTxs(txs);
}
}, [ sorting, txs ]);
const paginationProps = {
...pagination,
hasNextPage: data?.next_page_params !== undefined && data?.next_page_params !== null && Object.keys(data?.next_page_params).length > 0,
};
return (
<>
{ showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
<HStack mb={ 6 }>
{ /* TODO */ }
<FilterButton
isActive={ false }
// eslint-disable-next-line react/jsx-no-bind
onClick={ () => {} }
appliedFiltersNum={ 0 }
/>
{ showSortButton && (
<SortButton
// eslint-disable-next-line react/jsx-no-bind
handleSort={ () => {} }
isSortActive={ Boolean(sorting) }
display={{ base: 'block', lg: 'none' }}
/>
) }
<FilterInput
// eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} }
maxW="360px"
size="xs"
placeholder="Search by addresses, hash, method..."
/>
</HStack>
<Show below="lg"><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Show above="lg"><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/>
</Box>
<TxsHeader sorting={ sorting } setSorting={ setSorting } paginationProps={ paginationProps }/>
{ content }
</>
);
};
......
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}) => {
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
{ tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
......@@ -60,6 +58,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
type="transaction"
fontWeight="700"
truncation="constant"
target="_self"
/>
</Address>
</Flex>
......@@ -67,7 +66,6 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
{ /* TODO: we don't recieve method from api */ }
<Text
as="span"
variant="secondary"
......@@ -75,8 +73,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ /* { tx.method } */ }
CommitHash
{ tx.method }
</Text>
</Flex>
{ tx.block !== null && (
......@@ -112,11 +109,11 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
</Address>
</Flex>
<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>
</Box>
<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>
</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 SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = ({ isPending }: {isPending?: boolean}) => {
const TxsInternalsSkeletonDesktop = () => {
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<Box mb={ 8 }>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
</>
</Box>
);
};
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = ({ isPending }: {isPending?: boolean}) => {
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
flexDirection="column"
paddingBottom={ 3 }
paddingTop={ 4 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
flexDirection="column"
paddingBottom={ 3 }
paddingTop={ 4 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
)) }
</Box>
</>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
)) }
</Box>
);
};
......
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 type { Transaction } from 'types/api/transaction';
......@@ -6,55 +6,54 @@ import type { Sort } from 'types/client/txs-sort';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import TheadSticky from 'ui/shared/TheadSticky';
import TxsTableItem from './TxsTableItem';
type Props = {
txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void;
sorting: Sort;
sorting?: Sort;
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="810px" size="xs">
<Thead>
<Tr>
<Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th>
<Th width="11%">Block</Th>
<Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ appConfig.network.currency }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ appConfig.network.currency }` }
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="810px" size="xs">
<TheadSticky top={ 80 }>
<Tr>
<Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th>
<Th width="11%">Block</Th>
<Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ appConfig.network.currency.symbol }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ appConfig.network.currency.symbol }` }
</Link>
</Th>
</Tr>
</TheadSticky>
<Tbody>
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
/>
)) }
</Tbody>
</Table>
);
};
......
......@@ -12,7 +12,6 @@ import {
PopoverTrigger,
PopoverContent,
PopoverBody,
Portal,
useColorModeValue,
Show,
} from '@chakra-ui/react';
......@@ -40,7 +39,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
<Tooltip label={ tx.from.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.from.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address>
);
......@@ -49,7 +48,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
<Tooltip label={ tx.to.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address>
);
......@@ -57,28 +56,24 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
return (
<Tr>
<Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 }>
<Popover placement="right-start" openDelay={ 300 } isLazy>
{ ({ isOpen }) => (
<>
<PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger>
<Portal>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody>
<TxAdditionalInfo tx={ tx }/>
</PopoverBody>
</PopoverContent>
</Portal>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody>
<TxAdditionalInfo tx={ tx }/>
</PopoverBody>
</PopoverContent>
</>
) }
</Popover>
</Td>
<Td>
<VStack alignItems="start">
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
{ tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
......@@ -89,33 +84,25 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
hash={ tx.hash }
type="transaction"
fontWeight="700"
target="_self"
/>
</Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack>
</Td>
<Td>
{ /* TODO: we don't recieve method from api */ }
{ /* <TruncatedTextTooltip label={ tx.method }>
<TruncatedTextTooltip label={ tx.method }>
<Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
>
{ tx.method }
</Tag>
</TruncatedTextTooltip> */ }
<TruncatedTextTooltip label="CommitHash">
<Tag
colorScheme="gray"
>
CommitHash
</Tag>
</TruncatedTextTooltip>
</Td>
<Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td>
{ /* TODO: fix "show" problem */ }
<Show above="xl">
<Show above="xl" ssr={ false }>
<Td>
{ addressFrom }
</Td>
......@@ -126,7 +113,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
{ addressTo }
</Td>
</Show>
<Show below="xl">
<Show below="xl" ssr={ false }>
<Td colSpan={ 3 }>
<Box>
{ 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';
import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
......@@ -96,17 +97,17 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
};
if (data) {
// 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 {
// 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, {
onSuccess: () => {
queryClient.refetchQueries([ 'watchlist' ]).then(() => {
queryClient.refetchQueries([ QueryKeys.watchlist ]).then(() => {
onClose();
setPending(false);
});
......
......@@ -8,7 +8,7 @@ import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ appConfig.network.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
const NOTIFICATIONS_NAMES = [ appConfig.network.currency.symbol, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
type Props<Inputs extends FieldValues> = {
control: Control<Inputs>;
......
......@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -20,11 +21,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const fetch = useFetch();
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 ]);
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);
});
}, [ data, queryClient ]);
......
......@@ -25,7 +25,7 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
{ appConfig.network.nativeTokenAddress &&
<TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ appConfig.network.currency } balance:${ nbsp }` + nativeBalance }</Text>
<Text color={ mainTextColor }>{ `${ appConfig.network.currency.symbol } balance:${ nbsp }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack>
{ item.tokens_count && (
......
......@@ -27,7 +27,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const { mutate } = useMutation(() => {
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: () => {
// eslint-disable-next-line no-console
......
......@@ -52,7 +52,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
setSwitchDisabled(true);
const body = { ...item, notification_methods: { email: !notificationEnabled } };
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: () => {
showToast();
......
......@@ -4,7 +4,6 @@ import {
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
......@@ -20,28 +19,26 @@ interface Props {
const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="70%">Address</Th>
<Th width="30%">Private tag</Th>
<Th width="160px">Email notification</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<WatchlistTableItem
item={ item }
key={ item.address_hash }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="70%">Address</Th>
<Th width="30%">Private tag</Th>
<Th width="160px">Email notification</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<WatchlistTableItem
item={ item }
key={ item.address_hash }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
);
};
......
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