Commit 88dfeb9e authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #205 from blockscout/new-envs

New envs
parents 73bb6ce1 7d5c9bd7
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true,"chainId":100,"currency":"xDAI"},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60","chainId":300,"currency":"xDAI"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets","chainId":200,"currency":"xDAI"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets","chainId":1,"currency":"ETH"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets","chainId":61,"currency":"ETC"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","chainId":99,"currency":"POA","nativeTokenAddress": "0x029a799563238d0e75e20be2f4bda0ea68d00172"},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets","chainId":30,"currency":"RBTC"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true,"currency":"xDAI"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets","chainId":77,"currency":"SPOA"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other","chainId":246529,"currency":"ATS"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other","chainId":22,"currency":"LYX"},{"name":"Astar","type":"astar","group":"other","chainId":22,"currency":"ASTR"}]
NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx
......@@ -6,3 +6,17 @@ 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_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
......@@ -4,6 +4,7 @@ const RESTRICTED_MODULES = {
{ name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
],
};
module.exports = {
env: {
es6: true,
......@@ -202,6 +203,12 @@ module.exports = {
],
'no-restricted-imports': [ 'error', RESTRICTED_MODULES ],
'no-restricted-properties': [ 2, {
object: 'process',
property: 'env',
// FIXME: restrict the rule only NEXT_PUBLIC variables
message: 'Please use configs/app/config.ts to import any NEXT_PUBLIC environment variables. For other properties please disable this rule for a while.',
} ],
'react/jsx-key': 'error',
'react/jsx-no-bind': [ 'error', {
......@@ -275,5 +282,12 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: [ 'configs/**/*.js', 'configs/**/*.ts' ],
rules: {
// for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ],
},
},
],
};
......@@ -28,6 +28,7 @@ yarn-error.log*
# local env files
.env*.local
/configs/envs/.env.secrets
# vercel
.vercel
......
......@@ -20,40 +20,52 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or
For local development please follow next steps:
- clone repo
- install dependencies with `yarn`
- clone `env.example` into local env file `env.local` (see explanation of all used environment variables [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output
- clone `env.example` into `configs/envs/env.secrets` and fill it with necessary secret values (see description [below](#environment-variables))
- to spin up local dev server
- for predefined networks configs (see full available list in `package.json`) you can just run `yarn dev:<app_name>`
- for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev`
- navigate to the host from logs output
## Environment variables
### Variables list
The app instance could be customized by passing following variables to NodeJS environment.
The app instance could be customized by passing following variables to NodeJS environment at runtime.
**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` |
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
| NEXT_PUBLIC_NETWORK_TYPE | `string` | Network type (used as first part of the base path) | `xdai` |
| NEXT_PUBLIC_NETWORK_SUBTYPE | `string` | Network subtype (used as second part of the base path) | `mainnet` |
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
| NEXT_PUBLIC_NETWORK_CURRENCY | `string` | Network currency symbol | `xDAI` |
| NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS | `string` | Address of network's native token | `0x029a799563238d0e75e20be2f4bda0ea68d00172` |
| NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). If not provided, the network type will be used as its assets path part | `ethereum` |
| NEXT_PUBLIC_NETWORK_LOGO | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `https://www.fillmurray.com/240/40` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#network-configuration-properties) | Configuration of featured networks that will be shown in the app menu | `[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]` |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` *(optional)* | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` *(optional)* | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_SUPPORTED_NETWORKS | `Array<Network>` where `Network` can have following [properties](#network-configuration-properties) | Configuration of supported networks | `[{"name":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true,"chainId":99,"currency":"POA"}]` |
| NEXT_PUBLIC_APP_INSTANCE | `string` *(optional)* | Name of app instance | `wonderful_kepler` |
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` |
| NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` |
| NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` |
| NEXT_PUBLIC_API_ENDPOINT | `string` *(optional)* | By default the API endpoint base URL will be set up the same value as frontend app host. If it is not the case, pass the API endpoint base URL in this variable | `https://blockscout.com` |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` |
### Network configuration properties
### Featured network configuration properties
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `"Gnosis Chain"` |
| shortName | `string` | Used for SEO attributes (page title and description) | `"OoG"` |
| chainId | `number` | Id of the network. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `1` |
| currency | `string` | Network currency symbol. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `"xDAI"` |
| nativeTokenAddress | `string` | Address of network's native token | `"0x029a799563238d0e75e20be2f4bda0ea68d00172"` |
| type | `string` | Network type (used as first part of the base path) | `"xdai"` |
| subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` |
| title | `string` | Displayed name of the network | `'Gnosis Chain'` |
| basePath | `string` | Network explorer main page url | `'/xdai/mainnet'` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| isAccountSupported | `boolean` *(optional)* | Set to true if network has account feature | `true` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `"https://www.fillmurray.com/60/60"` |
| logo | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `"https://www.fillmurray.com/240/40"` |
| assetsNamePath | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). The project already has some pre-defined mapping for popular network, which is match assetsNamePath against provided network type and sub-type. So typically you don't need to provide this variable, if you network is in the list or its type in config is conformed to a name in TrustWallet repo. If it is not the case, pass value here | `"ethereum"` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `'https://www.fillmurray.com/60/60'` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
......
/* eslint-disable no-restricted-properties */
const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development';
const baseUrl = [
process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https',
'://',
process.env.NEXT_PUBLIC_APP_HOST,
process.env.NEXT_PUBLIC_APP_PORT ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '',
].join('');
const config = Object.freeze({
env,
isDev,
network: {
type: process.env.NEXT_PUBLIC_NETWORK_TYPE,
subtype: process.env.NEXT_PUBLIC_NETWORK_SUBTYPE,
logo: process.env.NEXT_PUBLIC_NETWORK_LOGO,
name: process.env.NEXT_PUBLIC_NETWORK_NAME,
id: process.env.NEXT_PUBLIC_NETWORK_ID,
shortName: process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME,
currency: process.env.NEXT_PUBLIC_NETWORK_CURRENCY,
assetsPathname: process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME,
nativeTokenAddress: process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS,
basePath: '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/'),
},
footerLinks: {
github: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK,
twitter: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK,
telegram: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK,
staking: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK,
},
featuredNetworks: process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"'),
blockScoutVersion: process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION,
isAccountSupported: process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?.replaceAll('\'', '"') === 'true',
protocol: process.env.NEXT_PUBLIC_APP_PROTOCOL,
host: process.env.NEXT_PUBLIC_APP_HOST,
port: process.env.NEXT_PUBLIC_APP_PORT,
baseUrl,
apiEndpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || baseUrl,
});
export default config;
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME=POA
NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa
NEXT_PUBLIC_NETWORK_TYPE=poa
NEXT_PUBLIC_NETWORK_SUBTYPE=core
NEXT_PUBLIC_NETWORK_ID=99
NEXT_PUBLIC_NETWORK_CURRENCY=POA
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
async function redirects() {
const homePagePath = '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/');
return [
{
source: '/',
destination: '/poa/core',
destination: homePagePath,
permanent: false,
},
];
......
import appConfig from 'configs/app/config';
import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch';
......@@ -13,9 +14,9 @@ export default function fetchFactory(_req: NextApiRequest) {
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
};
const url = `https://blockscout.com${ path }`;
const url = new URL(path, appConfig.apiEndpoint);
return nodeFetch(url, {
return nodeFetch(url.toString(), {
headers,
...init,
});
......
import availableNetworks from 'lib/networks/availableNetworks';
import appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks';
const KEY_WORDS = {
BLOB: 'blob:',
......@@ -11,20 +13,18 @@ const KEY_WORDS = {
UNSAFE_EVAL: '\'unsafe-eval\'',
};
const MAIN_DOMAINS = [ '*.blockscout.com', 'blockscout.com' ];
const isDev = process.env.NODE_ENV === 'development';
const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
// eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssets() {
const icons = availableNetworks
const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string')
.map(({ icon }) => new URL(icon as string));
const logos = availableNetworks
.filter(({ logo }) => typeof logo === 'string')
.map(({ logo }) => new URL(logo as string));
const logo = appConfig.network.logo ? new URL(appConfig.network.logo) : undefined;
return icons.concat(logos);
return logo ? icons.concat(logo) : icons;
}
function makePolicyMap() {
......@@ -39,7 +39,7 @@ function makePolicyMap() {
KEY_WORDS.SELF,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason
isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// client error monitoring
'sentry.io', '*.sentry.io',
......@@ -50,7 +50,7 @@ function makePolicyMap() {
// next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
isDev ? KEY_WORDS.UNSAFE_EVAL : '',
appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS,
......@@ -102,9 +102,9 @@ function makePolicyMap() {
KEY_WORDS.NONE,
],
...(process.env.SENTRY_CSP_REPORT_URI ? {
...(REPORT_URI ? {
'report-uri': [
process.env.SENTRY_CSP_REPORT_URI,
REPORT_URI,
],
} : {}),
......
import { useRouter } from 'next/router';
import findNetwork from 'lib/networks/findNetwork';
export default function useNetwork() {
const router = useRouter();
const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_sub_type === 'string' ? router.query.network_sub_type : undefined,
});
return selectedNetwork;
}
import isBrowser from 'lib/isBrowser';
import findNetwork from 'lib/networks/findNetwork';
import appConfig from 'configs/app/config';
import { ROUTES } from './routes';
import type { RouteName } from './routes';
......@@ -12,27 +11,26 @@ export function link(routeName: RouteName, urlParams?: Record<string, Array<stri
return '';
}
const network = findNetwork({
network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_type : '',
network_sub_type: typeof urlParams?.network_sub_type === 'string' ? urlParams?.network_sub_type : undefined,
});
// if we pass network type, we have to get subtype from params too
// otherwise getting it from config since it is not cross-chain link
const networkSubType = typeof urlParams?.network_type === 'string' ? urlParams?.network_sub_type : appConfig.network.subtype;
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !network?.subType) {
if (paramName === 'network_sub_type' && !networkSubType) {
return '';
}
let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dun't know how to manage it, fix me if you find an issue
// dunno know how to manage it, fix me if you find an issue
paramValue = paramValue.join(',');
}
return paramValue ? `/${ paramValue }` : '';
});
const url = new URL(path, isBrowser() ? window.location.origin : 'https://blockscout.com');
const url = new URL(path, appConfig.baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
......
......@@ -3,6 +3,8 @@ export interface Route {
crossNetworkNavigation?: boolean; // route will not change when switching networks
}
import appConfig from 'configs/app/config';
export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/[network_type]/[network_sub_type]';
......@@ -17,31 +19,24 @@ export const ROUTES = {
// ACCOUNT
watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
},
private_tags_address: {
pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
},
private_tags_tx: {
pattern: `${ BASE_PATH }/account/tag_transaction`,
crossNetworkNavigation: true,
},
public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`,
crossNetworkNavigation: true,
},
api_keys: {
pattern: `${ BASE_PATH }/account/api_key`,
crossNetworkNavigation: true,
},
custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`,
crossNetworkNavigation: true,
},
profile: {
pattern: `${ BASE_PATH }/auth/profile`,
crossNetworkNavigation: true,
},
// TRANSACTIONS
......@@ -139,6 +134,6 @@ function checkRoutes(route: Record<string, Route>) {
return route;
}
if (process.env.NODE_ENV === 'development') {
if (appConfig.isDev) {
checkRoutes(ROUTES);
}
import type { Network } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
import parseNetworkConfig from './parseNetworkConfig';
// will change later when we agree how to host network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': gnosisIcon,
'xdai/optimism': optimismIcon,
'xdai/aox': arbitrumIcon,
'eth/mainnet': ethereumIcon,
'etc/mainnet': ethereumClassicIcon,
'poa/core': poaIcon,
'rsk/mainnet': rskIcon,
'xdai/testnet': arbitrumIcon,
'poa/sokol': poaSokolIcon,
'artis/sigma1': artisIcon,
};
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'eth/mainnet': require('icons/networks/logos/eth.svg'),
'etc/mainnet': require('icons/networks/logos/etc.svg'),
'poa/core': require('icons/networks/logos/poa.svg'),
'rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'poa/sokol': require('icons/networks/logos/sokol.svg'),
'artis/sigma1': require('icons/networks/logos/artis.svg'),
'lukso/l14': require('icons/networks/logos/lukso.svg'),
astar: require('icons/networks/logos/astar.svg'),
shiden: require('icons/networks/logos/shiden.svg'),
shibuya: require('icons/networks/logos/shibuya.svg'),
};
const NETWORKS: Array<Network> = (() => {
const networksFromConfig: Array<Network> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
logo: network.logo || LOGOS[network.type + (network.subType ? `/${ network.subType }` : '')],
icon: network.icon || ICONS[network.type + (network.subType ? `/${ network.subType }` : '')],
}));
})();
export default NETWORKS;
// for easy .env.example update
// const FOR_CONFIG = JSON.stringify([
// {
// name: 'Gnosis Chain',
// type: 'xdai',
// subType: 'mainnet',
// group: 'mainnets',
// isAccountSupported: true,
// chainId: 100,
// currency: 'xDAI',
// },
// {
// name: 'Optimism on Gnosis Chain',
// shortName: 'OoG',
// type: 'xdai',
// subType: 'optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// logo: 'https://www.fillmurray.com/240/60',
// chainId: 300,
// currency: 'xDAI',
// },
// {
// name: 'Arbitrum on xDai',
// type: 'xdai',
// subType: 'aox',
// group: 'mainnets',
// chainId: 200,
// currency: 'xDAI',
// },
// {
// name: 'Ethereum',
// shortName: 'ETH',
// type: 'eth',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 1,
// currency: 'ETH',
// },
// {
// name: 'Ethereum Classic',
// shortName: 'ETC',
// type: 'etc',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 61,
// currency: 'ETC',
// },
// {
// name: 'POA',
// shortName: 'POA',
// type: 'poa',
// subType: 'core',
// group: 'mainnets',
// chainId: 99,
// currency: 'POA',
// isAccountSupported: true,
// nativeTokenAddress: '0x029a799563238d0e75e20be2f4bda0ea68d00172',
// },
// {
// name: 'RSK',
// shortName: 'RBTC',
// type: 'rsk',
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 30,
// currency: 'RBTC',
// },
// {
// name: 'Gnosis Chain Testnet',
// type: 'xdai',
// subType: 'testnet',
// group: 'testnets',
// isAccountSupported: true,
// currency: 'xDAI',
// },
// {
// name: 'POA Sokol',
// shortName: 'POA',
// type: 'poa',
// subType: 'sokol',
// group: 'testnets',
// chainId: 77,
// currency: 'SPOA',
// },
// {
// name: 'ARTIS Σ1',
// type: 'artis',
// subType: 'sigma1',
// group: 'other',
// chainId: 246529,
// currency: 'ATS',
// },
// {
// name: 'LUKSO L14',
// shortName: 'POA',
// type: 'lukso',
// subType: 'l14',
// group: 'other',
// chainId: 22,
// currency: 'LYX',
// },
// {
// name: 'Astar',
// type: 'astar',
// group: 'other',
// chainId: 22,
// currency: 'ASTR',
// },
// ]);
import appConfig from 'configs/app/config';
import type { FeaturedNetwork } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': gnosisIcon,
'/xdai/optimism': optimismIcon,
'/xdai/aox': arbitrumIcon,
'/eth/mainnet': ethereumIcon,
'/etc/mainnet': ethereumClassicIcon,
'/poa/core': poaIcon,
'/rsk/mainnet': rskIcon,
'/xdai/testnet': arbitrumIcon,
'/poa/sokol': poaSokolIcon,
'/artis/sigma1': artisIcon,
};
// for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([
// {
// title: 'Gnosis Chain',
// basePath: '/xdai/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Optimism on Gnosis Chain',
// basePath: '/xdai/optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// },
// {
// title: 'Arbitrum on xDai',
// basePath: '/xdai/aox',
// group: 'mainnets',
// },
// {
// title: 'Ethereum',
// basePath: '/eth/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Ethereum Classic',
// basePath: '/etx/mainnet',
// group: 'mainnets',
// },
// {
// title: 'POA',
// basePath: '/poa/core',
// group: 'mainnets',
// },
// {
// title: 'RSK',
// basePath: '/rsk/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Gnosis Chain Testnet',
// basePath: '/xdai/testnet',
// group: 'testnets',
// },
// {
// title: 'POA Sokol',
// basePath: '/poa/sokol',
// group: 'testnets',
// },
// {
// title: 'ARTIS Σ1',
// basePath: '/artis/sigma1',
// group: 'other',
// },
// {
// title: 'LUKSO L14',
// basePath: '/lukso/l14',
// group: 'other',
// },
// {
// title: 'Astar',
// basePath: '/astar',
// group: 'other',
// },
// ]).replaceAll('"', '\'');
function parseNetworkConfig() {
try {
return JSON.parse(appConfig.featuredNetworks || '[]');
} catch (error) {
return [];
}
}
const featuredNetworks: Array<FeaturedNetwork> = (() => {
const networksFromConfig: Array<FeaturedNetwork> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
icon: network.icon || ICONS[network.basePath],
}));
})();
export default featuredNetworks;
import availableNetworks from 'lib/networks/availableNetworks';
interface Params {
network_type: string;
network_sub_type?: string;
}
export default function findNetwork(params: Params) {
return availableNetworks.find((network) =>
network.type === params.network_type &&
network.subType ? network.subType === params.network_sub_type : network.type === params.network_type,
);
}
import NETWORKS from './availableNetworks';
export default function getAvailablePaths() {
return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType || 'mainnet' } }));
}
import findNetwork from './findNetwork';
import appConfig from 'configs/app/config';
export default function getNetworkTitle({ network_type: type, network_sub_type: subType }: {network_type?: string; network_sub_type?: string}) {
const currentNetwork = findNetwork({ network_type: type || '', network_sub_type: subType });
if (currentNetwork) {
return currentNetwork.name + (currentNetwork.shortName ? ` (${ currentNetwork.shortName })` : '') + ' Explorer';
}
return '';
export default function getNetworkTitle() {
return appConfig.network.name + (appConfig.network.shortName ? ` (${ appConfig.network.shortName })` : '') + ' Explorer';
}
const supportedNetworks = process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS?.replaceAll('\'', '"');
// should be CommonJS module since it used for next.config.js
function parseNetworkConfig() {
try {
return JSON.parse(supportedNetworks || '[]');
} catch (error) {
return [];
}
}
module.exports = parseNetworkConfig;
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import React from 'react';
import useNetwork from 'lib/hooks/useNetwork';
import isAccountRoute from 'lib/link/isAccountRoute';
import { link } from 'lib/link/link';
import { ROUTES } from 'lib/link/routes';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import NETWORKS from 'lib/networks/availableNetworks';
import featuredNetworks from 'lib/networks/featuredNetworks';
export default function useNetworkNavigationItems() {
const selectedNetwork = useNetwork();
const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName];
const router = useRouter();
const isAccount = isAccountRoute(currentRouteName);
return React.useMemo(() => {
return NETWORKS.map((network) => {
const routeName = (() => {
if ('crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation) {
if ((isAccount && network.isAccountSupported) || !isAccount) {
return currentRouteName;
}
}
return 'network_index';
})();
const url = link(routeName, { ...router.query, network_type: network.type, network_sub_type: network.subType });
return featuredNetworks.map((network) => {
const routeName = 'crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation ? currentRouteName : 'network_index';
const [ , networkType, networkSubtype ] = network.basePath.split('/');
const url = link(routeName, { ...router.query, network_type: networkType, network_sub_type: networkSubtype });
return {
...network,
url: url,
isActive: selectedNetwork?.type === network.type && selectedNetwork?.subType === network?.subType,
isActive: appConfig.network.basePath === network.basePath,
};
});
}, [ currentRoute, currentRouteName, isAccount, router.query, selectedNetwork?.subType, selectedNetwork?.type ]);
}, [ currentRoute, currentRouteName, router.query ]);
}
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
......@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
const networkTitle = getNetworkTitle();
return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '',
......
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
......@@ -14,8 +14,8 @@ type Props = {
tab: BlocksProps['tab'];
}
const BlocksNextPage: NextPage<Props> = ({ pageParams, tab }: Props) => {
const { title } = getSeo(pageParams);
const BlocksNextPage: NextPage<Props> = ({ tab }: Props) => {
const { title } = getSeo();
return (
<>
<Head>
......
import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
export default function getSeo() {
return {
title: params ? `${ networkTitle } - BlockScout` : '',
title: getNetworkTitle(),
};
}
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
return { paths: [], fallback: 'blocking' };
};
......@@ -3,7 +3,7 @@ import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) {
const networkTitle = getNetworkTitle(params || {});
const networkTitle = getNetworkTitle();
return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '',
......
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
import appConfig from 'configs/app/config';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy';
import { link } from 'lib/link/link';
import findNetwork from 'lib/networks/findNetwork';
const cspPolicy = getCspPolicy();
......@@ -20,9 +20,8 @@ export function middleware(req: NextRequest) {
network_type: networkType,
network_sub_type: networkSubtype,
};
const selectedNetwork = findNetwork(networkParams);
if (!selectedNetwork) {
if (appConfig.network.type !== networkType && appConfig.network.subtype !== networkSubtype) {
const url = req.nextUrl.clone();
url.pathname = `/404`;
return NextResponse.rewrite(url);
......
......@@ -9,8 +9,11 @@
},
"scripts": {
"dev": "next dev",
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets yarn dev",
"build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.secrets blockscout",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons"
......@@ -46,6 +49,7 @@
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"dotenv-cli": "^6.0.0",
"eslint": "8.16.0",
"eslint-config-next": "^12.3.0",
"eslint-plugin-es5": "^1.5.0",
......
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const ApiKeysPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const ApiKeysPage: NextPage<Props> = ({ pageParams }: Props) => {
export default ApiKeysPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const CustomAbiPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const CustomAbiPage: NextPage<Props> = ({ pageParams }: Props) => {
export default CustomAbiPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const PublicTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const PublicTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default PublicTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const TransactionTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const WatchListPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head>
......@@ -28,5 +28,5 @@ const WatchListPage: NextPage<Props> = ({ pageParams }: Props) => {
export default WatchListPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,3 +17,6 @@ const AppsPage = () => {
};
export default AppsPage;
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const AppPage: NextPage = () => {
export default AppPage;
export { getStaticPaths } from 'lib/next/apps/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const HomePage: NextPage = () => {
export default HomePage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,5 +17,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -15,5 +15,5 @@ const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
export default TransactionPage;
export { getStaticPaths } from 'lib/next/tx/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -14,8 +14,8 @@ type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
const AddressTagsPage: NextPage<Props> = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
......@@ -26,5 +26,5 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -17,5 +17,5 @@ const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
export default BlockPage;
export { getStaticPaths } from 'lib/next/block/getStaticPaths';
export { getStaticPaths } from 'lib/next/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -2,18 +2,9 @@ import type { FunctionComponent, SVGAttributes } from 'react';
export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export interface Network {
name: string;
chainId: number; // https://chainlist.org/
currency: string;
nativeTokenAddress: string;
shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string;
subType?: string;
group: 'mainnets' | 'testnets' | 'other';
export interface FeaturedNetwork {
title: string;
basePath: string;
group: NetworkGroup;
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
logo?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean;
assetsNamePath?: string;
}
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -7,7 +8,6 @@ import { block } from 'data/block';
import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import { space } from 'lib/html-entities';
import useLink from 'lib/link/useLink';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -23,7 +23,6 @@ const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const link = useLink();
const router = useRouter();
const network = useNetwork();
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
......@@ -79,7 +78,7 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Block reward"
hint={
`For each block, the miner is rewarded with a finite amount of ${ network?.currency || 'native token' }
`For each block, the miner is rewarded with a finite amount of ${ appConfig.network.currency || 'native token' }
on top of the fees paid for all transactions in the block.`
}
columnGap={ 1 }
......@@ -120,15 +119,16 @@ const BlockDetails = () => {
title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
>
<Text>{ (block.base_fee_per_gas / 10 ** 9).toLocaleString('en', { minimumFractionDigits: 18 }) } { network?.currency } </Text>
<Text>{ (block.base_fee_per_gas / 10 ** 9).toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency } </Text>
<Text variant="secondary" whiteSpace="pre">{ space }({ block.base_fee_per_gas.toLocaleString('en', { minimumFractionDigits: 9 }) } Gwei)</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ network?.currency || 'native token' } burned from transactions included in the block. Equals Block Base Fee per Gas * Gas Used.` }
hint={ `Amount of ${ appConfig.network.currency || 'native token' } burned from transactions included in the block.
Equals Block Base Fee per Gas * Gas Used.` }
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ block.burnt_fees.toLocaleString('en', { minimumFractionDigits: 18 }) } { network?.currency }</Text>
<Text ml={ 1 }>{ block.burnt_fees.toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency }</Text>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
<Utilization ml={ 4 } value={ block.burnt_fees / block.reward.tx_fee }/>
......
import { Flex, Link, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
......@@ -6,7 +7,6 @@ import type ArrayElement from 'types/utils/ArrayElement';
import type { blocks } from 'data/blocks';
import flameIcon from 'icons/flame.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -21,7 +21,6 @@ interface Props {
const BlocksListItem = ({ data, isPending }: Props) => {
const spinnerEmptyColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const link = useLink();
const network = useNetwork();
return (
<AccountListItemMobile rowGap={ 3 }>
......@@ -58,7 +57,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
</Flex>
</Box>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { network?.currency }</Text>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency }</Text>
<Text variant="secondary">{ (data.reward.static + data.reward.tx_fee - data.burnt_fees).toLocaleString('en', { maximumFractionDigits: 5 }) }</Text>
</Flex>
<Flex>
......
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import { blocks } from 'data/blocks';
import useNetwork from 'lib/hooks/useNetwork';
import BlocksTableItem from 'ui/blocks/BlocksTableItem';
const BlocksTable = () => {
const network = useNetwork();
return (
<TableContainer width="100%" mt={ 8 }>
......@@ -18,8 +17,8 @@ const BlocksTable = () => {
<Th width="144px">Miner</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="40%">Gas used</Th>
<Th width="30%">Reward { network?.currency }</Th>
<Th width="30%">Burnt fees { network?.currency }</Th>
<Th width="30%">Reward { appConfig.network.currency }</Th>
<Th width="30%">Burnt fees { appConfig.network.currency }</Th>
</Tr>
</Thead>
<Tbody>
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import * as Sentry from '@sentry/react';
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => {
const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
......@@ -20,8 +19,8 @@ const Home = () => {
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
setFormVisibility(Boolean(!token && appConfig.isAccountSupported));
}, []);
const checkSentry = React.useCallback(() => {
Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } });
......@@ -53,7 +52,7 @@ const Home = () => {
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageTitle text={
`Home Page for ${ selectedNetwork?.name } network`
`Home Page for ${ appConfig.network.name } network`
}/>
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
{ /* will be deleted when we move to new CI */ }
......
import { Image, chakra } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { Network } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
const EmptyElement = () => null;
const ASSETS_PATH_MAP: Record<string, string> = {
'xdai/mainnet': 'xdai',
'xdai/testnet': 'xdai',
'xdai/optimism': 'optimism',
'xdai/aox': 'arbitrum',
'eth/mainnet': 'ethereum',
'etc/mainnet': 'classic',
'poa/core': 'poa',
};
const getAssetsPath = (network: Network) => {
if (network.assetsNamePath) {
return network.assetsNamePath;
}
const key = [ network.type, network.subType ].filter(Boolean).join('/');
const nameFromMap = ASSETS_PATH_MAP[key];
return nameFromMap || network.type;
};
interface Props {
hash: string;
name: string;
name?: string;
className?: string;
}
const TokenLogo = ({ hash, name, className }: Props) => {
const network = useNetwork();
if (!network) {
return null;
}
const assetsPath = getAssetsPath(network);
const logoSrc = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${ assetsPath }/assets/${ hash }/logo.png`;
return <Image className={ className } src={ logoSrc } alt={ `${ name } logo` } fallback={ <EmptyElement/> }/>;
const logoSrc = `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
${ appConfig.network.assetsPathname || appConfig.network.type }
/assets/
${ hash }
/logo.png
`;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>;
};
export default React.memo(chakra(TokenLogo));
import { Box, VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import ghIcon from 'icons/social/git.svg';
......@@ -8,14 +9,13 @@ import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [
{ link: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK, icon: ghIcon, label: 'Github link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK, icon: twIcon, label: 'Twitter link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK, icon: tgIcon, label: 'Telegram link' },
{ link: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK, icon: statsIcon, label: 'Staking analytic link' },
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
{ link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link !== undefined);
const BLOCKSCOUT_VERSION = process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT_VERSION }`;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
interface Props {
isCollapsed?: boolean;
......@@ -60,7 +60,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
<Text variant="secondary" mb={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text>
</Box>
</VStack>
);
......
import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
......@@ -15,7 +15,6 @@ import NavLink from './NavLink';
const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isInBrowser = isBrowser();
const [ hasAccount, setHasAccount ] = React.useState(false);
......@@ -31,9 +30,9 @@ const NavigationDesktop = () => {
if (navBarCollapsedCookie === 'false') {
setCollapsedState(false);
}
setHasAccount(Boolean(selectedNetwork?.isAccountSupported && isAuth && isInBrowser));
setHasAccount(Boolean(appConfig.isAccountSupported && isAuth && isInBrowser));
}
}, [ isInBrowser, selectedNetwork?.isAccountSupported ]);
}, [ isInBrowser ]);
const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag);
......
import { Box, VStack } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink';
const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const hasAccount = appConfig.isAccountSupported && isAuth;
return (
<>
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import NextLink from 'next/link';
import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react';
import blockscoutLogo from 'icons/logo.svg';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
// predefined network logos
const LOGOS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': require('icons/networks/logos/gnosis.svg'),
'/eth/mainnet': require('icons/networks/logos/eth.svg'),
'/etc/mainnet': require('icons/networks/logos/etc.svg'),
'/poa/core': require('icons/networks/logos/poa.svg'),
'/rsk/mainnet': require('icons/networks/logos/rsk.svg'),
'/xdai/testnet': require('icons/networks/logos/gnosis.svg'),
'/poa/sokol': require('icons/networks/logos/sokol.svg'),
'/artis/sigma1': require('icons/networks/logos/artis.svg'),
'/lukso/l14': require('icons/networks/logos/lukso.svg'),
'/astar': require('icons/networks/logos/astar.svg'),
'/shiden': require('icons/networks/logos/shiden.svg'),
'/shibuya': require('icons/networks/logos/shibuya.svg'),
};
interface Props {
isCollapsed?: boolean;
onClick?: (event: React.SyntheticEvent) => void;
......@@ -17,8 +33,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white');
const link = useLink();
const href = link('network_index');
const network = useNetwork();
const logo = network?.logo;
const logo = appConfig.network.logo || LOGOS[appConfig.network.basePath];
const style = useColorModeValue({}, { filter: 'brightness(0) invert(1)' });
......@@ -29,7 +44,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<Image
h="20px"
src={ logo }
alt={ `${ network.type } ${ network.subType ? network.subType : '' } network icon` }
alt={ `${ appConfig.network.name } network icon` }
/>
);
} else if (typeof logo !== undefined) {
......
......@@ -3,18 +3,17 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import NETWORKS from 'lib/networks/availableNetworks';
import featuredNetworks from 'lib/networks/featuredNetworks';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const availableTabs = TABS.filter((tab) => NETWORKS.some(({ group }) => group === tab));
const availableTabs = TABS.filter((tab) => featuredNetworks.some(({ group }) => group === tab));
const NetworkMenuPopup = () => {
const selectedNetwork = useNetwork();
const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab);
return (
......@@ -35,7 +34,7 @@ const NetworkMenuPopup = () => {
.filter((network) => network.group === tab)
.map((network) => (
<NetworkMenuLink
key={ network.name }
key={ network.title }
{ ...network }
/>
)) }
......
......@@ -4,7 +4,6 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink';
......@@ -12,9 +11,9 @@ import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const NetworkMenuContentMobile = () => {
const selectedNetwork = useNetwork();
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const items = useNetworkNavigationItems();
const selectedNetwork = items.find(({ isActive }) => isActive);
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTab(event.target.value as NetworkGroup);
......@@ -30,7 +29,7 @@ const NetworkMenuContentMobile = () => {
.filter(({ group }) => group === selectedTab)
.map((network) => (
<NetworkMenuLink
key={ network.name }
key={ network.title }
{ ...network }
isMobile
/>
......
......@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { Network } from 'types/networks';
import type { FeaturedNetwork } from 'types/networks';
import checkIcon from 'icons/check.svg';
import placeholderIcon from 'icons/networks/icons/placeholder.svg';
import useColors from './useColors';
interface Props extends Network {
interface Props extends FeaturedNetwork {
isActive: boolean;
isMobile?: boolean;
url: string;
}
const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: Props) => {
const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon });
const iconEl = typeof icon === 'string' ? (
<Image w="30px" h="30px" src={ icon } alt={ `${ type } ${ subType ? subType : '' } network icon` }/>
<Image w="30px" h="30px" src={ icon } alt={ `${ title } network icon` }/>
) : (
<Icon
as={ hasIcon ? icon : placeholderIcon }
......@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }:
fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' }
>
{ name }
{ title }
</Text>
{ isActive && (
<Icon
......
import { Grid, GridItem, Text, Box, Icon, Link, Tag, Flex, Tooltip, chakra } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -8,7 +9,6 @@ import flameIcon from 'icons/flame.svg';
import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -26,8 +26,6 @@ import TokenTransfer from 'ui/tx/TokenTransfer';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
const TxDetails = () => {
const selectedNetwork = useNetwork();
const [ isExpanded, setIsExpanded ] = React.useState(false);
const handleCutClick = React.useCallback(() => {
......@@ -126,21 +124,21 @@ const TxDetails = () => {
title="Value"
hint="Value sent in the native token (and USD) if applicable."
>
<Text>{ tx.amount.value } { selectedNetwork?.currency }</Text>
<Text>{ tx.amount.value } { appConfig.network.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.amount.value_usd.toFixed(2) })</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Transaction fee"
hint="Total transaction fee."
>
<Text>{ tx.fee.value } { selectedNetwork?.currency }</Text>
<Text>{ tx.fee.value } { appConfig.network.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
>
<Text mr={ 1 }>{ tx.gas_price.toLocaleString('en', { minimumFractionDigits: 18 }) } { selectedNetwork?.currency }</Text>
<Text mr={ 1 }>{ tx.gas_price.toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency }</Text>
<Text variant="secondary">({ (tx.gas_price * Math.pow(10, 18)).toFixed(0) } Gwei)</Text>
</DetailsInfoItem>
<DetailsInfoItem
......@@ -174,10 +172,10 @@ const TxDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ selectedNetwork?.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
hint={ `Amount of ${ appConfig.network.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 } mr={ 1 }>{ tx.burnt_fees.value.toLocaleString('en', { minimumFractionDigits: 18 }) } { selectedNetwork?.currency }</Text>
<Text ml={ 1 } mr={ 1 }>{ tx.burnt_fees.value.toLocaleString('en', { minimumFractionDigits: 18 }) } { appConfig.network.currency }</Text>
<Text variant="secondary">(${ tx.burnt_fees.value_usd.toFixed(2) })</Text>
</DetailsInfoItem>
<GridItem colSpan={{ base: undefined, lg: 2 }}>
......
......@@ -2,15 +2,12 @@ import { Box } from '@chakra-ui/react';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: typeof txData}) => {
const selectedNetwork = useNetwork();
return (
<Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.id } { ...item } currency={ selectedNetwork?.currency }/>) }
{ data.map((item) => <TxInternalsListItem key={ item.id } { ...item }/>) }
</Box>
);
};
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import capitalize from 'lodash/capitalize';
import React from 'react';
......@@ -12,9 +13,9 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
type Props = ArrayElement<typeof data> & { currency?: string };
type Props = ArrayElement<typeof data>;
const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency }: Props) => {
const TxInternalsListItem = ({ type, status, from, to, value, gasLimit }: Props) => {
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex>
......@@ -33,7 +34,7 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency
</Address>
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { currency }</Text>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
<HStack spacing={ 3 }>
......
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
const TxInternalsTable = ({ data }: { data: typeof txData}) => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
......@@ -17,7 +16,7 @@ const TxInternalsTable = ({ data }: { data: typeof txData}) => {
<Th width="20%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="16%" isNumeric>Value { selectedNetwork?.currency }</Th>
<Th width="16%" isNumeric>Value { appConfig.network.currency }</Th>
<Th width="16%" isNumeric>Gas limit</Th>
</Tr>
</Thead>
......
import { AccordionItem, AccordionButton, AccordionIcon, Button, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { data } from 'data/txState';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
......@@ -17,7 +17,6 @@ import TxStateStorageItem from './TxStateStorageItem';
type Props = ArrayElement<typeof data>;
const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props) => {
const selectedNetwork = useNetwork();
const hasStorageData = Boolean(storage?.length);
......@@ -62,20 +61,20 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>Before</Text>
<Flex>
<Text>{ before.balance } { selectedNetwork?.currency }</Text>
<Text>{ before.balance } { appConfig.network.currency }</Text>
<TextSeparator/>
{ typeof before.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ before.nonce }</Text> }
</Flex>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>After</Text>
<Text>{ after.balance } { selectedNetwork?.currency }</Text>
<Text>{ after.balance } { appConfig.network.currency }</Text>
{ typeof after.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ after.nonce }</Text> }
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>State difference</Text>
<Stat>
{ diff } { selectedNetwork?.currency }
{ diff } { appConfig.network.currency }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Flex>
......
......@@ -6,15 +6,13 @@ import {
Th,
TableContainer,
} from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import { data } from 'data/txState';
import useNetwork from 'lib/hooks/useNetwork';
import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
......@@ -23,9 +21,9 @@ const TxStateTable = () => {
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency }` }</Th>
</Tr>
</Thead>
<Tbody>
......
import { Box, Heading, Text, Flex, Link, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
const selectedNetwork = useNetwork();
const sectionBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const sectionProps = {
borderBottom: '1px solid',
......@@ -33,7 +31,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
<Text>{ tx.fee.value } { selectedNetwork?.currency }</Text>
<Text>{ tx.fee.value } { appConfig.network.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
</Flex>
</Box>
......
......@@ -10,6 +10,7 @@ import {
Text,
useColorModeValue,
useDisclosure } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
......@@ -18,7 +19,6 @@ import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -30,7 +30,6 @@ import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const selectedNetwork = useNetwork();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
......@@ -108,11 +107,11 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Address>
</Flex>
<Box mt={ 2 }>
<Text as="span">Value { selectedNetwork?.currency } </Text>
<Text as="span">Value { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ tx.amount.value.toFixed(8) }</Text>
</Box>
<Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { selectedNetwork?.currency } </Text>
<Text as="span">Fee { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ tx.fee.value.toFixed(8) }</Text>
</Box>
</Box>
......
import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import type { txs as data } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork';
import TxsTableItem from './TxsTableItem';
......@@ -16,8 +16,6 @@ type Props = {
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="810px" size="xs">
......@@ -35,14 +33,14 @@ const TxsTable = ({ txs, sort, sorting }: Props) => {
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ selectedNetwork?.currency }` }
{ `Value ${ appConfig.network.currency }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ selectedNetwork?.currency }` }
{ `Fee ${ appConfig.network.currency }` }
</Link>
</Th>
</Tr>
......
import { Grid, GridItem } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React, { useCallback } from 'react';
import { Controller } from 'react-hook-form';
import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form';
import useNetwork from 'lib/hooks/useNetwork';
import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ appConfig.network.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
type Props<Inputs extends FieldValues> = {
control: Control<Inputs>;
}
export default function AddressFormNotifications<Inputs extends FieldValues, Checkboxes extends Path<Inputs>>({ control }: Props<Inputs>) {
const selectedNetwork = useNetwork();
const NOTIFICATIONS_NAMES = React.useMemo(() => {
return [ selectedNetwork?.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
}, [ selectedNetwork?.currency ]);
// eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
<CheckboxInput<Inputs, Checkboxes> text={ text } field={ field }/>
......
import { HStack, VStack, Text, Icon, useColorModeValue } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React from 'react';
import type { TWatchlistItem } from 'types/client/account';
import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities';
import AddressSnippet from 'ui/shared/AddressSnippet';
import TokenLogo from 'ui/shared/TokenLogo';
......@@ -14,7 +14,6 @@ const DECIMALS = 18;
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const selectedNetwork = useNetwork();
const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1);
const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A';
......@@ -24,8 +23,9 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
{ selectedNetwork && <TokenLogo hash={ selectedNetwork.nativeTokenAddress } name={ selectedNetwork.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ selectedNetwork?.currency } balance:${ nbsp }` + nativeBalance }</Text>
{ appConfig.network.nativeTokenAddress &&
<TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `${ appConfig.network.currency } balance:${ nbsp }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack>
{ item.tokens_count && (
......
......@@ -2668,6 +2668,26 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
dotenv-cli@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-6.0.0.tgz#8a30cbc59d0a8aaa166b2fee0a9a55e23a1223ab"
integrity sha512-qXlCOi3UMDhCWFKe0yq5sg3X+pJAz+RQDiFN38AMSbUrnY3uZshSfDJUAge951OS7J9gwLZGfsBlWRSOYz/TRg==
dependencies:
cross-spawn "^7.0.3"
dotenv "^16.0.0"
dotenv-expand "^8.0.1"
minimist "^1.2.5"
dotenv-expand@^8.0.1:
version "8.0.3"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
dotenv@^16.0.0:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
......@@ -4159,7 +4179,7 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.6:
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
......
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