Commit 52f5f6c9 authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into marketplace-improvements

parents c0429dbe 47425b88
...@@ -2,6 +2,8 @@ const RESTRICTED_MODULES = { ...@@ -2,6 +2,8 @@ const RESTRICTED_MODULES = {
paths: [ paths: [
{ name: 'dayjs', message: 'Please use lib/date/dayjs.ts instead of directly importing dayjs' }, { name: 'dayjs', message: 'Please use lib/date/dayjs.ts instead of directly importing dayjs' },
{ name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' }, { name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
{ name: '@metamask/providers', message: 'Please lazy-load @metamask/providers or use useProvider hook instead' },
{ name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' },
], ],
}; };
...@@ -307,7 +309,15 @@ module.exports = { ...@@ -307,7 +309,15 @@ module.exports = {
}, },
}, },
{ {
files: [ '*.config.ts', '*.config.js', 'playwright/**', 'deploy/tools/**', 'middleware.ts', 'nextjs/**' ], files: [
'*.config.ts',
'*.config.js',
'playwright/**',
'deploy/tools/**',
'middleware.ts',
'nextjs/**',
'instrumentation*.ts',
],
rules: { rules: {
// for configs allow to consume env variables from process.env directly // for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ], 'no-restricted-properties': [ 0 ],
......
...@@ -40,10 +40,9 @@ jobs: ...@@ -40,10 +40,9 @@ jobs:
run: yarn build run: yarn build
env: env:
NODE_ENV: production NODE_ENV: production
GENERATE_SOURCEMAPS: true
- name: Inject Sentry debug ID - name: Inject Sentry debug ID
run: yarn sentry-cli sourcemaps inject ./.next run: yarn sentry-cli sourcemaps inject ./.next
- name: Upload source maps to Sentry - name: Upload source maps to Sentry
run: yarn sentry-cli sourcemaps upload --release=${{ github.ref_name }} --validate ./.next run: yarn sentry-cli sourcemaps upload --release=${{ github.ref_name }} --url-prefix=~/_next/ --validate ./.next
\ No newline at end of file \ No newline at end of file
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
/out/ /out/
/public/assets/ /public/assets/
/public/envs.js /public/envs.js
/analyze
# production # production
/build /build
......
...@@ -27,6 +27,7 @@ const hiddenViews = (() => { ...@@ -27,6 +27,7 @@ const hiddenViews = (() => {
const config = Object.freeze({ const config = Object.freeze({
identiconType, identiconType,
hiddenViews, hiddenViews,
solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
}); });
export default config; export default config;
...@@ -47,6 +47,7 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com ...@@ -47,6 +47,7 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
#meta #meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
...@@ -392,6 +392,7 @@ const schema = yup ...@@ -392,6 +392,7 @@ const schema = yup
.transform(replaceQuotes) .transform(replaceQuotes)
.json() .json()
.of(yup.string<AddressViewId>().oneOf(ADDRESS_VIEWS_IDS)), .of(yup.string<AddressViewId>().oneOf(ADDRESS_VIEWS_IDS)),
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: yup.boolean(),
NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: yup NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: yup
.array() .array()
.transform(replaceQuotes) .transform(replaceQuotes)
......
...@@ -51,8 +51,8 @@ frontend: ...@@ -51,8 +51,8 @@ frontend:
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg
NEXT_PUBLIC_API_HOST: blockscout-main.k8s-dev.blockscout.com NEXT_PUBLIC_API_HOST: eth-goerli.blockscout.com
NEXT_PUBLIC_STATS_API_HOST: https://stats-test.k8s-dev.blockscout.com/ NEXT_PUBLIC_STATS_API_HOST: https://stats-goerli.k8s-dev.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
...@@ -67,10 +67,14 @@ frontend: ...@@ -67,10 +67,14 @@ frontend:
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: "['top_accounts']" NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: "['top_accounts']"
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: true
NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: "['value','fee_currency','gas_price','gas_fees','burnt_fees']" NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: "['value','fee_currency','gas_price','gas_fees','burnt_fees']"
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']" NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']"
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
OTEL_SDK_ENABLED: true
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger-collector.jaeger.svc.cluster.local:4318
NEXT_OTEL_VERBOSE: 1
envFromSecret: envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
......
...@@ -44,15 +44,21 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or ...@@ -44,15 +44,21 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or
## Local development ## Local development
1. Prepare your environment variables: To develop locally, follow one of the two paths outlined below:
- clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secrets for the [external services](./ENVS.md#external-services-configuration) integration; you can pick up only those that your needed
- choose one of the following options: A. Custom configuration:
A. create `.env.local` file in the root folder with environment variables from the [list](./ENVS.md); all required variables should be present in the file;
B. pick up one of the predefined configurations located at `/configs/envs` folder; no actual action is needed at this stage; 1. Create `.env.local` file in the root folder and include all required environment variables from the [list](./ENVS.md)
2. Run your local dev server: 2. Optionally, clone `.env.example` and name it `.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need.
- if you picked up option "A" above, use `yarn dev` command 3. Use `yarn dev` command to start the dev server.
- if your options is "B", use `yarn dev:<config_name>` command 4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`).
3. In browser navigate to the URL from the command output (by default, it is `http://localhost:3000`)
B. Pre-defined configuration:
1. Optionally, clone `.env.example` file into `configs/envs/.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need.
2. Choose one of the predefined configurations located in the `/configs/envs` folder.
3. Start your local dev server using the `yarn dev:<config_name>` command.
4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`).
&nbsp; &nbsp;
......
...@@ -49,6 +49,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -49,6 +49,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [SUAVE chain](ENVS.md#suave-chain) - [SUAVE chain](ENVS.md#suave-chain)
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring) - [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp; &nbsp;
...@@ -196,6 +197,7 @@ Settings for meta tags and OG tags ...@@ -196,6 +197,7 @@ Settings for meta tags and OG tags
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | | NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` |
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | | NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` |
| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supperted | - | - | `true` |
##### Address views list ##### Address views list
| Id | Description | | Id | Description |
...@@ -536,6 +538,16 @@ For blockchains that implementing SUAVE architecture additional fields will be s ...@@ -536,6 +538,16 @@ For blockchains that implementing SUAVE architecture additional fields will be s
&nbsp; &nbsp;
### OpenTelemetry
OpenTelemetry SDK for Node.js app could be enabled by passing `OTEL_SDK_ENABLED=true` variable. Configure the OpenTelemetry Protocol Exporter by using the generic environment variables described in the [OT docs](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options).
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| OTEL_SDK_ENABLED | `boolean` | Flag to enable the feature | Required | `false` | `true` |
&nbsp;
## External services configuration ## External services configuration
### Google ReCaptcha ### Google ReCaptcha
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="currentColor" fill-opacity=".8" fill-rule="evenodd" d="M9.767 2.074a.7.7 0 0 1 .626 0l7.38 3.69a.7.7 0 0 1 0 1.252l-7.38 3.69a.7.7 0 0 1-.626 0l-7.38-3.69a.7.7 0 0 1 0-1.252l7.38-3.69ZM4.266 6.39l5.814 2.907 5.815-2.907-5.815-2.907L4.266 6.39Zm-2.192 7.067a.7.7 0 0 1 .94-.313l7.066 3.534 7.067-3.534a.7.7 0 0 1 .627 1.252l-7.38 3.69a.7.7 0 0 1-.627 0l-7.38-3.69a.7.7 0 0 1-.313-.939Zm.94-4.003a.7.7 0 0 0-.627 1.252l7.38 3.69a.7.7 0 0 0 .626 0l7.38-3.69a.7.7 0 1 0-.626-1.252l-7.067 3.534-7.067-3.534Z" clip-rule="evenodd"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 3.6v4.812c0 6.447 5.414 8.703 6.79 9.17 1.377-.467 6.791-2.723 6.791-9.17V3.6H3ZM1.506 2.091A1.714 1.714 0 0 1 2.708 1.6h14.165c.448 0 .88.175 1.202.491.322.317.506.75.506 1.206v5.115c0 8.015-6.912 10.66-8.246 11.097a1.647 1.647 0 0 1-1.089 0C7.912 19.072 1 16.427 1 8.412V3.297c0-.455.184-.889.506-1.206Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.9 5.4a1.1 1.1 0 0 0-1.1 1.1v3.8a1.1 1.1 0 0 0 2.2 0V6.5a1.1 1.1 0 0 0-1.1-1.1Zm1.1 8.343a1.1 1.1 0 1 1-2.2 0 1.1 1.1 0 0 1 2.2 0Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 8.412V3.6h13.581v4.812c0 6.447-5.414 8.703-6.79 9.17C8.414 17.115 3 14.86 3 8.412ZM2.708 1.6c-.448 0-.88.175-1.202.491-.322.317-.506.75-.506 1.206v5.115c0 8.015 6.912 10.66 8.246 11.097.352.123.737.123 1.089 0 1.334-.437 8.246-3.082 8.246-11.097V3.297c0-.455-.184-.889-.506-1.206a1.714 1.714 0 0 0-1.202-.491H2.708ZM14.37 8.208a1 1 0 1 0-1.369-1.458L8.49 10.986 6.58 9.191a1 1 0 1 0-1.37 1.457l2.594 2.44a1 1 0 0 0 1.37 0l5.196-4.88Z" fill="currentColor"/>
</svg>
This diff is collapsed.
/* eslint-disable no-console */
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import {
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
const traceExporter = new OTLPTraceExporter();
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'blockscout_frontend',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || 'unknown_version',
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]:
process.env.NEXT_PUBLIC_APP_INSTANCE ||
process.env.NEXT_PUBLIC_APP_HOST?.replace('.blockscout.com', '').replaceAll('-', '_') ||
'unknown_app',
}),
spanProcessor: new SimpleSpanProcessor(traceExporter),
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter:
process.env.NODE_ENV === 'production' ?
new OTLPMetricExporter() :
new ConsoleMetricExporter(),
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (request) => {
try {
if (!request.url) {
return false;
}
const url = new URL(request.url, `http://${ request.headers.host }`);
if (
url.pathname.startsWith('/_next/static/') ||
url.pathname.startsWith('/_next/data/') ||
url.pathname.startsWith('/assets/') ||
url.pathname.startsWith('/static/') ||
url.pathname.startsWith('/favicon/') ||
url.pathname.startsWith('/envs.js')
) {
return true;
}
} catch (error) {}
return false;
},
},
}),
],
});
if (process.env.OTEL_SDK_ENABLED) {
sdk.start();
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
}
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation.node');
}
}
...@@ -26,12 +26,15 @@ import type { ...@@ -26,12 +26,15 @@ import type {
AddressTokensFilter, AddressTokensFilter,
AddressTokensResponse, AddressTokensResponse,
AddressWithdrawalsResponse, AddressWithdrawalsResponse,
AddressNFTsResponse,
AddressCollectionsResponse,
AddressNFTTokensFilter,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { BackendVersionConfig } from 'types/api/configs'; import type { BackendVersionConfig } from 'types/api/configs';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
...@@ -51,6 +54,7 @@ import type { ...@@ -51,6 +54,7 @@ import type {
TokenInstance, TokenInstance,
TokenInstanceTransfersCount, TokenInstanceTransfersCount,
TokenVerifiedInfo, TokenVerifiedInfo,
TokenInventoryFilters,
} from 'types/api/token'; } from 'types/api/token';
import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens'; import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
...@@ -77,16 +81,16 @@ export const SORTING_FIELDS = [ 'sort', 'order' ]; ...@@ -77,16 +81,16 @@ export const SORTING_FIELDS = [ 'sort', 'order' ];
export const RESOURCES = { export const RESOURCES = {
// ACCOUNT // ACCOUNT
csrf: { csrf: {
path: '/api/account/v1/get_csrf', path: '/api/account/v2/get_csrf',
}, },
user_info: { user_info: {
path: '/api/account/v1/user/info', path: '/api/account/v2/user/info',
}, },
email_resend: { email_resend: {
path: '/api/account/v1/email/resend', path: '/api/account/v2/email/resend',
}, },
custom_abi: { custom_abi: {
path: '/api/account/v1/user/custom_abis/:id?', path: '/api/account/v2/user/custom_abis/:id?',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
}, },
watchlist: { watchlist: {
...@@ -95,7 +99,7 @@ export const RESOURCES = { ...@@ -95,7 +99,7 @@ export const RESOURCES = {
filterFields: [ ], filterFields: [ ],
}, },
public_tags: { public_tags: {
path: '/api/account/v1/user/public_tags/:id?', path: '/api/account/v2/user/public_tags/:id?',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
}, },
private_tags_address: { private_tags_address: {
...@@ -109,7 +113,7 @@ export const RESOURCES = { ...@@ -109,7 +113,7 @@ export const RESOURCES = {
filterFields: [ ], filterFields: [ ],
}, },
api_keys: { api_keys: {
path: '/api/account/v1/user/api_keys/:id?', path: '/api/account/v2/user/api_keys/:id?',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
}, },
...@@ -305,6 +309,16 @@ export const RESOURCES = { ...@@ -305,6 +309,16 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
address_nfts: {
path: '/api/v2/addresses/:hash/nft',
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
},
address_collections: {
path: '/api/v2/addresses/:hash/nft/collections',
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
},
address_withdrawals: { address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals', path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
...@@ -343,6 +357,10 @@ export const RESOURCES = { ...@@ -343,6 +357,10 @@ export const RESOURCES = {
path: '/api/v2/smart-contracts/:hash/verification/via/:method', path: '/api/v2/smart-contracts/:hash/verification/via/:method',
pathParams: [ 'hash' as const, 'method' as const ], pathParams: [ 'hash' as const, 'method' as const ],
}, },
contract_solidityscan_report: {
path: '/api/v2/smart-contracts/:hash/solidityscan-report',
pathParams: [ 'hash' as const ],
},
verified_contracts: { verified_contracts: {
path: '/api/v2/smart-contracts', path: '/api/v2/smart-contracts',
...@@ -380,7 +398,7 @@ export const RESOURCES = { ...@@ -380,7 +398,7 @@ export const RESOURCES = {
token_inventory: { token_inventory: {
path: '/api/v2/tokens/:hash/instances', path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [], filterFields: [ 'holder_address_hash' as const ],
}, },
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
...@@ -576,7 +594,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -576,7 +594,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'addresses' | 'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
...@@ -638,6 +656,8 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : ...@@ -638,6 +656,8 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_nfts' ? AddressNFTsResponse :
Q extends 'address_collections' ? AddressCollectionsResponse :
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse : Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo : Q extends 'token_verified_info' ? TokenVerifiedInfo :
...@@ -659,6 +679,7 @@ Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : ...@@ -659,6 +679,7 @@ Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> : Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> : Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
Q extends 'contract_solidityscan_report' ? SolidityscanReport :
Q extends 'verified_contracts' ? VerifiedContractsResponse : Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters : Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? VisualizedContract : Q extends 'visualize_sol2uml' ? VisualizedContract :
...@@ -690,7 +711,10 @@ Q extends 'token_transfers' ? TokenTransferFilters : ...@@ -690,7 +711,10 @@ Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter : Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'address_nfts' ? AddressNFTTokensFilter :
Q extends 'address_collections' ? AddressNFTTokensFilter :
Q extends 'search' ? SearchResultFilters : Q extends 'search' ? SearchResultFilters :
Q extends 'token_inventory' ? TokenInventoryFilters :
Q extends 'tokens' ? TokensFilters : Q extends 'tokens' ? TokensFilters :
Q extends 'tokens_bridged' ? TokensBridgedFilters : Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters : Q extends 'verified_contracts' ? VerifiedContractsFilters :
......
...@@ -12,6 +12,7 @@ export enum NAMES { ...@@ -12,6 +12,7 @@ export enum NAMES {
INDEXING_ALERT='indexing_alert', INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected', ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug', MIXPANEL_DEBUG='_mixpanel_debug',
ADDRESS_NFT_DISPLAY_TYPE='address_nft_display_type'
} }
export function get(name?: NAMES | undefined | null, serverCookie?: string) { export function get(name?: NAMES | undefined | null, serverCookie?: string) {
......
import type * as Sentry from '@sentry/react'; import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app'; import appConfig from 'configs/app';
...@@ -10,11 +11,12 @@ export const config: Sentry.BrowserOptions | undefined = (() => { ...@@ -10,11 +11,12 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
} }
const tracesSampleRate: number | undefined = (() => { const tracesSampleRate: number | undefined = (() => {
if (feature.environment === 'staging') { switch (feature.environment) {
case 'development':
return 1; return 1;
} case 'staging':
return 0.75;
if (feature.environment === 'production' && feature.instance === 'eth') { case 'production':
return 0.2; return 0.2;
} }
})(); })();
...@@ -25,6 +27,7 @@ export const config: Sentry.BrowserOptions | undefined = (() => { ...@@ -25,6 +27,7 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
release: feature.release, release: feature.release,
enableTracing: feature.enableTracing, enableTracing: feature.enableTracing,
tracesSampleRate, tracesSampleRate,
integrations: feature.enableTracing ? [ new BrowserTracing() ] : undefined,
// error filtering settings // error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry // were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
......
import type { TokenType } from 'types/api/token'; import type { NFTTokenType, TokenType } from 'types/api/token';
export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [ export const NFT_TOKEN_TYPES: Array<{ title: string; id: NFTTokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' }, { title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' }, { title: 'ERC-1155', id: 'ERC-1155' },
]; ];
export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
...NFT_TOKEN_TYPES,
];
export const NFT_TOKEN_TYPE_IDS = NFT_TOKEN_TYPES.map(i => i.id);
export const TOKEN_TYPE_IDS = TOKEN_TYPES.map(i => i.id); export const TOKEN_TYPE_IDS = TOKEN_TYPES.map(i => i.id);
import { WindowPostMessageStream } from '@metamask/post-message-stream';
import { initializeProvider } from '@metamask/providers';
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi'; import type { WindowProvider } from 'wagmi';
...@@ -15,13 +13,16 @@ export default function useProvider() { ...@@ -15,13 +13,16 @@ export default function useProvider() {
const [ provider, setProvider ] = React.useState<WindowProvider>(); const [ provider, setProvider ] = React.useState<WindowProvider>();
const [ wallet, setWallet ] = React.useState<WalletType>(); const [ wallet, setWallet ] = React.useState<WalletType>();
React.useEffect(() => { const initializeProvider = React.useMemo(() => async() => {
if (!feature.isEnabled) { if (!feature.isEnabled) {
return; return;
} }
if (!('ethereum' in window && window.ethereum)) { if (!('ethereum' in window && window.ethereum)) {
if (feature.wallets.includes('metamask') && window.navigator.userAgent.includes('Firefox')) { if (feature.wallets.includes('metamask') && window.navigator.userAgent.includes('Firefox')) {
const { WindowPostMessageStream } = (await import('@metamask/post-message-stream'));
const { initializeProvider } = (await import('@metamask/providers'));
// workaround for MetaMask in Firefox // workaround for MetaMask in Firefox
// Firefox blocks MetaMask injection script because of our CSP for 'script-src' // Firefox blocks MetaMask injection script because of our CSP for 'script-src'
// so we have to inject it manually while the issue is not fixed // so we have to inject it manually while the issue is not fixed
...@@ -73,5 +74,9 @@ export default function useProvider() { ...@@ -73,5 +74,9 @@ export default function useProvider() {
} }
}, []); }, []);
React.useEffect(() => {
initializeProvider();
}, [ initializeProvider ]);
return { provider, wallet }; return { provider, wallet };
} }
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressCollectionsResponse, AddressNFTsResponse, AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo'; import * as tokens from 'mocks/tokens/tokenInfo';
import * as tokenInstance from 'mocks/tokens/tokenInstance'; import * as tokenInstance from 'mocks/tokens/tokenInstance';
...@@ -117,3 +117,51 @@ export const erc1155List = { ...@@ -117,3 +117,51 @@ export const erc1155List = {
erc1155b, erc1155b,
], ],
}; };
export const nfts: AddressNFTsResponse = {
items: [
{
...tokenInstance.base,
token: tokens.tokenInfoERC1155a,
token_type: 'ERC-1155',
value: '11',
},
{
...tokenInstance.unique,
token: tokens.tokenInfoERC721a,
token_type: 'ERC-721',
value: '1',
},
],
next_page_params: null,
};
const nftInstance = {
...tokenInstance.base,
token_type: 'ERC-1155',
value: '11',
};
export const collections: AddressCollectionsResponse = {
items: [
{
token: tokens.tokenInfoERC1155a,
amount: '100',
token_instances: Array(5).fill(nftInstance),
},
{
token: tokens.tokenInfoERC20LongSymbol,
amount: '100',
token_instances: Array(5).fill(nftInstance),
},
{
token: tokens.tokenInfoERC1155WithoutName,
amount: '1',
token_instances: [ nftInstance ],
},
],
next_page_params: {
token_contract_address_hash: '123',
token_type: 'ERC-1155',
},
};
export const solidityscanReportAverage = {
scan_report: {
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
critical: 0,
gas: 1,
high: 0,
informational: 0,
low: 2,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '72.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
export const solidityscanReportGreat = {
scan_report: {
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
critical: 0,
gas: 0,
high: 0,
informational: 0,
low: 0,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '100',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
export const solidityscanReportLow = {
scan_report: {
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
critical: 2,
gas: 1,
high: 3,
informational: 0,
low: 2,
medium: 10,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '22.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
import * as addressMock from '../address/address'; import * as addressMock from '../address/address';
import { tokenInfoERC721a } from './tokenInfo';
export const base: TokenInstance = { export const base: TokenInstance = {
animation_url: null, animation_url: null,
...@@ -74,7 +73,6 @@ export const base: TokenInstance = { ...@@ -74,7 +73,6 @@ export const base: TokenInstance = {
name: 'GENESIS #188848, 22a5f8bbb1602995. Blockchain pixel PFP NFT + "on music video" trait inspired by God', name: 'GENESIS #188848, 22a5f8bbb1602995. Blockchain pixel PFP NFT + "on music video" trait inspired by God',
}, },
owner: addressMock.withName, owner: addressMock.withName,
token: tokenInfoERC721a,
}; };
export const withRichMetadata: TokenInstance = { export const withRichMetadata: TokenInstance = {
......
...@@ -25,7 +25,7 @@ export const erc20: TokenTransfer = { ...@@ -25,7 +25,7 @@ export const erc20: TokenTransfer = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
circulating_market_cap: '117629601.61913824', circulating_market_cap: '117629601.61913824',
decimals: '18', decimals: '18',
exchange_rate: null, exchange_rate: '42',
holders: '46554', holders: '46554',
name: 'ARIANEE', name: 'ARIANEE',
symbol: 'ARIA', symbol: 'ARIA',
......
...@@ -124,6 +124,7 @@ export const withTokenTransfer: Transaction = { ...@@ -124,6 +124,7 @@ export const withTokenTransfer: Transaction = {
tokenTransferMock.erc1155C, tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D, tokenTransferMock.erc1155D,
], ],
token_transfers_overflow: true,
tx_types: [ tx_types: [
'token_transfer', 'token_transfer',
], ],
......
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.BUNDLE_ANALYZER === 'true',
});
const withRoutes = require('nextjs-routes/config')({ const withRoutes = require('nextjs-routes/config')({
outDir: 'nextjs', outDir: 'nextjs',
}); });
...@@ -38,7 +42,10 @@ const moduleExports = { ...@@ -38,7 +42,10 @@ const moduleExports = {
redirects, redirects,
headers, headers,
output: 'standalone', output: 'standalone',
productionBrowserSourceMaps: process.env.GENERATE_SOURCEMAPS === 'true', productionBrowserSourceMaps: true,
experimental: {
instrumentationHook: true,
},
}; };
module.exports = withRoutes(moduleExports); module.exports = withBundleAnalyzer(withRoutes(moduleExports));
...@@ -16,7 +16,7 @@ export function googleAnalytics(): CspDev.DirectiveDescriptor { ...@@ -16,7 +16,7 @@ export function googleAnalytics(): CspDev.DirectiveDescriptor {
], ],
'script-src': [ 'script-src': [
// inline script hash, see ui/shared/GoogleAnalytics.tsx // inline script hash, see ui/shared/GoogleAnalytics.tsx
'\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'', '\'sha256-WXRwCtfSfMoCPzPUIOUAosSaADdGgct0/Lhmnbm7MCA=\'',
'https://www.googletagmanager.com', 'https://www.googletagmanager.com',
'*.google-analytics.com', '*.google-analytics.com',
'*.analytics.google.com', '*.analytics.google.com',
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"npm": "8" "npm": "8"
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "./tools/scripts/dev.sh",
"dev:preset": "./tools/scripts/dev.preset.sh", "dev:preset": "./tools/scripts/dev.preset.sh",
"build": "next build", "build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./", "build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./",
...@@ -37,8 +37,17 @@ ...@@ -37,8 +37,17 @@
"@metamask/post-message-stream": "^7.0.0", "@metamask/post-message-stream": "^7.0.0",
"@metamask/providers": "^10.2.1", "@metamask/providers": "^10.2.1",
"@monaco-editor/react": "^4.4.6", "@monaco-editor/react": "^4.4.6",
"@next/bundle-analyzer": "^14.0.1",
"@opentelemetry/auto-instrumentations-node": "^0.39.4",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.45.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.45.0",
"@opentelemetry/resources": "^1.18.0",
"@opentelemetry/sdk-node": "^0.45.0",
"@opentelemetry/sdk-trace-node": "^1.18.0",
"@opentelemetry/semantic-conventions": "^1.18.0",
"@sentry/cli": "^2.21.2", "@sentry/cli": "^2.21.2",
"@sentry/react": "^7.72.0", "@sentry/react": "7.24.0",
"@sentry/tracing": "7.24.0",
"@slise/embed-react": "^2.2.0", "@slise/embed-react": "^2.2.0",
"@tanstack/react-query": "^5.4.3", "@tanstack/react-query": "^5.4.3",
"@tanstack/react-query-devtools": "^5.4.3", "@tanstack/react-query-devtools": "^5.4.3",
...@@ -47,13 +56,13 @@ ...@@ -47,13 +56,13 @@
"@web3modal/ethereum": "^2.6.2", "@web3modal/ethereum": "^2.6.2",
"@web3modal/react": "^2.6.2", "@web3modal/react": "^2.6.2",
"bignumber.js": "^9.1.0", "bignumber.js": "^9.1.0",
"blo": "^1.1.1",
"chakra-react-select": "^4.4.3", "chakra-react-select": "^4.4.3",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"d3": "^7.6.1", "d3": "^7.6.1",
"dappscout-iframe": "^0.1.0", "dappscout-iframe": "^0.1.0",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"ethereum-blockies-base64": "^1.0.2",
"framer-motion": "^6.5.1", "framer-motion": "^6.5.1",
"gradient-avatar": "^1.0.2", "gradient-avatar": "^1.0.2",
"graphiql": "^2.2.0", "graphiql": "^2.2.0",
......
...@@ -22,7 +22,7 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi ...@@ -22,7 +22,7 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi
return 'video'; return 'video';
} }
if (contentType === 'text/html') { if (contentType?.startsWith('text/html')) {
return 'html'; return 'html';
} }
......
import type { Address, AddressCoinBalanceHistoryItem, AddressCounters, AddressTabsCounters, AddressTokenBalance } from 'types/api/address'; import type {
Address,
AddressCoinBalanceHistoryItem,
AddressCollection,
AddressCounters,
AddressNFT,
AddressTabsCounters,
AddressTokenBalance,
} from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses'; import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; import { ADDRESS_HASH } from './addressParams';
...@@ -80,16 +88,22 @@ export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = { ...@@ -80,16 +88,22 @@ export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = {
value: '1000000000000000000000000', value: '1000000000000000000000000',
}; };
export const ADDRESS_TOKEN_BALANCE_ERC_721: AddressTokenBalance = { export const ADDRESS_NFT_721: AddressNFT = {
token_type: 'ERC-721',
token: TOKEN_INFO_ERC_721, token: TOKEN_INFO_ERC_721,
token_id: null, value: '1',
token_instance: null, ...TOKEN_INSTANCE,
value: '176', };
export const ADDRESS_NFT_1155: AddressNFT = {
token_type: 'ERC-1155',
token: TOKEN_INFO_ERC_1155,
value: '10',
...TOKEN_INSTANCE,
}; };
export const ADDRESS_TOKEN_BALANCE_ERC_1155: AddressTokenBalance = { export const ADDRESS_COLLECTION: AddressCollection = {
token: TOKEN_INFO_ERC_1155, token: TOKEN_INFO_ERC_1155,
token_id: '188882', amount: '4',
token_instance: TOKEN_INSTANCE, token_instances: Array(4).fill(TOKEN_INSTANCE),
value: '176',
}; };
import type { SmartContract } from 'types/api/contract'; import type { SmartContract, SolidityscanReport } from 'types/api/contract';
import type { VerifiedContract } from 'types/api/contracts'; import type { VerifiedContract } from 'types/api/contracts';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS } from './addressParams';
...@@ -53,3 +53,25 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = { ...@@ -53,3 +53,25 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
tx_count: 565058, tx_count: 565058,
verified_at: '2023-04-10T13:16:33.884921Z', verified_at: '2023-04-10T13:16:33.884921Z',
}; };
export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
scan_report: {
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
critical: 0,
gas: 1,
high: 0,
informational: 0,
low: 2,
medium: 0,
},
lines_analyzed_count: 18,
scan_time_taken: 1,
score: '3.61',
score_v2: '72.22',
threat_score: '94.74',
},
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
...@@ -108,6 +108,5 @@ export const TOKEN_INSTANCE: TokenInstance = { ...@@ -108,6 +108,5 @@ export const TOKEN_INSTANCE: TokenInstance = {
name: 'GENESIS #188882, 8a77ca1bcaa4036f. Blockchain pixel PFP NFT + "on music video" trait inspired by God', name: 'GENESIS #188882, 8a77ca1bcaa4036f. Blockchain pixel PFP NFT + "on music video" trait inspired by God',
}, },
owner: ADDRESS_PARAMS, owner: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_1155,
holder_address_hash: ADDRESS_HASH, holder_address_hash: ADDRESS_HASH,
}; };
...@@ -14,11 +14,6 @@ if [ ! -f "$config_file" ]; then ...@@ -14,11 +14,6 @@ if [ ! -f "$config_file" ]; then
exit 1 exit 1
fi fi
if [ ! -f "$secrets_file" ]; then
echo "Error: File '$secrets_file' not found."
exit 1
fi
# download assets for the running instance # download assets for the running instance
dotenv \ dotenv \
-e $config_file \ -e $config_file \
......
#!/bin/bash
# download assets for the running instance
dotenv \
-e .env.development.local \
-e .env.local \
-e .env.development \
-e .env \
-- bash -c './deploy/scripts/download_assets.sh ./public/assets'
# generate envs.js file and run the app
dotenv \
-v NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) \
-v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \
-e .env.secrets \
-e .env.development.local \
-e .env.local \
-e .env.development \
-e .env \
-- bash -c './deploy/scripts/make_envs_script.sh && next dev -- -p $NEXT_PUBLIC_APP_PORT' |
pino-pretty
\ No newline at end of file
...@@ -16,6 +16,6 @@ ...@@ -16,6 +16,6 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.node.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"],
"exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator"], "exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator"],
} }
...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { UserTags } from './addressParams'; import type { UserTags } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenInstance, TokenType } from './token'; import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address extends UserTags { export interface Address extends UserTags {
...@@ -49,17 +49,47 @@ export interface AddressTokenBalance { ...@@ -49,17 +49,47 @@ export interface AddressTokenBalance {
token_instance: TokenInstance | null; token_instance: TokenInstance | null;
} }
export type AddressNFT = TokenInstance & {
token: TokenInfo;
token_type: Omit<TokenType, 'ERC-20'>;
value: string;
}
export type AddressCollection = {
token: TokenInfo;
amount: string;
token_instances: Array<Omit<AddressNFT, 'token'>>;
}
export interface AddressTokensResponse { export interface AddressTokensResponse {
items: Array<AddressTokenBalance>; items: Array<AddressTokenBalance>;
next_page_params: { next_page_params: {
items_count: number; items_count: number;
token_name: 'string' | null; token_name: string | null;
token_type: TokenType; token_type: TokenType;
value: number; value: number;
fiat_value: string | null; fiat_value: string | null;
} | null; } | null;
} }
export interface AddressNFTsResponse {
items: Array<AddressNFT>;
next_page_params: {
items_count: number;
token_id: string;
token_type: TokenType;
token_contract_address_hash: string;
} | null;
}
export interface AddressCollectionsResponse {
items: Array<AddressCollection>;
next_page_params: {
token_contract_address_hash: string;
token_type: TokenType;
} | null;
}
export interface AddressTokensBalancesSocketMessage { export interface AddressTokensBalancesSocketMessage {
overflow: boolean; overflow: boolean;
token_balances: Array<AddressTokenBalance>; token_balances: Array<AddressTokenBalance>;
...@@ -97,6 +127,10 @@ export type AddressTokensFilter = { ...@@ -97,6 +127,10 @@ export type AddressTokensFilter = {
type: TokenType; type: TokenType;
} }
export type AddressNFTTokensFilter = {
type: Array<NFTTokenType> | undefined;
}
export interface AddressCoinBalanceHistoryItem { export interface AddressCoinBalanceHistoryItem {
block_number: number; block_number: number;
block_timestamp: string; block_timestamp: string;
......
...@@ -156,3 +156,25 @@ export interface SmartContractVerificationError { ...@@ -156,3 +156,25 @@ export interface SmartContractVerificationError {
constructor_arguments?: Array<string>; constructor_arguments?: Array<string>;
name?: Array<string>; name?: Array<string>;
} }
export type SolidityscanReport = {
scan_report: {
scan_status: string;
scan_summary: {
issue_severity_distribution: {
critical: number;
gas: number;
high: number;
informational: number;
low: number;
medium: number;
};
lines_analyzed_count: number;
scan_time_taken: number;
score: string;
score_v2: string;
threat_score: string;
};
scanner_reference_url: string;
};
}
import type { TokenInfoApplication } from './account'; import type { TokenInfoApplication } from './account';
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155'; export type NFTTokenType = 'ERC-721' | 'ERC-1155';
export type TokenType = 'ERC-20' | NFTTokenType;
export interface TokenInfo<T extends TokenType = TokenType> { export interface TokenInfo<T extends TokenType = TokenType> {
address: string; address: string;
...@@ -61,7 +62,6 @@ export interface TokenInstance { ...@@ -61,7 +62,6 @@ export interface TokenInstance {
external_app_url: string | null; external_app_url: string | null;
metadata: Record<string, unknown> | null; metadata: Record<string, unknown> | null;
owner: AddressParam | null; owner: AddressParam | null;
token: TokenInfo;
} }
export interface TokenInstanceTransfersCount { export interface TokenInstanceTransfersCount {
...@@ -78,3 +78,7 @@ export type TokenInventoryPagination = { ...@@ -78,3 +78,7 @@ export type TokenInventoryPagination = {
} }
export type TokenVerifiedInfo = Omit<TokenInfoApplication, 'id' | 'status'>; export type TokenVerifiedInfo = Omit<TokenInfoApplication, 'id' | 'status'>;
export type TokenInventoryFilters = {
holder_address_hash?: string;
}
import { Flex, Hide, Icon, Show, Text, Tooltip, useColorModeValue } from '@chakra-ui/react'; import { Flex, Hide, Show, Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,7 +9,6 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap ...@@ -9,7 +9,6 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
...@@ -26,6 +25,7 @@ import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; ...@@ -26,6 +25,7 @@ import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import ResetIconButton from 'ui/shared/ResetIconButton';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
...@@ -115,9 +115,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -115,9 +115,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
onFilterChange({}); onFilterChange({});
}, [ onFilterChange ]); }, [ onFilterChange ]);
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
...@@ -235,19 +232,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -235,19 +232,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
<Flex alignItems="center" py={ 1 }> <Flex alignItems="center" py={ 1 }>
<TokenEntity.Icon token={ tokenData } isLoading={ isPlaceholderData }/> <TokenEntity.Icon token={ tokenData } isLoading={ isPlaceholderData }/>
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter } { isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<Tooltip label="Reset filter"> <ResetIconButton onClick={ resetTokenFilter }/>
<Flex>
<Icon
as={ crossIcon }
boxSize={ 5 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
</Flex> </Flex>
</Flex> </Flex>
); );
......
...@@ -13,6 +13,8 @@ import AddressTokens from './AddressTokens'; ...@@ -13,6 +13,8 @@ import AddressTokens from './AddressTokens';
const ADDRESS_HASH = addressMock.withName.hash; const ADDRESS_HASH = addressMock.withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }); const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const API_URL_NFT = buildApiUrl('address_nfts', { hash: ADDRESS_HASH }) + '?type=';
const API_URL_COLLECTIONS = buildApiUrl('address_collections', { hash: ADDRESS_HASH }) + '?type=';
const nextPageParams = { const nextPageParams = {
items_count: 50, items_count: 50,
...@@ -52,6 +54,14 @@ const test = base.extend({ ...@@ -52,6 +54,14 @@ const test = base.extend({
status: 200, status: 200,
body: JSON.stringify(response1155), body: JSON.stringify(response1155),
})); }));
await page.route(API_URL_NFT, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.nfts),
}));
await page.route(API_URL_COLLECTIONS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.collections),
}));
use(page); use(page);
}, },
...@@ -76,10 +86,10 @@ test('erc20 +@dark-mode', async({ mount }) => { ...@@ -76,10 +86,10 @@ test('erc20 +@dark-mode', async({ mount }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721 +@dark-mode', async({ mount }) => { test('collections +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
...@@ -95,10 +105,10 @@ test('erc721 +@dark-mode', async({ mount }) => { ...@@ -95,10 +105,10 @@ test('erc721 +@dark-mode', async({ mount }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155 +@dark-mode', async({ mount }) => { test('nfts +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
...@@ -111,6 +121,8 @@ test('erc1155 +@dark-mode', async({ mount }) => { ...@@ -111,6 +121,8 @@ test('erc1155 +@dark-mode', async({ mount }) => {
{ hooksConfig }, { hooksConfig },
); );
await component.getByText('List').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -136,10 +148,10 @@ test.describe('mobile', () => { ...@@ -136,10 +148,10 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721', async({ mount }) => { test('nfts', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
...@@ -152,13 +164,15 @@ test.describe('mobile', () => { ...@@ -152,13 +164,15 @@ test.describe('mobile', () => {
{ hooksConfig }, { hooksConfig },
); );
await component.getByLabel('list').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155', async({ mount }) => { test('collections', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
......
This diff is collapsed.
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as solidityscanReportMock from 'mocks/contract/solidityscanReport';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import SolidityscanReport from './SolidityscanReport';
const addressHash = 'hash';
const REPORT_API_URL = buildApiUrl('contract_solidityscan_report', { hash: addressHash });
test('average report +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(solidityscanReportMock.solidityscanReportAverage),
}));
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
await component.getByLabel('SolidityScan score').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
});
test('great report', async({ mount, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(solidityscanReportMock.solidityscanReportGreat),
}));
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
await component.getByLabel('SolidityScan score').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
});
test('low report', async({ mount, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(solidityscanReportMock.solidityscanReportLow),
}));
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
await component.getByLabel('SolidityScan score').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
});
import {
Box,
Flex,
Text,
Grid,
Button,
Icon,
chakra,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
useDisclosure,
Skeleton,
Center,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import { SolidityscanReport } from 'types/api/contract';
import scoreNotOkIcon from 'icons/score/score-not-ok.svg';
import scoreOkIcon from 'icons/score/score-ok.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import LinkExternal from 'ui/shared/LinkExternal';
type DistributionItem = {
id: keyof SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
name: string;
color: string;
}
const DISTRIBUTION_ITEMS: Array<DistributionItem> = [
{ id: 'critical', name: 'Critical', color: '#891F11' },
{ id: 'high', name: 'High', color: '#EC672C' },
{ id: 'medium', name: 'Medium', color: '#FBE74D' },
{ id: 'low', name: 'Low', color: '#68C88E' },
{ id: 'informational', name: 'Informational', color: '#A3AEBE' },
{ id: 'gas', name: 'Gas', color: '#A47585' },
];
interface Props {
className?: string;
hash: string;
}
const SolidityscanReport = ({ className, hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { data, isPlaceholderData, isError } = useApiQuery('contract_solidityscan_report', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: SOLIDITYSCAN_REPORT,
},
});
const score = Number(data?.scan_report.scan_summary.score_v2);
const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const chartGrayColor = useColorModeValue('gray.100', 'gray.700');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
const popoverBgColor = useColorModeValue('white', 'gray.900');
if (isError || !score) {
return null;
}
let scoreColor;
let scoreLevel;
if (score >= 80) {
scoreColor = 'green.600';
scoreLevel = 'GREAT';
} else if (score >= 30) {
scoreColor = 'orange.600';
scoreLevel = 'AVERAGE';
} else {
scoreColor = 'red.600';
scoreLevel = 'LOW';
}
const vulnerabilities = data?.scan_report.scan_summary.issue_severity_distribution;
const vulnerabilitiesCounts = vulnerabilities ? Object.values(vulnerabilities) : [];
const vulnerabilitiesCount = vulnerabilitiesCounts.reduce((acc, val) => acc + val, 0);
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Skeleton isLoaded={ !isPlaceholderData }>
<Button
className={ className }
color={ scoreColor }
borderColor={ scoreColor }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="SolidityScan score"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<Icon as={ score < 80 ? scoreNotOkIcon : scoreOkIcon } boxSize={ 5 } mr={ 1 }/>
{ score }
</Button>
</Skeleton>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
<PopoverBody px="26px" py="20px" fontSize="sm">
<Box mb={ 5 }>Contract analyzed for 140+ vulnerability patterns by SolidityScan</Box>
<Flex alignItems="center" mb={ 5 }>
<Box
w={ 12 }
h={ 12 }
bgGradient={ `conic-gradient(${ scoreColor } 0, ${ scoreColor } ${ score }%, ${ chartGrayColor } 0, ${ chartGrayColor } 100%)` }
borderRadius="24px"
position="relative"
mr={ 3 }
>
<Center position="absolute" w="38px" h="38px" top="5px" right="5px" bg={ popoverBgColor } borderRadius="20px">
<Icon as={ score < 80 ? scoreNotOkIcon : scoreOkIcon } boxSize={ 5 } color={ scoreColor }/>
</Center>
</Box>
<Box>
<Flex>
<Text color={ scoreColor } fontSize="lg" fontWeight={ 500 }>{ score }</Text>
<Text color={ yetAnotherGrayColor } fontSize="lg" fontWeight={ 500 } whiteSpace="pre"> / 100</Text>
</Flex>
<Text color={ scoreColor } fontWeight={ 500 }>Security score is { scoreLevel }</Text>
</Box>
</Flex>
{ vulnerabilities && vulnerabilitiesCount > 0 && (
<Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Vulnerabilities distribution</Text>
<Grid templateColumns="20px 1fr 100px" alignItems="center" rowGap={ 2 }>
{ DISTRIBUTION_ITEMS.map(item => (
<>
<Box w={ 3 } h={ 3 } bg={ item.color } borderRadius="6px" mr={ 2 }></Box>
<Flex justifyContent="space-between" mr={ 3 }>
<Text>{ item.name }</Text>
<Text color={ vulnerabilities[item.id] > 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] }</Text>
</Flex>
<Box bg={ bgBar } h="10px" borderRadius="8px">
<Box bg={ item.color } w={ vulnerabilities[item.id] / vulnerabilitiesCount } h="10px" borderRadius="8px"/>
</Box>
</>
)) }
</Grid>
</Box>
) }
<LinkExternal href={ data?.scan_report.scanner_reference_url }>View full report</LinkExternal>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default chakra(SolidityscanReport);
...@@ -9,7 +9,7 @@ export const getNativeCoinValue = (value: string | Array<unknown>) => { ...@@ -9,7 +9,7 @@ export const getNativeCoinValue = (value: string | Array<unknown>) => {
return BigInt(0); return BigInt(0);
} }
return BigInt(Number(_value)); return BigInt(_value);
}; };
export const addZeroesAllowed = (valueType: string) => { export const addZeroesAllowed = (valueType: string) => {
......
...@@ -2,10 +2,8 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,10 +2,8 @@ import { Flex } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokensMock from 'mocks/address/tokens'; import * as tokensMock from 'mocks/address/tokens';
import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo'; import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
...@@ -175,76 +173,3 @@ base('long values', async({ mount, page }) => { ...@@ -175,76 +173,3 @@ base('long values', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA }); await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
}); });
test.describe('socket', () => {
const testWithSocket = test.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
testWithSocket.describe.configure({ mode: 'serial' });
testWithSocket('new item after token balance update', async({ page, mount, createSocket }) => {
await mount(
<TestApp withSocket>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
...tokensMock.erc20List.items,
tokensMock.erc20d,
],
}),
}), { times: 1 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'token_balance', {
block_number: 1,
});
const button = page.getByRole('button', { name: /select/i });
const text = await button.innerText();
expect(text).toContain('10');
});
testWithSocket('new item after coin balance update', async({ page, mount, createSocket }) => {
await mount(
<TestApp withSocket>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
...tokensMock.erc20List.items,
tokensMock.erc20d,
],
}),
}), { times: 1 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'coin_balance', {
coin_balance: coinBalanceMock.base,
});
const button = page.getByRole('button', { name: /select/i });
const text = await button.innerText();
expect(text).toContain('10');
});
});
...@@ -5,7 +5,6 @@ import NextLink from 'next/link'; ...@@ -5,7 +5,6 @@ import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
...@@ -13,8 +12,6 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -13,8 +12,6 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import useFetchTokens from '../utils/useFetchTokens'; import useFetchTokens from '../utils/useFetchTokens';
import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectDesktop from './TokenSelectDesktop';
...@@ -28,14 +25,13 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -28,14 +25,13 @@ const TokenSelect = ({ onClick }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressHash = getQueryParamString(router.query.hash); const addressHash = getQueryParamString(router.query.hash);
const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } }); const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey); const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isPending, refetch } = useFetchTokens({ hash: addressQueryData?.hash }); const { data, isError, isPending } = useFetchTokens({ hash: addressQueryData?.hash });
const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } }); const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey }); const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
...@@ -44,34 +40,6 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -44,34 +40,6 @@ const TokenSelect = ({ onClick }: Props) => {
onClick?.(); onClick?.();
}, [ onClick ]); }, [ onClick ]);
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
refetch();
setBlockNumber(payload.block_number);
}
}, [ blockNumber, refetch ]);
const handleCoinBalanceMessage: SocketMessage.AddressCoinBalance['handler'] = React.useCallback((payload) => {
if (payload.coin_balance.block_number !== blockNumber) {
refetch();
setBlockNumber(payload.coin_balance.block_number);
}
}, [ blockNumber, refetch ]);
const channel = useSocketChannel({
topic: `addresses:${ addressQueryData?.hash.toLowerCase() }`,
isDisabled: !addressQueryData,
});
useSocketMessage({
channel,
event: 'coin_balance',
handler: handleCoinBalanceMessage,
});
useSocketMessage({
channel,
event: 'token_balance',
handler: handleTokenBalanceMessage,
});
if (isPending) { if (isPending) {
return ( return (
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
......
import { Box, Flex, Text, Grid, HStack, Skeleton } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import NftFallback from 'ui/shared/nft/NftFallback';
import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import NFTItem from './NFTItem';
import NFTItemContainer from './NFTItemContainer';
type Props = {
collectionsQuery: QueryWithPagesResult<'address_collections'>;
address: string;
hasActiveFilters: boolean;
}
const AddressCollections = ({ collectionsQuery, address, hasActiveFilters }: Props) => {
const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination } = collectionsQuery;
const actionBar = isMobile && pagination.isVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
const content = data?.items ? data?.items.map((item, index) => {
const collectionUrl = route({
pathname: '/token/[hash]',
query: {
hash: item.token.address,
tab: 'inventory',
holder_address_hash: address,
scroll_to_tabs: 'true',
},
});
const hasOverload = Number(item.amount) > item.token_instances.length;
return (
<Box key={ item.token.address + index } mb={ 6 }>
<Flex mb={ 3 } flexWrap="wrap" lineHeight="30px">
<TokenEntity
width="auto"
noSymbol
token={ item.token }
isLoading={ isPlaceholderData }
noCopy
fontWeight="600"
/>
<Skeleton isLoaded={ !isPlaceholderData } mr={ 3 }>
<Text variant="secondary" whiteSpace="pre">{ ` - ${ Number(item.amount).toLocaleString() } item${ Number(item.amount) > 1 ? 's' : '' }` }</Text>
</Skeleton>
<LinkInternal href={ collectionUrl } isLoading={ isPlaceholderData }>
<Skeleton isLoaded={ !isPlaceholderData }>View in collection</Skeleton>
</LinkInternal>
</Flex>
<Grid
w="100%"
mb={ 7 }
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ item.token_instances.map((instance, index) => {
const key = item.token.address + '_' + (instance.id && !isPlaceholderData ? `id_${ instance.id }` : `index_${ index }`);
return (
<NFTItem
key={ key }
{ ...instance }
token={ item.token }
isLoading={ isPlaceholderData }
/>
);
}) }
{ hasOverload && (
<LinkInternal display="flex" href={ collectionUrl }>
<NFTItemContainer display="flex" alignItems="center" justifyContent="center" flexDirection="column" minH="248px">
<HStack gap={ 2 } mb={ 3 }>
<NftFallback bgColor="unset" w="30px" h="30px" boxSize="30px" p={ 0 }/>
<NftFallback bgColor="unset" w="30px" h="30px" boxSize="30px" p={ 0 }/>
<NftFallback bgColor="unset" w="30px" h="30px" boxSize="30px" p={ 0 }/>
</HStack>
View all NFTs
</NFTItemContainer>
</LinkInternal>
) }
</Grid>
</Box>
);
}) : null;
return (
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any token that matches your query.`,
hasActiveFilters,
}}
/>
);
};
export default AddressCollections;
...@@ -2,6 +2,7 @@ import { Grid } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
...@@ -10,10 +11,11 @@ import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPage ...@@ -10,10 +11,11 @@ import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPage
import NFTItem from './NFTItem'; import NFTItem from './NFTItem';
type Props = { type Props = {
tokensQuery: QueryWithPagesResult<'address_tokens'>; tokensQuery: QueryWithPagesResult<'address_nfts'>;
hasActiveFilters: boolean;
} }
const ERC1155Tokens = ({ tokensQuery }: Props) => { const AddressNFTs = ({ tokensQuery, hasActiveFilters }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination } = tokensQuery; const { isError, isPlaceholderData, data, pagination } = tokensQuery;
...@@ -32,13 +34,14 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -32,13 +34,14 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }} gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
> >
{ data.items.map((item, index) => { { data.items.map((item, index) => {
const key = item.token.address + '_' + (item.token_instance?.id && !isPlaceholderData ? `id_${ item.token_instance?.id }` : `index_${ index }`); const key = item.token.address + '_' + (item.id && !isPlaceholderData ? `id_${ item.id }` : `index_${ index }`);
return ( return (
<NFTItem <NFTItem
key={ key } key={ key }
{ ...item } { ...item }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
withTokenLink
/> />
); );
}) } }) }
...@@ -52,8 +55,12 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -52,8 +55,12 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
emptyText="There are no tokens of selected type." emptyText="There are no tokens of selected type."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any token that matches your query.`,
hasActiveFilters,
}}
/> />
); );
}; };
export default ERC1155Tokens; export default AddressNFTs;
import { Show, Hide } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import ERC721TokensListItem from './ERC721TokensListItem';
import ERC721TokensTable from './ERC721TokensTable';
type Props = {
tokensQuery: QueryWithPagesResult<'address_tokens'>;
}
const ERC721Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination } = tokensQuery;
const actionBar = isMobile && pagination.isVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } isLoading={ isPlaceholderData } top={ pagination.isVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map((item, index) => (
<ERC721TokensListItem
key={ item.token.address + (isPlaceholderData ? index : '') }
{ ...item }
isLoading={ isPlaceholderData }
/>
)) }</Show></>
) : null;
return (
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
/>
);
};
export default ERC721Tokens;
import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = AddressTokenBalance & { isLoading: boolean};
const ERC721TokensListItem = ({ token, value, isLoading }: Props) => {
const router = useRouter();
const hash = router.query.hash?.toString() || '';
return (
<ListItemMobile rowGap={ 2 }>
<TokenEntityWithAddressFilter
token={ token }
isLoading={ isLoading }
addressHash={ hash }
noCopy
jointSymbol
fontWeight={ 700 }
/>
<Flex alignItems="center" pl={ 8 }>
<AddressEntity
address={{ hash: token.address }}
isLoading={ isLoading }
truncation="constant"
noIcon
/>
<AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
</Flex>
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Quantity</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ value }</span>
</Skeleton>
</HStack>
</ListItemMobile>
);
};
export default ERC721TokensListItem;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import ERC721TokensTableItem from './ERC721TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
isLoading: boolean;
}
const ERC721TokensTable = ({ data, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="40%">Asset</Th>
<Th width="40%">Contract address</Th>
<Th width="20%" isNumeric>Quantity</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => (
<ERC721TokensTableItem key={ item.token.address + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default ERC721TokensTable;
import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter';
type Props = AddressTokenBalance & { isLoading: boolean};
const ERC721TokensTableItem = ({
token,
value,
isLoading,
}: Props) => {
const router = useRouter();
const hash = router.query.hash?.toString() || '';
return (
<Tr>
<Td verticalAlign="middle">
<TokenEntityWithAddressFilter
token={ token }
addressHash={ hash }
isLoading={ isLoading }
noCopy
jointSymbol
fontWeight="700"
/>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
<AddressEntity
address={{ hash: token.address }}
isLoading={ isLoading }
noIcon
/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ value }
</Skeleton>
</Td>
</Tr>
);
};
export default React.memo(ERC721TokensTableItem);
import { Box, Flex, Text, Link, useColorModeValue } from '@chakra-ui/react'; import { Tag, Flex, Text, Link, Skeleton, LightMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressNFT } from 'types/api/address';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -9,22 +9,20 @@ import NftEntity from 'ui/shared/entities/nft/NftEntity'; ...@@ -9,22 +9,20 @@ import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import NftMedia from 'ui/shared/nft/NftMedia'; import NftMedia from 'ui/shared/nft/NftMedia';
type Props = AddressTokenBalance & { isLoading: boolean }; import NFTItemContainer from './NFTItemContainer';
const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLoading }: Props) => { type Props = AddressNFT & { isLoading: boolean; withTokenLink?: boolean };
const tokenInstanceLink = tokenId ? route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) : undefined;
const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: Props) => {
const tokenInstanceLink = tokenInstance.id ?
route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenInstance.id } }) :
undefined;
return ( return (
<Box <NFTItemContainer position="relative">
w={{ base: '100%', lg: '210px' }} <Skeleton isLoaded={ !isLoading }>
border="1px solid" <LightMode><Tag background="gray.50" zIndex={ 1 } position="absolute" top="18px" right="18px">{ token.type }</Tag></LightMode>
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } </Skeleton>
borderRadius="12px"
p="10px"
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<Link href={ isLoading ? undefined : tokenInstanceLink }> <Link href={ isLoading ? undefined : tokenInstanceLink }>
<NftMedia <NftMedia
mb="18px" mb="18px"
...@@ -32,19 +30,25 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo ...@@ -32,19 +30,25 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
isLoading={ isLoading } isLoading={ isLoading }
/> />
</Link> </Link>
{ tokenId && ( <Flex justifyContent="space-between" w="100%">
<Flex mb={ 2 } ml={ 1 }> <Flex ml={ 1 } overflow="hidden">
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
<NftEntity hash={ token.address } id={ tokenId } isLoading={ isLoading } noIcon/> <NftEntity hash={ token.address } id={ tokenInstance.id } isLoading={ isLoading } noIcon/>
</Flex> </Flex>
) } <Skeleton isLoaded={ !isLoading }>
{ Number(value) > 1 && <Flex><Text variant="secondary" whiteSpace="pre">Qty </Text>{ value }</Flex> }
</Skeleton>
</Flex>
{ withTokenLink && (
<TokenEntity <TokenEntity
mt={ 2 }
token={ token } token={ token }
isLoading={ isLoading } isLoading={ isLoading }
noCopy noCopy
noSymbol noSymbol
/> />
</Box> ) }
</NFTItemContainer>
); );
}; };
......
import { Box, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
type Props = {
children: React.ReactNode;
className?: string;
};
const NFTItemContainer = ({ children, className }: Props) => {
return (
<Box
w={{ base: '100%', lg: '210px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
p="10px"
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
className={ className }
>
{ children }
</Box>
);
};
export default chakra(NFTItemContainer);
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import type { SocketMessage } from 'lib/socket/types';
import type { AddressTokenBalance, AddressTokensBalancesSocketMessage, AddressTokensResponse } from 'types/api/address';
import type { TokenType } from 'types/api/token';
import { calculateUsdValue } from './tokenUtils'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { calculateUsdValue } from './tokenUtils';
interface Props { interface Props {
hash?: string; hash?: string;
} }
const tokenBalanceItemIdentityFactory = (match: AddressTokenBalance) => (item: AddressTokenBalance) => ((
match.token.address === item.token.address &&
match.token_id === item.token_id &&
match.token_instance?.id === item.token_instance?.id
));
export default function useFetchTokens({ hash }: Props) { export default function useFetchTokens({ hash }: Props) {
const erc20query = useApiQuery('address_tokens', { const erc20query = useApiQuery('address_tokens', {
pathParams: { hash }, pathParams: { hash },
...@@ -25,11 +37,67 @@ export default function useFetchTokens({ hash }: Props) { ...@@ -25,11 +37,67 @@ export default function useFetchTokens({ hash }: Props) {
queryOptions: { enabled: Boolean(hash), refetchOnMount: false }, queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
}); });
const refetch = React.useCallback(() => { const queryClient = useQueryClient();
erc20query.refetch();
erc721query.refetch(); const updateTokensData = React.useCallback((type: TokenType, payload: AddressTokensBalancesSocketMessage) => {
erc1155query.refetch(); const queryKey = getResourceKey('address_tokens', { pathParams: { hash }, queryParams: { type } });
}, [ erc1155query, erc20query, erc721query ]);
queryClient.setQueryData(queryKey, (prevData: AddressTokensResponse | undefined) => {
const items = prevData?.items.map((currentItem) => {
const updatedData = payload.token_balances.find(tokenBalanceItemIdentityFactory(currentItem));
return updatedData ?? currentItem;
}) || [];
const extraItems = prevData?.next_page_params ?
[] :
payload.token_balances.filter((socketItem) => !items.some(tokenBalanceItemIdentityFactory(socketItem)));
if (!prevData) {
return {
items: extraItems,
next_page_params: null,
};
}
return {
items: items.concat(extraItems),
next_page_params: prevData.next_page_params,
};
});
}, [ hash, queryClient ]);
const handleTokenBalancesErc20Message: SocketMessage.AddressTokenBalancesErc20['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-20', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc721Message: SocketMessage.AddressTokenBalancesErc721['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-721', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc1155Message: SocketMessage.AddressTokenBalancesErc1155['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-1155', payload);
}, [ updateTokensData ]);
const channel = useSocketChannel({
topic: `addresses:${ hash?.toLowerCase() }`,
isDisabled: Boolean(hash) && (erc20query.isPlaceholderData || erc721query.isPlaceholderData || erc1155query.isPlaceholderData),
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_20',
handler: handleTokenBalancesErc20Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_721',
handler: handleTokenBalancesErc721Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_1155',
handler: handleTokenBalancesErc1155Message,
});
const data = React.useMemo(() => { const data = React.useMemo(() => {
return { return {
...@@ -52,6 +120,5 @@ export default function useFetchTokens({ hash }: Props) { ...@@ -52,6 +120,5 @@ export default function useFetchTokens({ hash }: Props) {
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending, isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError, isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data, data,
refetch,
}; };
} }
...@@ -77,7 +77,7 @@ const Stats = () => { ...@@ -77,7 +77,7 @@ const Stats = () => {
<StatsItem <StatsItem
icon={ clockIcon } icon={ clockIcon }
title="Average block time" title="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) } s` } value={ `${ (data.average_block_time / 1000).toFixed(1) }s` }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
) } ) }
......
...@@ -3,13 +3,11 @@ import React from 'react'; ...@@ -3,13 +3,11 @@ import React from 'react';
import type { TimeChartData } from 'ui/shared/chart/types'; import type { TimeChartData } from 'ui/shared/chart/types';
import useClientRect from 'lib/hooks/useClientRect';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine'; import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay'; import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartTooltip from 'ui/shared/chart/ChartTooltip'; import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useTimeChartController from 'ui/shared/chart/useTimeChartController'; import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props { interface Props {
data: TimeChartData; data: TimeChartData;
...@@ -22,12 +20,17 @@ const ChainIndicatorChart = ({ data }: Props) => { ...@@ -22,12 +20,17 @@ const ChainIndicatorChart = ({ data }: Props) => {
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const lineColor = useToken('colors', 'blue.500'); const lineColor = useToken('colors', 'blue.500');
const [ rect, ref ] = useClientRect<SVGSVGElement>(); const axesConfig = React.useMemo(() => {
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN); return {
const { xScale, yScale } = useTimeChartController({ x: { ticks: 4 },
y: { ticks: 3, nice: true },
};
}, [ ]);
const { rect, ref, axis, innerWidth, innerHeight } = useTimeChartController({
data, data,
width: innerWidth, margin: CHART_MARGIN,
height: innerHeight, axesConfig,
}); });
return ( return (
...@@ -35,13 +38,13 @@ const ChainIndicatorChart = ({ data }: Props) => { ...@@ -35,13 +38,13 @@ const ChainIndicatorChart = ({ data }: Props) => {
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }> <g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }>
<ChartArea <ChartArea
data={ data[0].items } data={ data[0].items }
xScale={ xScale } xScale={ axis.x.scale }
yScale={ yScale } yScale={ axis.y.scale }
/> />
<ChartLine <ChartLine
data={ data[0].items } data={ data[0].items }
xScale={ xScale } xScale={ axis.x.scale }
yScale={ yScale } yScale={ axis.y.scale }
stroke={ lineColor } stroke={ lineColor }
animation="left" animation="left"
strokeWidth={ 3 } strokeWidth={ 3 }
...@@ -51,8 +54,8 @@ const ChainIndicatorChart = ({ data }: Props) => { ...@@ -51,8 +54,8 @@ const ChainIndicatorChart = ({ data }: Props) => {
anchorEl={ overlayRef.current } anchorEl={ overlayRef.current }
width={ innerWidth } width={ innerWidth }
height={ innerHeight } height={ innerHeight }
xScale={ xScale } xScale={ axis.x.scale }
yScale={ yScale } yScale={ axis.y.scale }
data={ data } data={ data }
/> />
</ChartOverlay> </ChartOverlay>
......
...@@ -15,7 +15,7 @@ const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = { ...@@ -15,7 +15,7 @@ const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
title: 'Daily transactions', title: 'Daily transactions',
value: (stats) => Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), value: (stats) => Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>, icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `The total daily number of transactions on the blockchain for the last month.`, hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`,
api: { api: {
resourceName: 'homepage_chart_txs', resourceName: 'homepage_chart_txs',
dataFn: (response) => ([ { dataFn: (response) => ([ {
......
...@@ -84,7 +84,6 @@ const MarketplaceAppCard = ({ ...@@ -84,7 +84,6 @@ const MarketplaceAppCard = ({
marginBottom={ 4 } marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }} w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }} h={{ base: '64px', sm: '96px' }}
borderRadius={ 8 }
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
...@@ -92,6 +91,7 @@ const MarketplaceAppCard = ({ ...@@ -92,6 +91,7 @@ const MarketplaceAppCard = ({
<Image <Image
src={ isLoading ? undefined : logoUrl } src={ isLoading ? undefined : logoUrl }
alt={ `${ title } app icon` } alt={ `${ title } app icon` }
borderRadius="8px"
/> />
</Skeleton> </Skeleton>
......
import { Box, Flex, Icon } from '@chakra-ui/react'; import { Box, Flex, HStack, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/Tabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
...@@ -25,6 +24,7 @@ import AddressTxs from 'ui/address/AddressTxs'; ...@@ -25,6 +24,7 @@ import AddressTxs from 'ui/address/AddressTxs';
import AddressWithdrawals from 'ui/address/AddressWithdrawals'; import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
import SolidityscanReport from 'ui/address/SolidityscanReport';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
...@@ -35,13 +35,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -35,13 +35,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
export const tokenTabsByType: Record<TokenType, string> = { const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
'ERC-20': 'tokens_erc20',
'ERC-721': 'tokens_erc721',
'ERC-1155': 'tokens_erc1155',
} as const;
const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -194,7 +188,9 @@ const AddressPageContent = () => { ...@@ -194,7 +188,9 @@ const AddressPageContent = () => {
) } ) }
<AddressQrCode address={{ hash }} isLoading={ isLoading }/> <AddressQrCode address={{ hash }} isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/> <AccountActionsMenu isLoading={ isLoading }/>
<NetworkExplorers type="address" pathParam={ hash } ml="auto"/> <HStack ml="auto" gap={ 2 }/>
{ addressQuery.data?.is_contract && config.UI.views.address.solidityscanEnabled && <SolidityscanReport hash={ hash }/> }
<NetworkExplorers type="address" pathParam={ hash }/>
</Flex> </Flex>
); );
......
...@@ -56,6 +56,7 @@ const TokenPageContent = () => { ...@@ -56,6 +56,7 @@ const TokenPageContent = () => {
const hashString = getQueryParamString(router.query.hash); const hashString = getQueryParamString(router.query.hash);
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
const ownerFilter = getQueryParamString(router.query.holder_address_hash) || undefined;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -140,6 +141,7 @@ const TokenPageContent = () => { ...@@ -140,6 +141,7 @@ const TokenPageContent = () => {
const inventoryQuery = useQueryWithPages({ const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory', resourceName: 'token_inventory',
pathParams: { hash: hashString }, pathParams: { hash: hashString },
filters: ownerFilter ? { holder_address_hash: ownerFilter } : {},
scrollRef, scrollRef,
options: { options: {
enabled: Boolean( enabled: Boolean(
...@@ -150,7 +152,7 @@ const TokenPageContent = () => { ...@@ -150,7 +152,7 @@ const TokenPageContent = () => {
tab === 'inventory' tab === 'inventory'
), ),
), ),
placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: null }), placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: { unique_token: 1 } }),
}, },
}); });
...@@ -173,9 +175,11 @@ const TokenPageContent = () => { ...@@ -173,9 +175,11 @@ const TokenPageContent = () => {
const contractTabs = useContractTabs(contractQuery.data); const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? {
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } : id: 'inventory',
undefined, title: 'Inventory',
component: <TokenInventory inventoryQuery={ inventoryQuery } tokenQuery={ tokenQuery } ownerFilter={ ownerFilter }/>,
} : undefined,
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
contractQuery.data?.is_contract ? { contractQuery.data?.is_contract ? {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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