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 ...@@ -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_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_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_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 # api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__ NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
...@@ -57,6 +57,7 @@ jobs: ...@@ -57,6 +57,7 @@ jobs:
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with: with:
env_vars: VALUES_DIR=deploy/values/main,APP_NAME=bs-stack env_vars: VALUES_DIR=deploy/values/main,APP_NAME=bs-stack
globalEnv: main
appNamespace: front-main appNamespace: front-main
blockscoutIngressHost: blockscout blockscoutIngressHost: blockscout
frontendIngressHost: blockscout frontendIngressHost: blockscout
......
...@@ -81,6 +81,8 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -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_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_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_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 ### App configuration
| Variable | Type | Description | Default value | Variable | Type | Description | Default value
......
...@@ -136,6 +136,9 @@ const config = Object.freeze({ ...@@ -136,6 +136,9 @@ const config = Object.freeze({
googleAnalytics: { googleAnalytics: {
propertyId: getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID), propertyId: getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID),
}, },
graphQL: {
defaultTxnHash: getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION) || '',
},
}); });
export default config; export default config;
# ui 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_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_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 # network config
NEXT_PUBLIC_NETWORK_NAME=Goerli NEXT_PUBLIC_NETWORK_NAME=Goerli
......
...@@ -2,6 +2,7 @@ ...@@ -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_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_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_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 # network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli NEXT_PUBLIC_NETWORK_NAME=Base Göerli
......
...@@ -12,6 +12,22 @@ async function headers() { ...@@ -12,6 +12,22 @@ async function headers() {
key: 'X-Content-Type-Options', key: 'X-Content-Type-Options',
value: 'nosniff', 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: ...@@ -316,6 +316,7 @@ frontend:
- "/api-docs" - "/api-docs"
- "/csv-export" - "/csv-export"
- "/verified-contracts" - "/verified-contracts"
- "/graphiql"
resources: resources:
limits: limits:
...@@ -399,6 +400,8 @@ frontend: ...@@ -399,6 +400,8 @@ frontend:
_default: https://blockscout-main.test.aws-k8s.blockscout.com _default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL: NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw _default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
# enable blockscout-allowance # enable blockscout-allowance
allowance: allowance:
enabled: false enabled: false
......
...@@ -134,14 +134,18 @@ frontend: ...@@ -134,14 +134,18 @@ frontend:
_default: ENC[AES256_GCM,data:JZ+dOLHGXe2vzb380jPuw5weEp5UXPLWlYj2JsCIRZ4bdV3agTbGIw==,iv:gyzp3Bkhlw3JX2/mg1r8IWruY1b57esLrv09+jGkZUM=,tag:0N/XzMJM1hAVp+xlLCJupA==,type:str] _default: ENC[AES256_GCM,data:JZ+dOLHGXe2vzb380jPuw5weEp5UXPLWlYj2JsCIRZ4bdV3agTbGIw==,iv:gyzp3Bkhlw3JX2/mg1r8IWruY1b57esLrv09+jGkZUM=,tag:0N/XzMJM1hAVp+xlLCJupA==,type:str]
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID:
_default: ENC[AES256_GCM,data:kdRaaD6raCQNEQOXOOQ=,iv:SZ/aAf5yxhFaF4w3s+S0GIhjrJ1zIlAvyu83HoU3vvk=,tag:6PV731fEz3L6YaTwa63LWA==,type:str] _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: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-03-01T17:32:48Z" lastmodified: "2023-03-08T15:55:49Z"
mac: ENC[AES256_GCM,data:TiFoS4AA3cQz6zILW5BwaMrtcVz05YW7fnA8+JZuAZq8mym1s/0lLtgebX2tBlAnq3NGgZDJtfxWTrPQgPpbTl4w5TKa9Q9qVlh/aS7UDQ48TSgoLfIpUO0hx7XdkbhuDIyJjY1lmI8wFqx+8L3ZMYLIg0RTPslZmRI3yiNySMw=,iv:emawacKfMIM99ilwH97t70d+CYXkYnzWA+VFNK6pZco=,tag:PTmmyOg+6Dd2AbtJKHFDww==,type:str] mac: ENC[AES256_GCM,data:nl/CflZ5t09n3swP1vpR2rVjLVIW+H10/FCImDgeWuwt7F+0Whko3/UrPMypdfJeqiuCjWCmuynqjYqqZn2zjKabvc9bdo/RiQCyrdm7XIA+REA9XnKPnghYjkJYNm5kOLct5zif9Rq4wTfPOIpMRKnYvXrpnmiz5tE/lFXbYMw=,iv:mifwaplxQSJb5Q1CPWgR3hT5bTHTodNAoBEDKtdVLHI=,tag:rCAgVJxV52JV8O4CEZuLRg==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -129,7 +129,7 @@ blockscout: ...@@ -129,7 +129,7 @@ blockscout:
ENABLE_RUST_VERIFICATION_SERVICE: ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true' _default: 'true'
RUST_VERIFICATION_SERVICE_URL: RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8050 _default: http://eth-bytecode-db-svc:80
INDEXER_MEMORY_LIMIT: INDEXER_MEMORY_LIMIT:
_default: 5 _default: 5
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
...@@ -272,6 +272,7 @@ scVerifier: ...@@ -272,6 +272,7 @@ scVerifier:
# enable https # enable https
tls: tls:
enabled: true enabled: true
createSecret: true
resources: resources:
limits: limits:
memory: memory:
...@@ -364,6 +365,7 @@ frontend: ...@@ -364,6 +365,7 @@ frontend:
- "/api-docs" - "/api-docs"
- "/csv-export" - "/csv-export"
- "/verified-contracts" - "/verified-contracts"
- "/graphiql"
resources: resources:
limits: limits:
...@@ -442,3 +444,68 @@ frontend: ...@@ -442,3 +444,68 @@ frontend:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml _default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_IS_TESTNET: NEXT_PUBLIC_IS_TESTNET:
_default: true _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: ...@@ -39,6 +39,7 @@ frontend:
- "/api-docs" - "/api-docs"
- "/csv-export" - "/csv-export"
- "/verified-contracts" - "/verified-contracts"
- "/graphiql"
resources: resources:
limits: limits:
...@@ -124,3 +125,5 @@ frontend: ...@@ -124,3 +125,5 @@ frontend:
_default: https://blockscout-main.test.aws-k8s.blockscout.com _default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL: NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw _default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
...@@ -39,6 +39,7 @@ frontend: ...@@ -39,6 +39,7 @@ frontend:
- "/api-docs" - "/api-docs"
- "/csv-export" - "/csv-export"
- "/verified-contracts" - "/verified-contracts"
- "/graphiql"
resources: resources:
limits: limits:
...@@ -120,3 +121,5 @@ frontend: ...@@ -120,3 +121,5 @@ frontend:
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml _default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_IS_TESTNET: NEXT_PUBLIC_IS_TESTNET:
_default: true _default: true
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
_default: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
...@@ -354,6 +354,11 @@ export const RESOURCES = { ...@@ -354,6 +354,11 @@ export const RESOURCES = {
path: '/api/v2/search/check-redirect', path: '/api/v2/search/check-redirect',
}, },
// GraphQL
graphql: {
path: '/graphql',
},
// DEPRECATED // DEPRECATED
old_api: { old_api: {
path: '/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 appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks';
import { KEY_WORDS } from '../utils';
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\'',
};
const MAIN_DOMAINS = [ const MAIN_DOMAINS = [
`*.${ appConfig.host }`, `*.${ appConfig.host }`,
...@@ -20,16 +12,6 @@ const MAIN_DOMAINS = [ ...@@ -20,16 +12,6 @@ const MAIN_DOMAINS = [
// eslint-disable-next-line no-restricted-properties // eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI; 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() { function getMarketplaceAppsHosts() {
return { return {
frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host), frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host),
...@@ -37,25 +19,12 @@ function getMarketplaceAppsHosts() { ...@@ -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 export function app(): CspDev.DirectiveDescriptor {
// "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() {
const marketplaceAppsHosts = getMarketplaceAppsHosts(); const marketplaceAppsHosts = getMarketplaceAppsHosts();
return { return {
'default-src': [ 'default-src': [
// KEY_WORDS.NONE, KEY_WORDS.NONE,
// temporarily, see if warnings for "/_next/static/chunks/8861-ad3efb7f624b7bc1.js" go away
...MAIN_DOMAINS,
], ],
'connect-src': [ 'connect-src': [
...@@ -65,9 +34,6 @@ function makePolicyMap() { ...@@ -65,9 +34,6 @@ function makePolicyMap() {
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason // webpack hmr in safari doesn't recognize localhost as 'self' for some reason
appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '', appConfig.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// client error monitoring
'sentry.io', '*.sentry.io',
// API // API
appConfig.api.endpoint, appConfig.api.endpoint,
appConfig.api.socket, appConfig.api.socket,
...@@ -75,67 +41,28 @@ function makePolicyMap() { ...@@ -75,67 +41,28 @@ function makePolicyMap() {
// chain RPC server // chain RPC server
appConfig.network.rpcUrl, appConfig.network.rpcUrl,
'https://infragrid.v.network', // RPC providers
// ad
'request-global.czilladx.com',
// walletconnect
'*.walletconnect.com',
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
// RPC providers
'https://infragrid.v.network',
// github (spec for api-docs page) // github (spec for api-docs page)
'raw.githubusercontent.com', 'raw.githubusercontent.com',
].filter(Boolean),
// google analytics
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://stats.g.doubleclick.net',
],
'script-src': [ 'script-src': [
KEY_WORDS.SELF, KEY_WORDS.SELF,
...MAIN_DOMAINS,
// next.js generates and rebuilds source maps in dev using eval() // next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278 // https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '', appConfig.isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS,
// hash of ColorModeScript // hash of ColorModeScript
'\'sha256-e7MRMmTzLsLQvIy1iizO1lXf7VWYoQ6ysj5fuUzvRwE=\'', '\'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': [ 'style-src': [
KEY_WORDS.SELF, KEY_WORDS.SELF,
...MAIN_DOMAINS, ...MAIN_DOMAINS,
// google fonts
'fonts.googleapis.com',
// reCAPTCHA from google
'https://www.gstatic.com',
// yes, it is unsafe as it stands, but // yes, it is unsafe as it stands, but
// - we cannot use hashes because all styles are generated dynamically // - we cannot use hashes because all styles are generated dynamically
// - we cannot use nonces since we are not following along SSR path // - we cannot use nonces since we are not following along SSR path
...@@ -147,52 +74,29 @@ function makePolicyMap() { ...@@ -147,52 +74,29 @@ function makePolicyMap() {
'img-src': [ 'img-src': [
KEY_WORDS.SELF, KEY_WORDS.SELF,
KEY_WORDS.DATA, KEY_WORDS.DATA,
...MAIN_DOMAINS, ...MAIN_DOMAINS,
// github assets (e.g trustwallet token icons) // we agreed that using wildcard for images is mostly safe
'raw.githubusercontent.com', // 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
// auth0 assets and avatars // there could be 3 possible workarounds
's.gravatar.com', // a/ use server side rendering approach, that we don't want to do
'i0.wp.com', 'i1.wp.com', 'i2.wp.com', 'i3.wp.com', // b/ wrap every image/video in iframe with a source to static page for which we enforce certain img-src rule;
'lh3.googleusercontent.com', // google avatars // the downsides is page performance slowdown and code complexity (have to manage click on elements, color mode for
'avatars.githubusercontent.com', // github avatars // embedded page, etc)
// c/ use wildcard for img-src directive; this can lead to some security vulnerabilities but we were unable to find evidence
// network assets // that loose img-src directive alone could cause serious flaws on the site as long as we keep script-src and connect-src strict
...getNetworksExternalAssetsHosts(), //
// feel free to propose alternative solution and fix this
// 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',
// google analytics 'media-src': [
'https://www.google-analytics.com', '*', // see comment for img-src directive
], ],
'font-src': [ 'font-src': [
KEY_WORDS.DATA, KEY_WORDS.DATA,
// google fonts
'fonts.gstatic.com',
'fonts.googleapis.com',
],
'prefetch-src': [
...MAIN_DOMAINS,
], ],
'object-src': [ 'object-src': [
...@@ -205,40 +109,12 @@ function makePolicyMap() { ...@@ -205,40 +109,12 @@ function makePolicyMap() {
'frame-src': [ 'frame-src': [
...marketplaceAppsHosts.frames, ...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': [
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 { ...@@ -140,7 +140,7 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/graphiql' as const }, nextRoute: { pathname: '/graphiql' as const },
icon: graphQLIcon, icon: graphQLIcon,
isActive: pathname === '/graphiql', isActive: pathname === '/graphiql',
isNewUi: false, isNewUi: true,
}, },
{ {
text: 'RPC API', text: 'RPC API',
......
...@@ -4,9 +4,9 @@ import { route } from 'nextjs-routes'; ...@@ -4,9 +4,9 @@ import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies'; 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) { export function middleware(req: NextRequest) {
const isPageRequest = req.headers.get('accept')?.includes('text/html'); const isPageRequest = req.headers.get('accept')?.includes('text/html');
...@@ -28,7 +28,7 @@ export function middleware(req: NextRequest) { ...@@ -28,7 +28,7 @@ export function middleware(req: NextRequest) {
const end = Date.now(); const end = Date.now();
const res = NextResponse.next(); 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 }`); res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
return res; return res;
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
"test:jest:watch": "jest --watch" "test:jest:watch": "jest --watch"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "2.4.3", "@chakra-ui/react": "2.5.1",
"@chakra-ui/theme-tools": "^2.0.14", "@chakra-ui/theme-tools": "^2.0.14",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4", "@emotion/styled": "^11.10.4",
...@@ -51,6 +51,9 @@ ...@@ -51,6 +51,9 @@
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"ethers": "^5.7.2", "ethers": "^5.7.2",
"framer-motion": "^6.5.1", "framer-motion": "^6.5.1",
"graphiql": "^2.2.0",
"graphql": "^16.6.0",
"graphql-ws": "^5.11.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.0.0", "lodash": "^4.0.0",
"next": "12.2.5", "next": "12.2.5",
...@@ -79,6 +82,7 @@ ...@@ -79,6 +82,7 @@
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@total-typescript/ts-reset": "^0.3.7", "@total-typescript/ts-reset": "^0.3.7",
"@types/csp-dev": "^1.0.0",
"@types/d3": "^7.4.0", "@types/d3": "^7.4.0",
"@types/dom-to-image": "^2.6.4", "@types/dom-to-image": "^2.6.4",
"@types/jest": "^29.2.0", "@types/jest": "^29.2.0",
......
...@@ -10,7 +10,7 @@ import SwaggerUI from 'ui/apiDocs/SwaggerUI'; ...@@ -10,7 +10,7 @@ import SwaggerUI from 'ui/apiDocs/SwaggerUI';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage: NextPage = () => { const APIDocsPage: NextPage = () => {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return ( return (
...@@ -22,7 +22,7 @@ const AppsPage: NextPage = () => { ...@@ -22,7 +22,7 @@ const AppsPage: NextPage = () => {
); );
}; };
export default AppsPage; export default APIDocsPage;
export const getServerSideProps: GetServerSideProps<Props> = async(args) => { export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.apiDoc.specUrl) { 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 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 = () => { import ContentLoader from 'ui/shared/ContentLoader';
return null; 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() { export { getServerSideProps } from 'lib/next/getServerSideProps';
return {
notFound: true,
};
}
...@@ -28,7 +28,7 @@ import TestApp from 'playwright/TestApp'; ...@@ -28,7 +28,7 @@ import TestApp from 'playwright/TestApp';
test('disabled', async({ mount }) => { test('disabled', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Button variant={ variant } colorScheme={ colorScheme } disabled>Click me</Button> <Button variant={ variant } colorScheme={ colorScheme } isDisabled>Click me</Button>
</TestApp>, </TestApp>,
); );
await expect(component.locator('button')).toHaveScreenshot(); await expect(component.locator('button')).toHaveScreenshot();
......
...@@ -2,5 +2,5 @@ export type IndexingStatus = { ...@@ -2,5 +2,5 @@ export type IndexingStatus = {
finished_indexing: boolean; finished_indexing: boolean;
finished_indexing_blocks: boolean; finished_indexing_blocks: boolean;
indexed_blocks_ratio: string; indexed_blocks_ratio: string;
indexed_inernal_transactions_ratio: string; indexed_internal_transactions_ratio: string;
} }
...@@ -15,6 +15,7 @@ declare module "nextjs-routes" { ...@@ -15,6 +15,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }> | DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }> | DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf"> | StaticRoute<"/api/csrf">
| StaticRoute<"/api/media-type">
| StaticRoute<"/api/proxy"> | StaticRoute<"/api/proxy">
| StaticRoute<"/api-docs"> | StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }> | DynamicRoute<"/apps/[id]", { "id": string }>
......
...@@ -50,7 +50,7 @@ const AddressTokens = () => { ...@@ -50,7 +50,7 @@ const AddressTokens = () => {
return ( return (
<> <>
<TokenBalances/> <TokenBalances/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
......
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
const SwaggerUIReact = dynamic(() => import('swagger-ui-react'), { const SwaggerUIReact = dynamic(() => import('swagger-ui-react'), {
loading: () => <Spinner/>, loading: () => <ContentLoader/>,
ssr: false, ssr: false,
}); });
import { Box, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Box, useColorModeValue } from '@chakra-ui/react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import ContentLoader from 'ui/shared/ContentLoader';
import 'swagger-ui-react/swagger-ui.css'; import 'swagger-ui-react/swagger-ui.css';
......
...@@ -101,7 +101,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -101,7 +101,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<FormControl variant="floating" id="address"> <FormControl variant="floating" id="address">
<Input <Input
{ ...field } { ...field }
disabled={ true } isDisabled={ true }
/> />
<FormLabel data-in-modal="true">Auto-generated API key token</FormLabel> <FormLabel data-in-modal="true">Auto-generated API key token</FormLabel>
</FormControl> </FormControl>
...@@ -147,7 +147,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -147,7 +147,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isDirty } isDisabled={ !isDirty }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Generate API key' } { data ? 'Save' : 'Generate API key' }
......
...@@ -174,7 +174,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -174,7 +174,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isDirty } isDisabled={ !isDirty }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Create custom ABI' } { 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 }) => { ...@@ -52,12 +52,12 @@ const IndexingAlert = ({ className }: { className?: string }) => {
handler: handleBlocksIndexStatus, 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) => { queryClient.setQueryData(getResourceKey('homepage_indexing_status'), (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus; const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing = payload.finished; newData.finished_indexing = payload.finished;
newData.indexed_inernal_transactions_ratio = payload.ratio; newData.indexed_internal_transactions_ratio = payload.ratio;
return newData; return newData;
}); });
...@@ -71,7 +71,7 @@ const IndexingAlert = ({ className }: { className?: string }) => { ...@@ -71,7 +71,7 @@ const IndexingAlert = ({ className }: { className?: string }) => {
useSocketMessage({ useSocketMessage({
channel: internalTxsIndexingChannel, channel: internalTxsIndexingChannel,
event: 'internal_txs_index_status', event: 'internal_txs_index_status',
handler: handleIntermalTxsIndexStatus, handler: handleInternalTxsIndexStatus,
}); });
if (isError) { if (isError) {
...@@ -87,8 +87,8 @@ const IndexingAlert = ({ className }: { className?: string }) => { ...@@ -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 } ` } 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.` ; We're indexing this chain right now. Some of the counts may be inaccurate.` ;
} else if (data.finished_indexing === false) { } else if (data.finished_indexing === false) {
content = `${ data.indexed_inernal_transactions_ratio && content = `${ data.indexed_internal_transactions_ratio &&
`${ Math.floor(Number(data.indexed_inernal_transactions_ratio) * 100) }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` } `${ 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.`; We're indexing this chain right now. Some of the counts may be inaccurate.`;
} }
......
...@@ -112,7 +112,7 @@ const AddressPageContent = () => { ...@@ -112,7 +112,7 @@ const AddressPageContent = () => {
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <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> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : content } { addressQuery.isLoading ? <SkeletonTabs/> : content }
{ !addressQuery.isLoading && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> } { !addressQuery.isLoading && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
......
...@@ -113,7 +113,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -113,7 +113,7 @@ const ApiKeysPage: React.FC = () => {
<Button <Button
size="lg" size="lg"
onClick={ apiKeyModalProps.onOpen } onClick={ apiKeyModalProps.onOpen }
disabled={ !canAdd } isDisabled={ !canAdd }
> >
Add API key Add API key
</Button> </Button>
......
...@@ -178,7 +178,7 @@ const TokenPageContent = () => { ...@@ -178,7 +178,7 @@ const TokenPageContent = () => {
) } ) }
<TokenContractInfo tokenQuery={ tokenQuery }/> <TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails 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> <Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : ( { tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : (
......
...@@ -124,7 +124,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -124,7 +124,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isDirty } isDisabled={ !isDirty }
isLoading={ pending } isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
......
...@@ -123,7 +123,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -123,7 +123,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isDirty } isDisabled={ !isDirty }
isLoading={ pending } isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
......
...@@ -236,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -236,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isDirty } isDisabled={ !isDirty }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
Send request Send request
......
...@@ -72,7 +72,7 @@ const DeleteModal: React.FC<Props> = ({ ...@@ -72,7 +72,7 @@ const DeleteModal: React.FC<Props> = ({
onClick={ onDeleteClick } onClick={ onDeleteClick }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
// FIXME: chackra's button is disabled when isLoading // FIXME: chackra's button is disabled when isLoading
disabled={ false } isDisabled={ false }
> >
Delete Delete
</Button> </Button>
......
...@@ -25,7 +25,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -25,7 +25,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
variant="outline" variant="outline"
size="sm" size="sm"
onClick={ resetPage } onClick={ resetPage }
disabled={ page === 1 } isDisabled={ page === 1 }
mr={ 4 } mr={ 4 }
> >
First First
...@@ -38,7 +38,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -38,7 +38,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
w="36px" w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 3 } mr={ 3 }
disabled={ !canGoBackwards || page === 1 } isDisabled={ !canGoBackwards || page === 1 }
/> />
<Button <Button
variant="outline" variant="outline"
...@@ -59,7 +59,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -59,7 +59,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
w="36px" w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 3 } ml={ 3 }
disabled={ !hasNextPage } isDisabled={ !hasNextPage }
/> />
{ /* not implemented yet */ } { /* not implemented yet */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}> { /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
......
...@@ -32,7 +32,7 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is ...@@ -32,7 +32,7 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
variant="subtle" variant="subtle"
colorScheme="gray" colorScheme="gray"
onClick={ handelPrevClick } onClick={ handelPrevClick }
disabled={ isPrevDisabled } isDisabled={ isPrevDisabled }
/> />
</Tooltip> </Tooltip>
<Tooltip label={ nextLabel }> <Tooltip label={ nextLabel }>
...@@ -45,7 +45,7 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is ...@@ -45,7 +45,7 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
colorScheme="gray" colorScheme="gray"
ml="10px" ml="10px"
onClick={ handelNextClick } onClick={ handelNextClick }
disabled={ isNextDisabled } isDisabled={ isNextDisabled }
/> />
</Tooltip> </Tooltip>
</Box> </Box>
......
...@@ -63,6 +63,7 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => { ...@@ -63,6 +63,7 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> } fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
onError={ handleLoadError } onError={ handleLoadError }
onLoad={ handleLoad } onLoad={ handleLoad }
loading={ url ? 'lazy' : undefined }
/> />
</AspectRatio> </AspectRatio>
); );
......
import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react'; import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import NftImage from './NftImage'; import NftImage from './NftImage';
import NftVideo from './NftVideo'; import NftVideo from './NftVideo';
import type { MediaType } from './utils';
import { getPreliminaryMediaType } from './utils';
interface Props { interface Props {
imageUrl: string | null; imageUrl: string | null;
...@@ -11,7 +14,7 @@ interface Props { ...@@ -11,7 +14,7 @@ interface Props {
} }
const NftMedia = ({ imageUrl, animationUrl, className }: 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(() => { React.useEffect(() => {
if (!animationUrl) { if (!animationUrl) {
...@@ -20,10 +23,26 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => { ...@@ -20,10 +23,26 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
// media could be either gif or video // media could be either gif or video
// so we pre-fetch the resources in order to get its content type // so we pre-fetch the resources in order to get its content type
fetch(animationUrl, { method: 'HEAD' }) // have to do it via Node.js due to strict CSP for connect-src
.then((response) => { // but in order not to abuse our server firstly we check file url extension
const contentType = response.headers.get('content-type'); // and if it is valid we will trust it and display corresponding media component
setType(contentType?.startsWith('video') ? 'video' : 'image');
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 ]); }, [ 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'; ...@@ -3,6 +3,7 @@ import NextLink from 'next/link';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import type { NavItem } from 'lib/hooks/useNavItems'; import type { NavItem } from 'lib/hooks/useNavItems';
import { isInternalItem } from 'lib/hooks/useNavItems'; import { isInternalItem } from 'lib/hooks/useNavItems';
...@@ -17,6 +18,7 @@ type Props = { ...@@ -17,6 +18,7 @@ type Props = {
} }
const NavLink = ({ item, isCollapsed, px, className }: Props) => { const NavLink = ({ item, isCollapsed, px, className }: Props) => {
const isMobile = useIsMobile();
const colors = useColors(); const colors = useColors();
const isExpanded = isCollapsed === false; const isExpanded = isCollapsed === false;
...@@ -45,7 +47,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => { ...@@ -45,7 +47,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => {
<Tooltip <Tooltip
label={ item.text } label={ item.text }
hasArrow={ false } hasArrow={ false }
isDisabled={ isCollapsed === false || (isCollapsed === undefined && isXLScreen) } isDisabled={ isMobile || isCollapsed === false || (isCollapsed === undefined && isXLScreen) }
placement="right" placement="right"
variant="nav" variant="nav"
gutter={ 20 } gutter={ 20 }
......
...@@ -94,7 +94,7 @@ const Tokens = () => { ...@@ -94,7 +94,7 @@ const Tokens = () => {
} }
if (!data.items.length) { if (!data.items.length) {
if (debouncedFilter) { if (debouncedFilter || type) {
return ( return (
<> <>
{ bar } { bar }
......
...@@ -192,7 +192,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd ...@@ -192,7 +192,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
size="lg" size="lg"
type="submit" type="submit"
isLoading={ pending } isLoading={ pending }
disabled={ !isDirty } isDisabled={ !isDirty }
> >
{ !isAdd ? 'Save changes' : 'Add address' } { !isAdd ? 'Save changes' : 'Add address' }
</Button> </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