Commit 373a9ccc authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into nft-metadata

parents 41dc5ea7 f002b82f
......@@ -40,7 +40,8 @@ NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
......@@ -57,6 +57,7 @@ jobs:
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
env_vars: VALUES_DIR=deploy/values/main,APP_NAME=bs-stack
globalEnv: main
appNamespace: front-main
blockscoutIngressHost: blockscout
frontendIngressHost: blockscout
......
......@@ -81,6 +81,8 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` |
| NEXT_PUBLIC_API_SPEC_URL | `string` *(optional)* | Spec to be displayed on api-docs page | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` |
| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` *(optional)* | Txn hash for default query at GraphQl playground page | `0x69e3923eef50eada197c3336d546936d0c994211492c9f947a24c02827568f9f` |
### App configuration
| Variable | Type | Description | Default value
......
......@@ -136,6 +136,9 @@ const config = Object.freeze({
googleAnalytics: {
propertyId: getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID),
},
graphQL: {
defaultTxnHash: getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION) || '',
},
});
export default config;
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS=[{'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':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Astar (EVM)','url':'https://blockscout.com/astar','group':'mainnets','type':'astar'},{'title':'Shiden (EVM)','url':'https://blockscout.com/shiden','group':'mainnets','type':'astar'},{'title':'Klaytn Mainnet (Cypress)','url':'https://klaytn-mainnet.aws-k8s.blockscout.com/','group':'mainnets','type':'klaytn'},{'title':'Goerli','url':'https://blockscout.com/eth/goerli/','group':'testnets','type':'goerli'},{'title':'Optimism Goerli','url':'https://blockscout.com/optimism/goerli/','group':'testnets','type':'optimism_goerli'},{'title':'Optimism Bedrock Alpha','url':'https://blockscout.com/optimism/bedrock-alpha','group':'testnets','type':'optimism_bedrock_alpha'},{'title':'Gnosis Chiado','url':'https://blockscout.com/gnosis/chiado/','group':'testnets','type':'gnosis_chiado'},{'title':'Shibuya (EVM)','url':'https://blockscout.com/shibuya','group':'testnets','type':'shibuya'},{'title':'Optimism Opcraft','url':'https://blockscout.com/optimism/opcraft','group':'other','type':'optimism_opcraft'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'other','type':'optimism_gnosis'},{'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':'POA','url':'https://blockscout.com/poa/core','group':'other','type':'poa_core'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'other','type':'poa_sokol'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
# network config
NEXT_PUBLIC_NETWORK_NAME=Goerli
......
......@@ -2,6 +2,7 @@
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Göerli','url':'https://eth-goerli.blockscout.com/','group':'testnets','type':'goerli'},{'title':'Base Göerli','url':'https://l2-goerli.blockscout.com/','group':'testnets','type':'base_goerli'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
# network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli
......
......@@ -12,6 +12,22 @@ async function headers() {
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
],
},
];
......
......@@ -316,6 +316,7 @@ frontend:
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
resources:
limits:
......@@ -399,6 +400,8 @@ frontend:
_default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
# enable blockscout-allowance
allowance:
enabled: false
......
......@@ -134,14 +134,18 @@ frontend:
_default: ENC[AES256_GCM,data:JZ+dOLHGXe2vzb380jPuw5weEp5UXPLWlYj2JsCIRZ4bdV3agTbGIw==,iv:gyzp3Bkhlw3JX2/mg1r8IWruY1b57esLrv09+jGkZUM=,tag:0N/XzMJM1hAVp+xlLCJupA==,type:str]
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID:
_default: ENC[AES256_GCM,data:kdRaaD6raCQNEQOXOOQ=,iv:SZ/aAf5yxhFaF4w3s+S0GIhjrJ1zIlAvyu83HoU3vvk=,tag:6PV731fEz3L6YaTwa63LWA==,type:str]
ethBytecodeDb:
environment:
ETH_BYTECODE_DB__DATABASE__URL:
_default: ENC[AES256_GCM,data:xySAxuKJAxuUALZEf9eqxoqJfPIT6DiDDV7ZEv2E74oiRKzYssEU9/TNgx6Ptd0kIQB1Nyf6EHVq4c4q,iv:9Dt8QKev9osxWJCPgsIErAOTB7snAAyGLHuuSyfRzRo=,tag:mKjoQJxtSJGZ2luPJYpX8g==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-03-01T17:32:48Z"
mac: ENC[AES256_GCM,data:TiFoS4AA3cQz6zILW5BwaMrtcVz05YW7fnA8+JZuAZq8mym1s/0lLtgebX2tBlAnq3NGgZDJtfxWTrPQgPpbTl4w5TKa9Q9qVlh/aS7UDQ48TSgoLfIpUO0hx7XdkbhuDIyJjY1lmI8wFqx+8L3ZMYLIg0RTPslZmRI3yiNySMw=,iv:emawacKfMIM99ilwH97t70d+CYXkYnzWA+VFNK6pZco=,tag:PTmmyOg+6Dd2AbtJKHFDww==,type:str]
lastmodified: "2023-03-08T15:55:49Z"
mac: ENC[AES256_GCM,data:nl/CflZ5t09n3swP1vpR2rVjLVIW+H10/FCImDgeWuwt7F+0Whko3/UrPMypdfJeqiuCjWCmuynqjYqqZn2zjKabvc9bdo/RiQCyrdm7XIA+REA9XnKPnghYjkJYNm5kOLct5zif9Rq4wTfPOIpMRKnYvXrpnmiz5tE/lFXbYMw=,iv:mifwaplxQSJb5Q1CPWgR3hT5bTHTodNAoBEDKtdVLHI=,tag:rCAgVJxV52JV8O4CEZuLRg==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -129,7 +129,7 @@ blockscout:
ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8050
_default: http://eth-bytecode-db-svc:80
INDEXER_MEMORY_LIMIT:
_default: 5
ACCOUNT_ENABLED:
......@@ -272,6 +272,7 @@ scVerifier:
# enable https
tls:
enabled: true
createSecret: true
resources:
limits:
memory:
......@@ -364,6 +365,7 @@ frontend:
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
resources:
limits:
......@@ -442,3 +444,68 @@ frontend:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_IS_TESTNET:
_default: true
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
# enable eth-bytecode-db deploy
ethBytecodeDb:
enabled: true
image:
_default: ghcr.io/blockscout/eth-bytecode-db:main
replicas:
app: 1
docker:
port: 80
targetPort: 8050
metricsPort: 6060
strategy: RollingUpdate
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
annotations: {}
host:
_default: eth-bytecode-db-test.aws-k8s.blockscout.com
main: eth-bytecode-db-main.test.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
requests:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
environment:
ETH_BYTECODE_DB__SERVER__HTTP__MAX_BODY_SIZE:
_default: '10485760' # 10 Mb
ETH_BYTECODE_DB__VERIFIER__URI:
_default: https://grpc.sc-verifier-main.test.aws-k8s.blockscout.com
ETH_BYTECODE_DB__DATABASE__RUN_MIGRATIONS:
_default: true
ETH_BYTECODE_DB__TRACING__FORMAT:
_default: json
ETH_BYTECODE_DB__METRICS__ENABLED:
_default: true
# ENV:
# _default: 'true'
########### just example
########### put DATABASE envs in secrets.yaml under ethBytecodeDb.environment
# DATABASE_URL:
# # postgres pod
# _default: postgresql://<user>:<pass>@postgres:5432/<db>
# # RDS
# _default: postgresql://<user>:<pass>@db-eth-bytecode-db-svc:5432/<db>
......@@ -39,6 +39,7 @@ frontend:
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
resources:
limits:
......@@ -124,3 +125,5 @@ frontend:
_default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
......@@ -39,6 +39,7 @@ frontend:
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
resources:
limits:
......@@ -120,3 +121,5 @@ frontend:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_IS_TESTNET:
_default: true
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
......@@ -354,6 +354,11 @@ export const RESOURCES = {
path: '/api/v2/search/check-redirect',
},
// GraphQL
graphql: {
path: '/graphql',
},
// DEPRECATED
old_api: {
path: '/api',
......
import * as descriptors from './policies';
import { makePolicyString, mergeDescriptors } from './utils';
function generateCspPolicy() {
const policyDescriptor = mergeDescriptors(
descriptors.app(),
descriptors.ad(),
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
descriptors.sentry(),
descriptors.walletConnect(),
);
return makePolicyString(policyDescriptor);
}
export default generateCspPolicy;
import type CspDev from 'csp-dev';
import isSelfHosted from 'lib/isSelfHosted';
export function ad(): CspDev.DirectiveDescriptor {
if (!isSelfHosted()) {
return {};
}
return {
'connect-src': [
'coinzilla.com',
'*.coinzilla.com',
'request-global.czilladx.com',
],
'frame-src': [
'request-global.czilladx.com',
],
'script-src': [
'coinzillatag.com',
'servedbyadbutler.com',
// what hash is this?
'\'sha256-wMOeDjJaOTjCfNjluteV+tSqHW547T89sgxd8W6tQJM=\'',
// what hash is this?
'\'sha256-FcyIn1h7zra8TVnnRhYrwrplxJW7dpD5TV7kP2AG/kI=\'',
],
'img-src': [
'servedbyadbutler.com',
'cdn.coinzilla.io',
],
'font-src': [
'request-global.czilladx.com',
],
};
}
import type CspDev from 'csp-dev';
import appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks';
const KEY_WORDS = {
BLOB: 'blob:',
DATA: 'data:',
NONE: '\'none\'',
REPORT_SAMPLE: `'report-sample'`,
SELF: '\'self\'',
STRICT_DYNAMIC: `'strict-dynamic'`,
UNSAFE_INLINE: '\'unsafe-inline\'',
UNSAFE_EVAL: '\'unsafe-eval\'',
};
import { KEY_WORDS } from '../utils';
const MAIN_DOMAINS = [
`*.${ appConfig.host }`,
......@@ -20,16 +12,6 @@ const MAIN_DOMAINS = [
// eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getNetworksExternalAssetsHosts() {
const icons = featuredNetworks
.filter(({ icon }) => typeof icon === 'string')
.map(({ icon }) => new URL(icon as string).host);
const logo = appConfig.network.logo ? new URL(appConfig.network.logo).host : undefined;
return logo ? icons.concat(logo) : icons;
}
function getMarketplaceAppsHosts() {
return {
frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host),
......@@ -37,25 +19,12 @@ function getMarketplaceAppsHosts() {
};
}
// 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() {
export function app(): CspDev.DirectiveDescriptor {
const marketplaceAppsHosts = getMarketplaceAppsHosts();
return {
'default-src': [
// KEY_WORDS.NONE,
// temporarily, see if warnings for "/_next/static/chunks/8861-ad3efb7f624b7bc1.js" go away
...MAIN_DOMAINS,
KEY_WORDS.NONE,
],
'connect-src': [
......@@ -65,9 +34,6 @@ function makePolicyMap() {
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// client error monitoring
'sentry.io', '*.sentry.io',
// API
appConfig.api.endpoint,
appConfig.api.socket,
......@@ -75,67 +41,28 @@ function makePolicyMap() {
// chain RPC server
appConfig.network.rpcUrl,
// ad
'request-global.czilladx.com',
// walletconnect
'*.walletconnect.com',
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
// RPC providers
'https://infragrid.v.network',
'https://infragrid.v.network', // RPC providers
// github (spec for api-docs page)
'raw.githubusercontent.com',
// google analytics
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://stats.g.doubleclick.net',
],
].filter(Boolean),
'script-src': [
KEY_WORDS.SELF,
...MAIN_DOMAINS,
// next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS,
// hash of ColorModeScript
'\'sha256-e7MRMmTzLsLQvIy1iizO1lXf7VWYoQ6ysj5fuUzvRwE=\'',
// ad
'coinzillatag.com',
'servedbyadbutler.com',
'\'sha256-wMOeDjJaOTjCfNjluteV+tSqHW547T89sgxd8W6tQJM=\'',
'\'sha256-FcyIn1h7zra8TVnnRhYrwrplxJW7dpD5TV7kP2AG/kI=\'',
// reCAPTCHA from google
'https://www.google.com/recaptcha/api.js',
'https://www.gstatic.com',
'https://translate.google.com',
'\'sha256-FDyPg8CqqIpPAfGVKx1YeKduyLs0ghNYWII21wL+7HM=\'',
// google analytics
'\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'',
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
],
'style-src': [
KEY_WORDS.SELF,
...MAIN_DOMAINS,
// google fonts
'fonts.googleapis.com',
// reCAPTCHA from google
'https://www.gstatic.com',
// yes, it is unsafe as it stands, but
// - we cannot use hashes because all styles are generated dynamically
// - we cannot use nonces since we are not following along SSR path
......@@ -147,52 +74,29 @@ function makePolicyMap() {
'img-src': [
KEY_WORDS.SELF,
KEY_WORDS.DATA,
...MAIN_DOMAINS,
// github assets (e.g trustwallet token icons)
'raw.githubusercontent.com',
// auth0 assets and avatars
's.gravatar.com',
'i0.wp.com', 'i1.wp.com', 'i2.wp.com', 'i3.wp.com',
'lh3.googleusercontent.com', // google avatars
'avatars.githubusercontent.com', // github avatars
// network assets
...getNetworksExternalAssetsHosts(),
// marketplace apps logos
...marketplaceAppsHosts.logos,
// ad
'servedbyadbutler.com',
'cdn.coinzilla.io',
// walletconnect
'*.walletconnect.com',
// token's media
'ipfs.io',
// reCAPTCHA from google
'https://translate.google.com',
'https://www.gstatic.com',
// we agreed that using wildcard for images is mostly safe
// why do we have to use it? the main reason is that for NFT and inventory pages we get resources urls from API only on the client
// so they cannot be added to the policy on the server
// there could be 3 possible workarounds
// a/ use server side rendering approach, that we don't want to do
// b/ wrap every image/video in iframe with a source to static page for which we enforce certain img-src rule;
// the downsides is page performance slowdown and code complexity (have to manage click on elements, color mode for
// embedded page, etc)
// c/ use wildcard for img-src directive; this can lead to some security vulnerabilities but we were unable to find evidence
// that loose img-src directive alone could cause serious flaws on the site as long as we keep script-src and connect-src strict
//
// feel free to propose alternative solution and fix this
'*',
],
// google analytics
'https://www.google-analytics.com',
'media-src': [
'*', // see comment for img-src directive
],
'font-src': [
KEY_WORDS.DATA,
// google fonts
'fonts.gstatic.com',
'fonts.googleapis.com',
],
'prefetch-src': [
...MAIN_DOMAINS,
],
'object-src': [
......@@ -205,40 +109,12 @@ function makePolicyMap() {
'frame-src': [
...marketplaceAppsHosts.frames,
// ad
'request-global.czilladx.com',
// reCAPTCHA from google
// 'https://www.google.com/',
'https://www.google.com/recaptcha/api2/anchor',
'https://www.google.com/recaptcha/api2/bframe',
],
...(REPORT_URI ? {
...(REPORT_URI && !appConfig.isDev ? {
'report-uri': [
REPORT_URI,
],
} : {}),
};
}
function getCspPolicy() {
const policyMap = makePolicyMap();
const policyString = Object.entries(policyMap)
.map(([ key, value ]) => {
if (!value || value.length === 0) {
return;
}
const uniqueValues = unique(value);
return [ key, uniqueValues.join(' ') ].join(' ');
})
.filter(Boolean)
.join(';');
return policyString;
}
export default getCspPolicy;
import type CspDev from 'csp-dev';
import appConfig from 'configs/app/config';
export function googleAnalytics(): CspDev.DirectiveDescriptor {
if (!appConfig.googleAnalytics.propertyId) {
return {};
}
return {
'connect-src': [
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://stats.g.doubleclick.net',
],
'script-src': [
// inline script hash, see ui/shared/GoogleAnalytics.tsx
'\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'',
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
],
'img-src': [
'https://www.google-analytics.com',
],
};
}
import type CspDev from 'csp-dev';
export function googleFonts(): CspDev.DirectiveDescriptor {
// we use Inter and Poppins in the app
return {
'style-src': [
'fonts.googleapis.com',
],
'font-src': [
'fonts.gstatic.com',
'fonts.googleapis.com',
],
};
}
import type CspDev from 'csp-dev';
import appConfig from 'configs/app/config';
export function googleReCaptcha(): CspDev.DirectiveDescriptor {
if (!appConfig.reCaptcha.siteKey) {
return {};
}
return {
'script-src': [
'https://www.google.com/recaptcha/api.js',
'https://www.gstatic.com',
'https://translate.google.com',
'\'sha256-FDyPg8CqqIpPAfGVKx1YeKduyLs0ghNYWII21wL+7HM=\'',
],
'style-src': [
'https://www.gstatic.com',
],
'img-src': [
'https://translate.google.com',
'https://www.gstatic.com',
],
'frame-src': [
'https://www.google.com/recaptcha/api2/anchor',
'https://www.google.com/recaptcha/api2/bframe',
],
};
}
export { ad } from './ad';
export { app } from './app';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
export { sentry } from './sentry';
export { walletConnect } from './walletConnect';
import type CspDev from 'csp-dev';
export function sentry(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'sentry.io',
'*.sentry.io',
],
};
}
import type CspDev from 'csp-dev';
import appConfig from 'configs/app/config';
export function walletConnect(): CspDev.DirectiveDescriptor {
if (!appConfig.walletConnect.projectId || !appConfig.network.rpcUrl) {
return {};
}
return {
'connect-src': [
'*.walletconnect.com',
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
],
'img-src': [
'*.walletconnect.com',
],
};
}
import type CspDev from 'csp-dev';
export const KEY_WORDS = {
BLOB: 'blob:',
DATA: 'data:',
NONE: '\'none\'',
REPORT_SAMPLE: `'report-sample'`,
SELF: '\'self\'',
STRICT_DYNAMIC: `'strict-dynamic'`,
UNSAFE_INLINE: '\'unsafe-inline\'',
UNSAFE_EVAL: '\'unsafe-eval\'',
};
// we cannot use lodash/uniq and lodash/mergeWith in middleware code since it calls new Set() and it'is causing an error in Next.js
// "Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime"
export function unique(array: Array<string | undefined>) {
const set: Record<string, boolean> = {};
for (const item of array) {
item && (set[item] = true);
}
return Object.keys(set);
}
export function mergeDescriptors(...descriptors: Array<CspDev.DirectiveDescriptor>) {
return descriptors.reduce((result, item) => {
for (const _key in item) {
const key = _key as CspDev.Directive;
const value = item[key];
if (!value) {
continue;
}
if (result[key]) {
result[key]?.push(...value);
} else {
result[key] = [ ...value ];
}
}
return result;
}, {} as CspDev.DirectiveDescriptor);
}
export function makePolicyString(policyDescriptor: CspDev.DirectiveDescriptor) {
return Object.entries(policyDescriptor)
.map(([ key, value ]) => {
if (!value || value.length === 0) {
return;
}
const uniqueValues = unique(value);
return [ key, uniqueValues.join(' ') ].join(' ');
})
.filter(Boolean)
.join(';');
}
......@@ -140,7 +140,7 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/graphiql' as const },
icon: graphQLIcon,
isActive: pathname === '/graphiql',
isNewUi: false,
isNewUi: true,
},
{
text: 'RPC API',
......
......@@ -4,9 +4,9 @@ import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy';
import generateCspPolicy from 'lib/csp/generateCspPolicy';
const cspPolicy = getCspPolicy();
const cspPolicy = generateCspPolicy();
export function middleware(req: NextRequest) {
const isPageRequest = req.headers.get('accept')?.includes('text/html');
......@@ -28,7 +28,7 @@ export function middleware(req: NextRequest) {
const end = Date.now();
const res = NextResponse.next();
res.headers.append('Content-Security-Policy-Report-Only', cspPolicy);
res.headers.append(appConfig.isDev ? 'Content-Security-Policy' : 'Content-Security-Policy-Report-Only', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
return res;
......
......@@ -29,7 +29,7 @@
"test:jest:watch": "jest --watch"
},
"dependencies": {
"@chakra-ui/react": "2.4.3",
"@chakra-ui/react": "2.5.1",
"@chakra-ui/theme-tools": "^2.0.14",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
......@@ -51,6 +51,9 @@
"dom-to-image": "^2.6.0",
"ethers": "^5.7.2",
"framer-motion": "^6.5.1",
"graphiql": "^2.2.0",
"graphql": "^16.6.0",
"graphql-ws": "^5.11.3",
"js-cookie": "^3.0.1",
"lodash": "^4.0.0",
"next": "12.2.5",
......@@ -79,6 +82,7 @@
"@svgr/webpack": "^6.5.1",
"@testing-library/react": "^13.4.0",
"@total-typescript/ts-reset": "^0.3.7",
"@types/csp-dev": "^1.0.0",
"@types/d3": "^7.4.0",
"@types/dom-to-image": "^2.6.4",
"@types/jest": "^29.2.0",
......
......@@ -10,7 +10,7 @@ import SwaggerUI from 'ui/apiDocs/SwaggerUI';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage: NextPage = () => {
const APIDocsPage: NextPage = () => {
const networkTitle = getNetworkTitle();
return (
......@@ -22,7 +22,7 @@ const AppsPage: NextPage = () => {
);
};
export default AppsPage;
export default APIDocsPage;
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.apiDoc.specUrl) {
......
import type { NextApiRequest, NextApiResponse } from 'next';
import nodeFetch from 'node-fetch';
import { httpLogger } from 'lib/api/logger';
import getQueryParamString from 'lib/router/getQueryParamString';
export default async function mediaTypeHandler(req: NextApiRequest, res: NextApiResponse) {
httpLogger(req, res);
try {
const url = getQueryParamString(req.query.url);
const response = await nodeFetch(url, { method: 'HEAD' });
if (response.status !== 200) {
throw new Error();
}
const contentType = response.headers.get('content-type');
const mediaType = contentType?.startsWith('video') ? 'video' : 'image';
res.status(200).json({ type: mediaType });
} catch (error) {
res.status(200).json({ type: undefined });
}
}
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
const GraphQL = dynamic(() => import('ui/graphQL/GraphQL'), {
loading: () => <ContentLoader/>,
ssr: false,
});
import Head from 'next/head';
import React from 'react';
const GraphQLPage: NextPage = () => {
return null;
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const GraphiqlPage: NextPage = () => {
return (
<Page>
<Head><title>Graph Page</title></Head>
<PageTitle text="GraphQL playground"/>
<GraphQL/>
</Page>
);
};
export default GraphQLPage;
export default GraphiqlPage;
export async function getServerSideProps() {
return {
notFound: true,
};
}
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -28,7 +28,7 @@ import TestApp from 'playwright/TestApp';
test('disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } disabled>Click me</Button>
<Button variant={ variant } colorScheme={ colorScheme } isDisabled>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
......
......@@ -2,5 +2,5 @@ export type IndexingStatus = {
finished_indexing: boolean;
finished_indexing_blocks: boolean;
indexed_blocks_ratio: string;
indexed_inernal_transactions_ratio: string;
indexed_internal_transactions_ratio: string;
}
......@@ -15,6 +15,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf">
| StaticRoute<"/api/media-type">
| StaticRoute<"/api/proxy">
| StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }>
......
......@@ -50,7 +50,7 @@ const AddressTokens = () => {
return (
<>
<TokenBalances/>
{ /* should stay before tabs to scroll up whith pagination */ }
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
......
/* eslint-disable @typescript-eslint/naming-convention */
const SwaggerUIReact = dynamic(() => import('swagger-ui-react'), {
loading: () => <Spinner/>,
loading: () => <ContentLoader/>,
ssr: false,
});
import { Box, Spinner, useColorModeValue } from '@chakra-ui/react';
import { Box, useColorModeValue } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import React from 'react';
import appConfig from 'configs/app/config';
import ContentLoader from 'ui/shared/ContentLoader';
import 'swagger-ui-react/swagger-ui.css';
......
......@@ -101,7 +101,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<FormControl variant="floating" id="address">
<Input
{ ...field }
disabled={ true }
isDisabled={ true }
/>
<FormLabel data-in-modal="true">Auto-generated API key token</FormLabel>
</FormControl>
......@@ -147,7 +147,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isDirty }
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
......@@ -174,7 +174,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isDirty }
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
......
import { Box, useColorMode } from '@chakra-ui/react';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql';
import React from 'react';
import appConfig from 'configs/app/config';
import buildUrl from 'lib/api/buildUrl';
import 'graphiql/graphiql.css';
import isBrowser from 'lib/isBrowser';
const graphQLStyle = {
'.graphiql-container': {
backgroundColor: 'unset',
},
};
const GraphQL = () => {
const { colorMode } = useColorMode();
// colorModeState used as a key to re-render GraphiQL conponent after color mode change
const [ colorModeState, setColorModeState ] = React.useState(colorMode);
React.useEffect(() => {
if (isBrowser()) {
const graphqlTheme = window.localStorage.getItem('graphiql:theme');
if (graphqlTheme !== colorMode) {
window.localStorage.setItem('graphiql:theme', colorMode);
setColorModeState(colorMode);
}
}
}, [ colorMode ]);
const initialQuery = `{
transaction(
hash: "${ appConfig.graphQL.defaultTxnHash }"
) {
hash
blockNumber
value
gasUsed
}
}`;
const graphqlUrl = buildUrl('graphql');
const fetcher = createGraphiQLFetcher({
url: graphqlUrl,
// graphql ws implementation with absinthe plugin is incompatible with graphiql-ws protocol
// or the older one subscriptions-transport-ws
// so we (isstuev & vbaranov) decided to configure playground without subscriptions
// in case of any complaint consider reconfigure the graphql ws server with absinthe_graphql_ws package
// subscriptionUrl: `wss://${appConfig.host}/socket/`,
});
return (
<Box h="100vh" overflowX="scroll" sx={ graphQLStyle }>
<Box h="100vh" minW="900px" sx={ graphQLStyle }>
<GraphiQL fetcher={ fetcher } defaultQuery={ initialQuery } key={ colorModeState }/>
</Box>
</Box>
);
};
export default GraphQL;
......@@ -52,12 +52,12 @@ const IndexingAlert = ({ className }: { className?: string }) => {
handler: handleBlocksIndexStatus,
});
const handleIntermalTxsIndexStatus: SocketMessage.InternalTxsIndexStatus['handler'] = React.useCallback((payload) => {
const handleInternalTxsIndexStatus: SocketMessage.InternalTxsIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_indexing_status'), (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing = payload.finished;
newData.indexed_inernal_transactions_ratio = payload.ratio;
newData.indexed_internal_transactions_ratio = payload.ratio;
return newData;
});
......@@ -71,7 +71,7 @@ const IndexingAlert = ({ className }: { className?: string }) => {
useSocketMessage({
channel: internalTxsIndexingChannel,
event: 'internal_txs_index_status',
handler: handleIntermalTxsIndexStatus,
handler: handleInternalTxsIndexStatus,
});
if (isError) {
......@@ -87,8 +87,8 @@ const IndexingAlert = ({ className }: { className?: string }) => {
content = `${ data.indexed_blocks_ratio && `${ Math.floor(Number(data.indexed_blocks_ratio) * 100) }% Blocks Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.` ;
} else if (data.finished_indexing === false) {
content = `${ data.indexed_inernal_transactions_ratio &&
`${ Math.floor(Number(data.indexed_inernal_transactions_ratio) * 100) }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` }
content = `${ data.indexed_internal_transactions_ratio &&
`${ Math.floor(Number(data.indexed_internal_transactions_ratio) * 100) }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.`;
}
......
......@@ -112,7 +112,7 @@ const AddressPageContent = () => {
/>
) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ }
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : content }
{ !addressQuery.isLoading && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
......
......@@ -113,7 +113,7 @@ const ApiKeysPage: React.FC = () => {
<Button
size="lg"
onClick={ apiKeyModalProps.onOpen }
disabled={ !canAdd }
isDisabled={ !canAdd }
>
Add API key
</Button>
......
......@@ -178,7 +178,7 @@ const TokenPageContent = () => {
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
{ /* should stay before tabs to scroll up whith pagination */ }
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : (
......
......@@ -124,7 +124,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isDirty }
isDisabled={ !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -123,7 +123,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Button
size="lg"
type="submit"
disabled={ !isDirty }
isDisabled={ !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -236,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Button
size="lg"
type="submit"
disabled={ !isDirty }
isDisabled={ !isDirty }
isLoading={ mutation.isLoading }
>
Send request
......
......@@ -72,7 +72,7 @@ const DeleteModal: React.FC<Props> = ({
onClick={ onDeleteClick }
isLoading={ mutation.isLoading }
// FIXME: chackra's button is disabled when isLoading
disabled={ false }
isDisabled={ false }
>
Delete
</Button>
......
......@@ -25,7 +25,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
variant="outline"
size="sm"
onClick={ resetPage }
disabled={ page === 1 }
isDisabled={ page === 1 }
mr={ 4 }
>
First
......@@ -38,7 +38,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 3 }
disabled={ !canGoBackwards || page === 1 }
isDisabled={ !canGoBackwards || page === 1 }
/>
<Button
variant="outline"
......@@ -59,7 +59,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 3 }
disabled={ !hasNextPage }
isDisabled={ !hasNextPage }
/>
{ /* not implemented yet */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
......
......@@ -32,7 +32,7 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
variant="subtle"
colorScheme="gray"
onClick={ handelPrevClick }
disabled={ isPrevDisabled }
isDisabled={ isPrevDisabled }
/>
</Tooltip>
<Tooltip label={ nextLabel }>
......@@ -45,7 +45,7 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
colorScheme="gray"
ml="10px"
onClick={ handelNextClick }
disabled={ isNextDisabled }
isDisabled={ isNextDisabled }
/>
</Tooltip>
</Box>
......
......@@ -63,6 +63,7 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
onError={ handleLoadError }
onLoad={ handleLoad }
loading={ url ? 'lazy' : undefined }
/>
</AspectRatio>
);
......
import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import NftImage from './NftImage';
import NftVideo from './NftVideo';
import type { MediaType } from './utils';
import { getPreliminaryMediaType } from './utils';
interface Props {
imageUrl: string | null;
......@@ -11,7 +14,7 @@ interface Props {
}
const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
const [ type, setType ] = React.useState<'image' | 'video' | undefined>(!animationUrl ? 'image' : undefined);
const [ type, setType ] = React.useState<MediaType | undefined>(!animationUrl ? 'image' : undefined);
React.useEffect(() => {
if (!animationUrl) {
......@@ -20,10 +23,26 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
// media could be either gif or video
// so we pre-fetch the resources in order to get its content type
fetch(animationUrl, { method: 'HEAD' })
.then((response) => {
const contentType = response.headers.get('content-type');
setType(contentType?.startsWith('video') ? 'video' : 'image');
// have to do it via Node.js due to strict CSP for connect-src
// but in order not to abuse our server firstly we check file url extension
// and if it is valid we will trust it and display corresponding media component
const preliminaryType = getPreliminaryMediaType(animationUrl);
if (preliminaryType) {
setType(preliminaryType);
return;
}
const url = route({ pathname: '/api/media-type', query: { url: animationUrl } });
fetch(url)
.then((response) => response.json())
.then((_data) => {
const data = _data as { type: MediaType | undefined };
setType(data.type || 'image');
})
.catch(() => {
setType('image');
});
}, [ animationUrl ]);
......
export type MediaType = 'image' | 'video';
const IMAGE_EXTENSIONS = [
'.jpg', 'jpeg',
'.png',
'.gif',
'.svg',
];
const VIDEO_EXTENSIONS = [
'.mp4',
'.webm',
'.ogg',
];
export function getPreliminaryMediaType(url: string): MediaType | undefined {
if (IMAGE_EXTENSIONS.some((ext) => url.endsWith(ext))) {
return 'image';
}
if (url.startsWith('data:image')) {
return 'image';
}
if (VIDEO_EXTENSIONS.some((ext) => url.endsWith(ext))) {
return 'video';
}
}
......@@ -3,6 +3,7 @@ import NextLink from 'next/link';
import { route } from 'nextjs-routes';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import type { NavItem } from 'lib/hooks/useNavItems';
import { isInternalItem } from 'lib/hooks/useNavItems';
......@@ -17,6 +18,7 @@ type Props = {
}
const NavLink = ({ item, isCollapsed, px, className }: Props) => {
const isMobile = useIsMobile();
const colors = useColors();
const isExpanded = isCollapsed === false;
......@@ -45,7 +47,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => {
<Tooltip
label={ item.text }
hasArrow={ false }
isDisabled={ isCollapsed === false || (isCollapsed === undefined && isXLScreen) }
isDisabled={ isMobile || isCollapsed === false || (isCollapsed === undefined && isXLScreen) }
placement="right"
variant="nav"
gutter={ 20 }
......
......@@ -94,7 +94,7 @@ const Tokens = () => {
}
if (!data.items.length) {
if (debouncedFilter) {
if (debouncedFilter || type) {
return (
<>
{ bar }
......
......@@ -192,7 +192,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
size="lg"
type="submit"
isLoading={ pending }
disabled={ !isDirty }
isDisabled={ !isDirty }
>
{ !isAdd ? 'Save changes' : 'Add address' }
</Button>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment