Commit 3d8e3986 authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into category-tabs

parents b352e18e 878cb938
{ {
"name": "blockscout dev", "name": "blockscout dev",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18", "image": "mcr.microsoft.com/devcontainers/typescript-node:20",
"forwardPorts": [ 3000 ], "forwardPorts": [ 3000 ],
"customizations": { "customizations": {
"vscode": { "vscode": {
......
...@@ -25,16 +25,16 @@ jobs: ...@@ -25,16 +25,16 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip checks') && !(github.event.action == 'unlabeled' && github.event.label.name != 'skip checks') }} if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip checks') && !(github.event.action == 'unlabeled' && github.event.label.name != 'skip checks') }}
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20.11.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v3 uses: actions/cache@v4
id: cache-node-modules id: cache-node-modules
with: with:
path: | path: |
...@@ -57,16 +57,16 @@ jobs: ...@@ -57,16 +57,16 @@ jobs:
needs: [ code_quality ] needs: [ code_quality ]
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20.11.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v3 uses: actions/cache@v4
id: cache-node-modules id: cache-node-modules
with: with:
path: | path: |
...@@ -94,16 +94,16 @@ jobs: ...@@ -94,16 +94,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20.11.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v3 uses: actions/cache@v4
id: cache-node-modules id: cache-node-modules
with: with:
path: | path: |
...@@ -122,7 +122,7 @@ jobs: ...@@ -122,7 +122,7 @@ jobs:
needs: [ code_quality, envs_validation ] needs: [ code_quality, envs_validation ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.35.1-focal image: mcr.microsoft.com/playwright:v1.41.1-focal
strategy: strategy:
fail-fast: false fail-fast: false
...@@ -134,18 +134,18 @@ jobs: ...@@ -134,18 +134,18 @@ jobs:
run: apt-get update && apt-get install git-lfs run: apt-get update && apt-get install git-lfs
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
lfs: 'true' lfs: 'true'
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20.11.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v3 uses: actions/cache@v4
id: cache-node-modules id: cache-node-modules
with: with:
path: | path: |
...@@ -164,7 +164,7 @@ jobs: ...@@ -164,7 +164,7 @@ jobs:
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: playwright-report-${{ matrix.project }} name: playwright-report-${{ matrix.project }}
path: playwright-report path: playwright-report
......
...@@ -16,16 +16,16 @@ jobs: ...@@ -16,16 +16,16 @@ jobs:
if: ${{ github.ref_type == 'tag' }} if: ${{ github.ref_type == 'tag' }}
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20.11.0
cache: 'yarn' cache: 'yarn'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v3 uses: actions/cache@v4
id: cache-node-modules id: cache-node-modules
with: with:
path: | path: |
......
# ***************************** # *****************************
# *** STAGE 1: Dependencies *** # *** STAGE 1: Dependencies ***
# ***************************** # *****************************
FROM node:18-alpine AS deps FROM node:20.11.0-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
...@@ -10,7 +10,7 @@ RUN apk add --no-cache libc6-compat ...@@ -10,7 +10,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN apk add git RUN apk add git
RUN yarn --frozen-lockfile --ignore-optional RUN yarn --frozen-lockfile
### FEATURE REPORTER ### FEATURE REPORTER
...@@ -30,7 +30,7 @@ RUN yarn --frozen-lockfile ...@@ -30,7 +30,7 @@ RUN yarn --frozen-lockfile
# ***************************** # *****************************
# ****** STAGE 2: Build ******* # ****** STAGE 2: Build *******
# ***************************** # *****************************
FROM node:18-alpine AS builder FROM node:20.11.0-alpine AS builder
RUN apk add --no-cache --upgrade libc6-compat bash RUN apk add --no-cache --upgrade libc6-compat bash
# pass commit sha and git tag to the app image # pass commit sha and git tag to the app image
...@@ -78,7 +78,7 @@ RUN cd ./deploy/tools/envs-validator && yarn build ...@@ -78,7 +78,7 @@ RUN cd ./deploy/tools/envs-validator && yarn build
# ******* STAGE 3: Run ******** # ******* STAGE 3: Run ********
# ***************************** # *****************************
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM node:18-alpine AS runner FROM node:20.11.0-alpine AS runner
RUN apk add --no-cache --upgrade bash curl jq unzip RUN apk add --no-cache --upgrade bash curl jq unzip
### APP ### APP
......
...@@ -8,6 +8,7 @@ const chain = Object.freeze({ ...@@ -8,6 +8,7 @@ const chain = Object.freeze({
shortName: getEnvValue('NEXT_PUBLIC_NETWORK_SHORT_NAME'), shortName: getEnvValue('NEXT_PUBLIC_NETWORK_SHORT_NAME'),
currency: { currency: {
name: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_NAME'), name: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_NAME'),
weiName: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME'),
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'), symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'),
decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS, decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS,
}, },
......
...@@ -20,6 +20,7 @@ export { default as sol2uml } from './sol2uml'; ...@@ -20,6 +20,7 @@ export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats'; export { default as stats } from './stats';
export { default as suave } from './suave'; export { default as suave } from './suave';
export { default as txInterpretation } from './txInterpretation'; export { default as txInterpretation } from './txInterpretation';
export { default as web3Wallet } from './web3Wallet'; export { default as userOps } from './userOps';
export { default as verifiedTokens } from './verifiedTokens'; export { default as verifiedTokens } from './verifiedTokens';
export { default as web3Wallet } from './web3Wallet';
export { default as zkEvmRollup } from './zkEvmRollup'; export { default as zkEvmRollup } from './zkEvmRollup';
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const title = 'User operations';
const config: Feature<{ isEnabled: true }> = (() => {
if (getEnvValue('NEXT_PUBLIC_HAS_USER_OPS') === 'true') {
return Object.freeze({
title,
isEnabled: true,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId } from 'types/client/navigation-items'; import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId } from 'types/client/navigation-items';
import type { ChainIndicatorId } from 'types/homepage'; import type { ChainIndicatorId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks'; import type { NetworkExplorer } from 'types/networks';
...@@ -66,6 +67,9 @@ const UI = Object.freeze({ ...@@ -66,6 +67,9 @@ const UI = Object.freeze({
explorers: { explorers: {
items: parseEnvJson<Array<NetworkExplorer>>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [], items: parseEnvJson<Array<NetworkExplorer>>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [],
}, },
ides: {
items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
},
}); });
export default UI; export default UI;
...@@ -49,6 +49,9 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com ...@@ -49,6 +49,9 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
#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
...@@ -33,6 +33,7 @@ NEXT_PUBLIC_GIT_TAG=v1.0.11 ...@@ -33,6 +33,7 @@ NEXT_PUBLIC_GIT_TAG=v1.0.11
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'}] NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'http://localhost:3000/nft-marketplace-logo.png'}]
## misc ## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/%23address={hash}&blockscout=eth-goerli.blockscout.com'}]
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE= NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=
# app features # app features
......
...@@ -11,6 +11,7 @@ import * as yup from 'yup'; ...@@ -11,6 +11,7 @@ import * as yup from 'yup';
import type { AdButlerConfig } from '../../../types/client/adButlerConfig'; import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders'; import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders';
import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders'; import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders';
import type { ContractCodeIde } from '../../../types/client/contract';
import type { MarketplaceAppOverview } from '../../../types/client/marketplace'; import type { MarketplaceAppOverview } from '../../../types/client/marketplace';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items'; import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items'; import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
...@@ -264,6 +265,13 @@ const networkExplorerSchema: yup.ObjectSchema<NetworkExplorer> = yup ...@@ -264,6 +265,13 @@ const networkExplorerSchema: yup.ObjectSchema<NetworkExplorer> = yup
}), }),
}); });
const contractCodeIdeSchema: yup.ObjectSchema<ContractCodeIde> = yup
.object({
title: yup.string().required(),
url: yup.string().test(urlTest).required(),
icon_url: yup.string().test(urlTest).required(),
});
const nftMarketplaceSchema: yup.ObjectSchema<NftMarketplaceItem> = yup const nftMarketplaceSchema: yup.ObjectSchema<NftMarketplaceItem> = yup
.object({ .object({
name: yup.string().required(), name: yup.string().required(),
...@@ -331,6 +339,7 @@ const schema = yup ...@@ -331,6 +339,7 @@ const schema = yup
NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(), NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(),
NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest), NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest),
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(), NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(),
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL: yup.string(),
...@@ -417,6 +426,11 @@ const schema = yup ...@@ -417,6 +426,11 @@ const schema = yup
.transform(replaceQuotes) .transform(replaceQuotes)
.json() .json()
.of(networkExplorerSchema), .of(networkExplorerSchema),
NEXT_PUBLIC_CONTRACT_CODE_IDES: yup
.array()
.transform(replaceQuotes)
.json()
.of(contractCodeIdeSchema),
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(),
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(), NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(), NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
...@@ -447,6 +461,7 @@ const schema = yup ...@@ -447,6 +461,7 @@ const schema = yup
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
...@@ -9,6 +9,7 @@ NEXT_PUBLIC_APP_PORT=3000 ...@@ -9,6 +9,7 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_PROTOCOL=http NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://example.com'}] NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS=[{'id':'1','title':'Ethereum','short_title':'ETH','base_url':'https://example.com'}]
NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}] NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES=[{'type':'omni','title':'OmniBridge','short_title':'OMNI'}]
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com
NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_FOOTER_LINKS=https://example.com NEXT_PUBLIC_FOOTER_LINKS=https://example.com
......
...@@ -161,6 +161,7 @@ frontend: ...@@ -161,6 +161,7 @@ frontend:
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
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'}]"
NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
envFromSecret: envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
......
...@@ -73,6 +73,8 @@ frontend: ...@@ -73,6 +73,8 @@ frontend:
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'}]"
NEXT_PUBLIC_HAS_USER_OPS: true
NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
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
......
...@@ -14,7 +14,7 @@ Thanks for showing interest to contribute to Blockscout. The following steps wil ...@@ -14,7 +14,7 @@ Thanks for showing interest to contribute to Blockscout. The following steps wil
cd <fork_name> cd <fork_name>
``` ```
3. Make sure you're running Node.js 18+ and NPM 8+; if not, upgrade it accordingly, for example using [nvm](https://github.com/nvm-sh/nvm). 3. Make sure you're running Node.js 20+ and NPM 10+; if not, upgrade it accordingly, for example using [nvm](https://github.com/nvm-sh/nvm).
```sh ```sh
node -v node -v
npm -v npm -v
......
...@@ -33,6 +33,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -33,6 +33,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Banner ads](ENVS.md#banner-ads) - [Banner ads](ENVS.md#banner-ads)
- [Text ads](ENVS.md#text-ads) - [Text ads](ENVS.md#text-ads)
- [Beacon chain](ENVS.md#beacon-chain) - [Beacon chain](ENVS.md#beacon-chain)
- [User operations](ENVS.md#user-operations-feature-erc-4337)
- [Optimistic rollup (L2) chain](ENVS.md#optimistic-rollup-l2-chain) - [Optimistic rollup (L2) chain](ENVS.md#optimistic-rollup-l2-chain)
- [ZkEvm rollup (L2) chain](NVS.md#zkevm-rollup-l2-chain) - [ZkEvm rollup (L2) chain](NVS.md#zkevm-rollup-l2-chain)
- [Export data to CSV file](ENVS.md#export-data-to-csv-file) - [Export data to CSV file](ENVS.md#export-data-to-csv-file)
...@@ -77,6 +78,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -77,6 +78,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | | NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference | - | - | `https://core.poa.network` | | NEXT_PUBLIC_NETWORK_RPC_URL | `string` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference | - | - | `https://core.poa.network` |
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | | NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` |
| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | | NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | | NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` |
...@@ -258,6 +260,7 @@ Settings for meta tags and OG tags ...@@ -258,6 +260,7 @@ Settings for meta tags and OG tags
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_CONTRACT_CODE_IDES | `Array<ContractCodeIde>` where `ContractCodeIde` can have following [properties](#contract-code-ide-configuration-properties) | Used to build up links to IDEs with contract source code. | - | - | `[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout={domain}','icon_url':'https://example.com/icon.svg'}]` |
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` | | NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` |
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | | NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` |
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | | NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` |
...@@ -272,6 +275,14 @@ Settings for meta tags and OG tags ...@@ -272,6 +275,14 @@ Settings for meta tags and OG tags
*Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>` *Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>`
#### Contract code IDE configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| title | `string` | Displayed name of the IDE | Required | - | `Remix IDE` |
| url | `string` | URL of the IDE with placeholders for contract hash (`{hash}`) and current domain (`{domain}`) | Required | - | `https://remix.blockscout.com/?address={hash}&blockscout={domain}` |
| icon_url | `string` | URL of the IDE icon | Required | - | `https://example.com/icon.svg` |
&nbsp; &nbsp;
## App features ## App features
...@@ -345,6 +356,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -345,6 +356,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp; &nbsp;
### User operations feature (ERC-4337)
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HAS_USER_OPS | `boolean` | Set to true to show user operations related data and pages | - | - | `true` |
&nbsp;
### Optimistic rollup (L2) chain ### Optimistic rollup (L2) chain
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
<g clip-path="url(#a)" fill="currentColor">
<path d="M5.176 5.291a1.253 1.253 0 0 0-1.76.159L.29 9.2a1.247 1.247 0 0 0 0 1.6l3.125 3.75a1.25 1.25 0 1 0 1.919-1.6L2.876 10l2.459-2.949a1.25 1.25 0 0 0-.159-1.76Z"/>
<path d="M5.176 5.291a1.253 1.253 0 0 0-1.76.159L.29 9.2a1.247 1.247 0 0 0 0 1.6l3.125 3.75a1.25 1.25 0 1 0 1.919-1.6L2.876 10l2.459-2.949a1.25 1.25 0 0 0-.159-1.76Zm6.318-2.766a1.261 1.261 0 0 0-1.47.981l-2.5 12.5a1.247 1.247 0 0 0 1.23 1.494 1.249 1.249 0 0 0 1.223-1.006l2.5-12.5a1.246 1.246 0 0 0-.982-1.469Z"/>
<path d="M11.494 2.525a1.261 1.261 0 0 0-1.47.981l-2.5 12.5a1.247 1.247 0 0 0 1.23 1.494 1.249 1.249 0 0 0 1.223-1.006l2.5-12.5a1.246 1.246 0 0 0-.982-1.469ZM19.708 9.2l-3.124-3.75a1.249 1.249 0 1 0-1.92 1.601l2.46 2.95-2.46 2.948a1.25 1.25 0 1 0 1.92 1.602l3.124-3.75a1.246 1.246 0 0 0 0-1.601Z"/>
<path d="m19.708 9.2-3.124-3.75a1.249 1.249 0 1 0-1.92 1.601l2.46 2.95-2.46 2.948a1.25 1.25 0 1 0 1.92 1.602l3.124-3.75a1.246 1.246 0 0 0 0-1.601Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h20v20H0z"/>
</clipPath>
</defs>
</svg>
<svg viewBox="0 0 18 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="2" cy="2" r="2" fill="currentColor"/>
<circle cx="9" cy="2" r="2" fill="currentColor"/>
<circle cx="16" cy="2" r="2" fill="currentColor"/>
</svg>
<svg viewBox="-1 -1 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="-1 -1 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.24.661a.964.964 0 1 0-1.831.607l.426 1.285a.964.964 0 0 0 1.83-.606L7.24.661Zm10.478.985A.964.964 0 0 0 16.354.283l-3.495 3.495-.204-.204a2.893 2.893 0 0 0-4.091 0L6.75 5.388A.964.964 0 0 0 5.388 6.75L3.575 8.562a2.893 2.893 0 0 0 0 4.091l.204.204-3.496 3.497a.964.964 0 1 0 1.364 1.363l3.496-3.496.205.205a2.893 2.893 0 0 0 4.091 0l1.813-1.813a.964.964 0 0 0 1.361-1.361l1.815-1.814a2.893 2.893 0 0 0 0-4.091l-.206-.206 3.496-3.495ZM11.25 9.887l1.813-1.813a.964.964 0 0 0 0-1.364l-.839-.839a.996.996 0 0 1-.096-.096l-.838-.838a.964.964 0 0 0-1.363 0L8.114 6.751l3.137 3.136Zm-4.5-1.773L4.939 9.926a.964.964 0 0 0 0 1.364l.864.864a1.193 1.193 0 0 1 .044.043l.865.866a.964.964 0 0 0 1.363 0l1.812-1.812-3.136-3.137Zm4.196 6.72a.964.964 0 0 1 1.218.613l.426 1.285a.964.964 0 1 1-1.83.607l-.426-1.286a.964.964 0 0 1 .612-1.219Zm3.888-3.887a.964.964 0 0 1 1.218-.612l1.286.425a.964.964 0 1 1-.607 1.831l-1.285-.426a.964.964 0 0 1-.612-1.218ZM1.268 5.409a.964.964 0 1 0-.607 1.83l1.286.426a.964.964 0 1 0 .606-1.83l-1.285-.426Z" fill="#38A169"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.24.661a.964.964 0 1 0-1.831.607l.426 1.285a.964.964 0 0 0 1.83-.606L7.24.661Zm10.478.985A.964.964 0 0 0 16.354.283l-3.495 3.495-.204-.204a2.893 2.893 0 0 0-4.091 0L6.75 5.388A.964.964 0 0 0 5.388 6.75L3.575 8.562a2.893 2.893 0 0 0 0 4.091l.204.204-3.496 3.497a.964.964 0 1 0 1.364 1.363l3.496-3.496.205.205a2.893 2.893 0 0 0 4.091 0l1.813-1.813a.964.964 0 0 0 1.361-1.361l1.815-1.814a2.893 2.893 0 0 0 0-4.091l-.206-.206 3.496-3.495ZM11.25 9.887l1.813-1.813a.964.964 0 0 0 0-1.364l-.839-.839a.996.996 0 0 1-.096-.096l-.838-.838a.964.964 0 0 0-1.363 0L8.114 6.751l3.137 3.136Zm-4.5-1.773L4.939 9.926a.964.964 0 0 0 0 1.364l.864.864a1.193 1.193 0 0 1 .044.043l.865.866a.964.964 0 0 0 1.363 0l1.812-1.812-3.136-3.137Zm4.196 6.72a.964.964 0 0 1 1.218.613l.426 1.285a.964.964 0 1 1-1.83.607l-.426-1.286a.964.964 0 0 1 .612-1.219Zm3.888-3.887a.964.964 0 0 1 1.218-.612l1.286.425a.964.964 0 1 1-.607 1.831l-1.285-.426a.964.964 0 0 1-.612-1.218ZM1.268 5.409a.964.964 0 1 0-.607 1.83l1.286.426a.964.964 0 1 0 .606-1.83l-1.285-.426Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 -1 19 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 -1 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.394.273a.931.931 0 0 1 0 1.316l-1.825 1.825.194.194h.001a2.795 2.795 0 0 1 0 3.956l-1.747 1.747a.931.931 0 0 1-1.354 1.278l-.609-.61-.011-.01-.012-.012L8.71 5.635l-.011-.011-.011-.012-.61-.609A.931.931 0 0 1 9.355 3.65l1.747-1.747a2.794 2.794 0 0 1 3.956 0l.195.194L17.077.273a.931.931 0 0 1 1.317 0Zm-2.945 5.973L13.7 7.994l-3.028-3.029 1.748-1.747a.93.93 0 0 1 1.319 0l1.708 1.708a.93.93 0 0 1 0 1.32ZM6.563 8.304l-.893-.893a.931.931 0 0 0-1.354 1.278L2.57 10.435l-.001.001a2.793 2.793 0 0 0 0 3.956l.195.194-1.825 1.825a.931.931 0 0 0 1.317 1.316l1.824-1.824.194.194a2.793 2.793 0 0 0 3.956 0h.001l1.747-1.747a.931.931 0 0 0 1.278-1.353l-.894-.893.894-.894a.931.931 0 0 0-1.317-1.317l-.893.894L7.88 9.62l.893-.894a.931.931 0 1 0-1.317-1.316l-.893.893Zm-.655 1.978-.003-.003-.004-.003-.27-.27-1.747 1.748a.931.931 0 0 0 0 1.32l.845.844.01.01.01.01.843.843.001.002a.93.93 0 0 0 1.319 0l1.748-1.748-2.752-2.753Z" fill="#A0AEC0"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.394.273a.931.931 0 0 1 0 1.316l-1.825 1.825.194.194h.001a2.795 2.795 0 0 1 0 3.956l-1.747 1.747a.931.931 0 0 1-1.354 1.278l-.609-.61-.011-.01-.012-.012L8.71 5.635l-.011-.011-.011-.012-.61-.609A.931.931 0 0 1 9.355 3.65l1.747-1.747a2.794 2.794 0 0 1 3.956 0l.195.194L17.077.273a.931.931 0 0 1 1.317 0Zm-2.945 5.973L13.7 7.994l-3.028-3.029 1.748-1.747a.93.93 0 0 1 1.319 0l1.708 1.708a.93.93 0 0 1 0 1.32ZM6.563 8.304l-.893-.893a.931.931 0 0 0-1.354 1.278L2.57 10.435l-.001.001a2.793 2.793 0 0 0 0 3.956l.195.194-1.825 1.825a.931.931 0 0 0 1.317 1.316l1.824-1.824.194.194a2.793 2.793 0 0 0 3.956 0h.001l1.747-1.747a.931.931 0 0 0 1.278-1.353l-.894-.893.894-.894a.931.931 0 0 0-1.317-1.317l-.893.894L7.88 9.62l.893-.894A.931.931 0 1 0 7.456 7.41l-.893.893Zm-.655 1.978-.003-.003-.004-.003-.27-.27-1.747 1.748a.931.931 0 0 0 0 1.32l.845.844.01.01.01.01.843.843.001.002a.93.93 0 0 0 1.319 0l1.748-1.748-2.752-2.753Z" fill="currentColor"/>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
<path fill="currentColor" d="M22.576 11.994a1.212 1.212 0 0 0 0-2.424H9.14L10.703 8a1.212 1.212 0 0 0-1.709-1.709L5.358 9.928a1.212 1.212 0 0 0-.267 1.32 1.212 1.212 0 0 0 1.121.746h16.364Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M15.859 18.661c.091-.363.14-.754.14-1.161 0-.445-.059-.871-.167-1.263h7.955a1.213 1.213 0 0 1 1.121.745 1.212 1.212 0 0 1-.267 1.321l-3.636 3.637a1.214 1.214 0 0 1-2.049-.347 1.212 1.212 0 0 1 .34-1.362l1.564-1.57h-5.001Zm-3.633 1.862a3.636 3.636 0 0 0 1.275-1.83 3.615 3.615 0 0 0-.035-2.225 3.638 3.638 0 0 0-1.334-1.787 3.675 3.675 0 0 0-4.264 0 3.638 3.638 0 0 0-1.333 1.787 3.615 3.615 0 0 0-.036 2.226 3.636 3.636 0 0 0 1.275 1.829 5.482 5.482 0 0 0-2.714 2.606.588.588 0 0 0 .038.583.593.593 0 0 0 .51.288h1.413C7.2 22.76 8.465 21.8 10 21.8c1.535 0 2.8.96 2.979 2.2h1.412a.599.599 0 0 0 .511-.288.588.588 0 0 0 .038-.583 5.482 5.482 0 0 0-2.714-2.606ZM11.7 17.5a1.7 1.7 0 1 1-3.4 0 1.7 1.7 0 0 1 3.4 0Z" clip-rule="evenodd"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 21">
<path d="m.29 5.053 3.722-3.828a.972.972 0 0 1 1.337.06c.177.183.283.429.292.69.01.257-.074.509-.234.704L3.399 4.751H17.77c.262 0 .514.107.7.3a1.05 1.05 0 0 1 0 1.459.976.976 0 0 1-.7.298H.987a.969.969 0 0 1-.55-.17 1.021 1.021 0 0 1-.368-.46 1.062 1.062 0 0 1 .22-1.125zm18.723 6a.97.97 0 0 1 .55.17c.163.111.292.271.368.46a1.061 1.061 0 0 1-.22 1.125l-3.737 3.841-.006.008a.997.997 0 0 1-.322.255.966.966 0 0 1-1.13-.197c-.097-.1-.174-.22-.224-.352a1.067 1.067 0 0 1 .03-.828c.06-.128.146-.241.25-.333l.008-.006 2.02-2.087h-5.88c.103-.684.183-1.403 0-2.072zM8.501 13.694a3.636 3.636 0 0 1-1.275 1.829 5.482 5.482 0 0 1 2.714 2.606.588.588 0 0 1-.038.583.593.593 0 0 1-.51.288H7.978C7.8 17.76 6.535 16.8 5 16.8s-2.8.96-2.979 2.2H.61a.599.599 0 0 1-.511-.288.588.588 0 0 1-.038-.583 5.482 5.482 0 0 1 2.714-2.606 3.636 3.636 0 0 1-1.275-1.83 3.615 3.615 0 0 1 .036-2.224A3.638 3.638 0 0 1 2.868 9.68a3.675 3.675 0 0 1 4.264 0 3.638 3.638 0 0 1 1.333 1.788c.246.72.259 1.497.036 2.225zM6.7 12.5a1.7 1.7 0 1 1-3.4 0 1.7 1.7 0 0 1 3.4 0z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"/>
</svg>
<svg viewBox="2 2 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.667 3.75c0-.688-.563-1.25-1.25-1.25-.688 0-1.25.563-1.25 1.25 0 .688.562 1.25 1.25 1.25.687 0 1.25-.563 1.25-1.25Zm0 12.5c0-.688-.563-1.25-1.25-1.25-.688 0-1.25.563-1.25 1.25 0 .688.562 1.25 1.25 1.25.687 0 1.25-.563 1.25-1.25Zm0-6.25c0-.688-.563-1.25-1.25-1.25-.688 0-1.25.563-1.25 1.25 0 .688.562 1.25 1.25 1.25.687 0 1.25-.563 1.25-1.25Z" fill="currentColor" fill-opacity=".8"/>
</svg>
...@@ -77,6 +77,7 @@ import type { ...@@ -77,6 +77,7 @@ import type {
import type { TxInterpretationResponse } from 'types/api/txInterpretation'; import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
...@@ -579,6 +580,20 @@ export const RESOURCES = { ...@@ -579,6 +580,20 @@ export const RESOURCES = {
filterFields: [], filterFields: [],
}, },
// USER OPS
user_ops: {
path: '/api/v2/proxy/account-abstraction/operations',
filterFields: [ 'transaction_hash' as const, 'sender' as const ],
},
user_op: {
path: '/api/v2/proxy/account-abstraction/operations/:hash',
pathParams: [ 'hash' as const ],
},
user_ops_account: {
path: '/api/v2/proxy/account-abstraction/accounts/:hash',
pathParams: [ 'hash' as const ],
},
// CONFIGS // CONFIGS
config_backend_version: { config_backend_version: {
path: '/api/v2/config/backend-version', path: '/api/v2/config/backend-version',
...@@ -651,7 +666,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -651,7 +666,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup'; 'domains_lookup' | 'addresses_lookup' | 'user_ops';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -754,6 +769,9 @@ Q extends 'addresses_lookup' ? EnsAddressLookupResponse : ...@@ -754,6 +769,9 @@ Q extends 'addresses_lookup' ? EnsAddressLookupResponse :
Q extends 'domain_info' ? EnsDomainDetailed : Q extends 'domain_info' ? EnsDomainDetailed :
Q extends 'domain_events' ? EnsDomainEventsResponse : Q extends 'domain_events' ? EnsDomainEventsResponse :
Q extends 'domains_lookup' ? EnsDomainLookupResponse : Q extends 'domains_lookup' ? EnsDomainLookupResponse :
Q extends 'user_ops' ? UserOpsResponse :
Q extends 'user_op' ? UserOp :
Q extends 'user_ops_account' ? UserOpsAccount :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -775,6 +793,7 @@ Q extends 'tokens_bridged' ? TokensBridgedFilters : ...@@ -775,6 +793,7 @@ Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters : Q extends 'verified_contracts' ? VerifiedContractsFilters :
Q extends 'addresses_lookup' ? EnsAddressLookupFilters : Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters : Q extends 'domains_lookup' ? EnsDomainLookupFilters :
Q extends 'user_ops' ? UserOpsFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -57,6 +57,7 @@ export default function useApiFetch() { ...@@ -57,6 +57,7 @@ export default function useApiFetch() {
}, },
{ {
resource: resource.path, resource: resource.path,
omitSentryErrorLog: true, // disable logging of API errors to Sentry
}, },
); );
}, [ fetch, csrfToken ]); }, [ fetch, csrfToken ]);
......
...@@ -4,20 +4,22 @@ import React from 'react'; ...@@ -4,20 +4,22 @@ import React from 'react';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
export const retry = (failureCount: number, error: unknown) => {
const errorPayload = getErrorObjPayload<{ status: number }>(error);
const status = errorPayload?.status || getErrorObjStatusCode(error);
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
};
export default function useQueryClientConfig() { export default function useQueryClientConfig() {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, error) => { retry,
const errorPayload = getErrorObjPayload<{ status: number }>(error);
const status = errorPayload?.status || getErrorObjStatusCode(error);
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
},
throwOnError: (error) => { throwOnError: (error) => {
const status = getErrorObjStatusCode(error); const status = getErrorObjStatusCode(error);
// don't catch error for "Too many requests" response // don't catch error for "Too many requests" response
......
export default function throwOnAbsentParamError(param: unknown) {
if (!param) {
throw new Error('Required param not provided', { cause: { status: 404 } });
}
}
import type { ResourceError, ResourceName } from 'lib/api/resources';
type Params = ({
isError: true;
error: ResourceError<unknown>;
} | {
isError: false;
error: null;
}) & {
resource?: ResourceName;
}
export const RESOURCE_LOAD_ERROR_MESSAGE = 'Resource load error';
export default function throwOnResourceLoadError({ isError, error, resource }: Params) {
if (isError) {
throw Error(RESOURCE_LOAD_ERROR_MESSAGE, { cause: { ...error, resource } as unknown as Error });
}
}
export default function hetToDecimal(hex: string) {
const strippedHex = hex.startsWith('0x') ? hex.slice(2) : hex;
return parseInt(strippedHex, 16);
}
import _clamp from 'lodash/clamp';
import React from 'react';
import { useInView } from 'react-intersection-observer';
const STEP = 10;
const MIN_ITEMS_NUM = 50;
export default function useLazyRenderedList(list: Array<unknown>, isEnabled: boolean) {
const [ renderedItemsNum, setRenderedItemsNum ] = React.useState(MIN_ITEMS_NUM);
const { ref, inView } = useInView({
rootMargin: '200px',
triggerOnce: false,
skip: !isEnabled || list.length <= MIN_ITEMS_NUM,
});
React.useEffect(() => {
if (inView) {
setRenderedItemsNum((prev) => _clamp(prev + STEP, 0, list.length));
}
}, [ inView, list.length ]);
return { cutRef: ref, renderedItemsNum };
}
...@@ -46,6 +46,13 @@ export default function useNavItems(): ReturnType { ...@@ -46,6 +46,13 @@ export default function useNavItems(): ReturnType {
icon: 'transactions', icon: 'transactions',
isActive: pathname === '/txs' || pathname === '/tx/[hash]', isActive: pathname === '/txs' || pathname === '/tx/[hash]',
}; };
const userOps: NavItem | null = config.features.userOps.isEnabled ? {
text: 'User operations',
nextRoute: { pathname: '/ops' as const },
icon: 'user_op',
isActive: pathname === '/ops' || pathname === '/op/[hash]',
} : null;
const verifiedContracts: NavItem | null = const verifiedContracts: NavItem | null =
{ {
text: 'Verified contracts', text: 'Verified contracts',
...@@ -54,7 +61,7 @@ export default function useNavItems(): ReturnType { ...@@ -54,7 +61,7 @@ export default function useNavItems(): ReturnType {
isActive: pathname === '/verified-contracts', isActive: pathname === '/verified-contracts',
}; };
const ensLookup = config.features.nameService.isEnabled ? { const ensLookup = config.features.nameService.isEnabled ? {
text: 'ENS lookup', text: 'Name services lookup',
nextRoute: { pathname: '/name-domains' as const }, nextRoute: { pathname: '/name-domains' as const },
icon: 'ENS', icon: 'ENS',
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]', isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]',
...@@ -64,6 +71,7 @@ export default function useNavItems(): ReturnType { ...@@ -64,6 +71,7 @@ export default function useNavItems(): ReturnType {
blockchainNavItems = [ blockchainNavItems = [
[ [
txs, txs,
userOps,
blocks, blocks,
{ {
text: 'Txn batches', text: 'Txn batches',
...@@ -71,7 +79,7 @@ export default function useNavItems(): ReturnType { ...@@ -71,7 +79,7 @@ export default function useNavItems(): ReturnType {
icon: 'txn_batches', icon: 'txn_batches',
isActive: pathname === '/zkevm-l2-txn-batches' || pathname === '/zkevm-l2-txn-batch/[number]', isActive: pathname === '/zkevm-l2-txn-batches' || pathname === '/zkevm-l2-txn-batch/[number]',
}, },
], ].filter(Boolean),
[ [
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
...@@ -95,6 +103,7 @@ export default function useNavItems(): ReturnType { ...@@ -95,6 +103,7 @@ export default function useNavItems(): ReturnType {
{ text: 'Output roots', nextRoute: { pathname: '/l2-output-roots' as const }, icon: 'output_roots', isActive: pathname === '/l2-output-roots' }, { text: 'Output roots', nextRoute: { pathname: '/l2-output-roots' as const }, icon: 'output_roots', isActive: pathname === '/l2-output-roots' },
], ],
[ [
userOps,
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
ensLookup, ensLookup,
...@@ -103,6 +112,7 @@ export default function useNavItems(): ReturnType { ...@@ -103,6 +112,7 @@ export default function useNavItems(): ReturnType {
} else { } else {
blockchainNavItems = [ blockchainNavItems = [
txs, txs,
userOps,
blocks, blocks,
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
......
...@@ -39,6 +39,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -39,6 +39,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/l2-withdrawals': 'Root page', '/l2-withdrawals': 'Root page',
'/zkevm-l2-txn-batches': 'Root page', '/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page', '/zkevm-l2-txn-batch/[number]': 'Regular page',
'/ops': 'Root page',
'/op/[hash]': 'Regular page',
'/404': 'Regular page', '/404': 'Regular page',
'/name-domains': 'Root page', '/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page', '/name-domains/[name]': 'Regular page',
......
...@@ -42,6 +42,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -42,6 +42,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-withdrawals': DEFAULT_TEMPLATE, '/l2-withdrawals': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE, '/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE, '/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/ops': DEFAULT_TEMPLATE,
'/op/[hash]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE, '/404': DEFAULT_TEMPLATE,
'/name-domains': DEFAULT_TEMPLATE, '/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE,
......
...@@ -37,6 +37,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -37,6 +37,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-withdrawals': 'withdrawals (L2 > L1)', '/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches', '/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%', '/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/ops': 'user operations',
'/op/[hash]': 'user operation %hash%',
'/404': 'error - page not found', '/404': 'error - page not found',
'/name-domains': 'domains search and resolve', '/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details', '/name-domains/[name]': '%name% domain details',
......
...@@ -37,6 +37,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -37,6 +37,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/l2-withdrawals': 'Withdrawals (L2 > L1)', '/l2-withdrawals': 'Withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches', '/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details', '/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
'/ops': 'User operations',
'/op/[hash]': 'User operation details',
'/404': '404', '/404': '404',
'/name-domains': 'Domains search and resolve', '/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details', '/name-domains/[name]': 'Domain details',
......
...@@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react'; ...@@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app'; import appConfig from 'configs/app';
import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError';
const feature = appConfig.features.sentry; const feature = appConfig.features.sentry;
...@@ -59,6 +60,9 @@ export const config: Sentry.BrowserOptions | undefined = (() => { ...@@ -59,6 +60,9 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
'The quota has been exceeded', 'The quota has been exceeded',
'Attempt to connect to relay via', 'Attempt to connect to relay via',
'WebSocket connection failed for URL: wss://relay.walletconnect.com', 'WebSocket connection failed for URL: wss://relay.walletconnect.com',
// API errors
RESOURCE_LOAD_ERROR_MESSAGE,
], ],
denyUrls: [ denyUrls: [
// Facebook flakiness // Facebook flakiness
......
import type { Unit } from 'types/unit';
import config from 'configs/app';
const weiName = config.chain.currency.weiName || 'wei';
export const currencyUnits: Record<Unit, string> = {
wei: weiName,
gwei: `G${ weiName }`,
ether: config.chain.currency.symbol || 'ETH',
};
import { createPublicClient, http } from 'viem';
import currentChain from './currentChain';
export const publicClient = createPublicClient({
chain: currentChain,
transport: http(),
batch: {
multicall: true,
},
});
import type { Chain } from 'wagmi';
import config from 'configs/app';
const currentChain: Chain = {
id: Number(config.chain.id),
name: config.chain.name ?? '',
network: config.chain.name ?? '',
nativeCurrency: {
decimals: config.chain.currency.decimals,
name: config.chain.currency.name ?? '',
symbol: config.chain.currency.symbol ?? '',
},
rpcUrls: {
'public': {
http: [ config.chain.rpcUrl ?? '' ],
},
'default': {
http: [ config.chain.rpcUrl ?? '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: config.app.baseUrl,
},
},
};
export default currentChain;
import type { SearchResultToken, SearchResultBlock, SearchResultAddressOrContract, SearchResultTx, SearchResultLabel, SearchResult } from 'types/api/search'; import type {
SearchResultToken,
SearchResultBlock,
SearchResultAddressOrContract,
SearchResultTx,
SearchResultLabel,
SearchResult,
SearchResultUserOp,
} from 'types/api/search';
export const token1: SearchResultToken = { export const token1: SearchResultToken = {
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B', address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
...@@ -101,6 +109,13 @@ export const tx1: SearchResultTx = { ...@@ -101,6 +109,13 @@ export const tx1: SearchResultTx = {
url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd', url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
}; };
export const userOp1: SearchResultUserOp = {
timestamp: '2024-01-11T14:15:48.000000Z',
type: 'user_operation',
user_operation_hash: '0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61',
url: '/op/0xcb560d77b0f3af074fa05c1e5c691bcdfe457e630062b5907e9e71fc74b2ec61',
};
export const baseResponse: SearchResult = { export const baseResponse: SearchResult = {
items: [ items: [
token1, token1,
......
import type { UserOp } from 'types/api/userOps';
export const userOpData: UserOp = {
timestamp: '2024-01-19T12:42:12.000000Z',
transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a',
user_logs_start_index: 40,
fee: '187125856691380',
call_gas_limit: '26624',
gas: '258875',
status: true,
aggregator_signature: null,
block_hash: '0xff5f41ec89e5fb3dfcf103bbbd67469fed491a7dd7cffdf00bd9e3bf45d8aeab',
pre_verification_gas: '48396',
factory: null,
signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b',
verification_gas_limit: '61285',
max_fee_per_gas: '1575000898',
aggregator: null,
hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83',
gas_price: '1575000898',
user_logs_count: 1,
block_number: '10399597',
gas_used: '118810',
sender: {
ens_domain_name: null,
hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
},
nonce: '0x000000000000000000000000000000000000000000000000000000000000004f',
entry_point: {
ens_domain_name: null,
hash: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
},
sponsor_type: 'paymaster_sponsor',
raw: {
// eslint-disable-next-line max-len
call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000',
call_gas_limit: '26624',
init_code: '0x',
max_fee_per_gas: '1575000898',
max_priority_fee_per_gas: '1575000898',
nonce: '79',
// eslint-disable-next-line max-len
paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b',
pre_verification_gas: '48396',
sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
signature: '0x2b95a173c1ea314d2c387e0d84194d221c14805e02157b7cefaf607a53e9081c0099ccbeaa1020ab91b862d4a4743dc1e20b4953f5bb6c13afeac760cef34fd11b',
verification_gas_limit: '61285',
},
max_priority_fee_per_gas: '1575000898',
revert_reason: null,
bundler: {
ens_domain_name: null,
hash: '0xd53Eb5203e367BbDD4f72338938224881Fc501Ab',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
},
// eslint-disable-next-line max-len
call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000',
paymaster: {
ens_domain_name: null,
hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
},
};
import type { UserOpsResponse } from 'types/api/userOps';
export const userOpsData: UserOpsResponse = {
items: [
{
address: {
ens_domain_name: null,
hash: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
},
block_number: '10399597',
fee: '187125856691380',
hash: '0xe72500491b3f2549ac53bd9de9dbb1d2edfc33cdddf5c079d6d64dfec650ef83',
status: true,
timestamp: '2022-01-19T12:42:12.000000Z',
transaction_hash: '0x715fe1474ac7bea3d6f4a03b1c5b6d626675fb0b103be29f849af65e9f1f9c6a',
},
{
address:
{ ens_domain_name: null,
hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
},
block_number: '10399596',
fee: '381895502291373',
hash: '0xcb945ae86608bdc88c3318245403c81a880fcb1e49fef18ac59477761c056cea',
status: false,
timestamp: '2022-01-19T12:42:00.000000Z',
transaction_hash: '0x558d699e7cbc235461d48ed04b8c3892d598a4000f20851760d00dc3513c2e48',
},
{
address: {
ens_domain_name: null,
hash: '0x2c298CcaFFD1549e1C21F46966A6c236fCC66dB2',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
},
block_number: '10399560',
fee: '165019501210143',
hash: '0x84c1270b12af3f0ffa204071f1bf503ebf9b1ccf6310680383be5a2b6fd1d8e5',
status: true,
timestamp: '2022-01-19T12:32:00.000000Z',
transaction_hash: '0xc4c1c38680ec63139411aa2193275e8de44be15217c4256db9473bf0ea2b6264',
},
],
next_page_params: {
page_size: 50,
page_token: '10396582,0x9bf4d2a28813c5c244884cb20cdfe01dabb3f927234ae961eab6e38502de7a28',
},
};
...@@ -147,3 +147,13 @@ export const accounts: GetServerSideProps<Props> = async(context) => { ...@@ -147,3 +147,13 @@ export const accounts: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const userOps: GetServerSideProps<Props> = async(context) => {
if (!config.features.userOps.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
...@@ -39,6 +39,8 @@ declare module "nextjs-routes" { ...@@ -39,6 +39,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/login"> | StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }> | DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains"> | StaticRoute<"/name-domains">
| DynamicRoute<"/op/[hash]", { "hash": string }>
| StaticRoute<"/ops">
| StaticRoute<"/search-results"> | StaticRoute<"/search-results">
| StaticRoute<"/stats"> | StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]", { "hash": string }>
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
"private": false, "private": false,
"homepage": "https://github.com/blockscout/frontend#readme", "homepage": "https://github.com/blockscout/frontend#readme",
"engines": { "engines": {
"node": "18", "node": "20.11.0",
"npm": "8" "npm": "10.2.4"
}, },
"scripts": { "scripts": {
"dev": "./tools/scripts/dev.sh", "dev": "./tools/scripts/dev.sh",
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
"svg:build-sprite": "icons build -i ./icons -o ./public/icons --optimize", "svg:build-sprite": "icons build -i ./icons -o ./public/icons --optimize",
"test:pw": "./tools/scripts/pw.sh", "test:pw": "./tools/scripts/pw.sh",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.35.1-focal ./tools/scripts/pw.docker.sh", "test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.sh",
"test:pw:ci": "yarn test:pw --project=$PW_PROJECT", "test:pw:ci": "yarn test:pw --project=$PW_PROJECT",
"test:jest": "jest", "test:jest": "jest",
"test:jest:watch": "jest --watch", "test:jest:watch": "jest --watch",
...@@ -99,8 +99,8 @@ ...@@ -99,8 +99,8 @@
"xss": "^1.0.14" "xss": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "1.35.1", "@playwright/experimental-ct-react": "1.41.1",
"@playwright/test": "^1.35.1", "@playwright/test": "1.41.1",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@tanstack/eslint-plugin-query": "^5.0.5", "@tanstack/eslint-plugin-query": "^5.0.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
"@types/jest": "^29.2.0", "@types/jest": "^29.2.0",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/mixpanel-browser": "^2.38.1", "@types/mixpanel-browser": "^2.38.1",
"@types/node": "18.11.18", "@types/node": "20.11.0",
"@types/phoenix": "^1.5.4", "@types/phoenix": "^1.5.4",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/react": "18.0.9", "@types/react": "18.0.9",
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
const UserOp = dynamic(() => import('ui/pages/UserOp'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/op/[hash]" query={ props }>
<UserOp/>
</PageNextJs>
);
};
export default Page;
export { userOps as getServerSideProps } from 'nextjs/getServerSideProps';
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const UserOps = dynamic(() => import('ui/pages/UserOps'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/ops">
<UserOps/>
</PageNextJs>
);
};
export default Page;
export { userOps as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -36,6 +36,9 @@ export const featureEnvs = { ...@@ -36,6 +36,9 @@ export const featureEnvs = {
{ name: 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK', value: 'true' }, { name: 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' }, { name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
], ],
userOps: [
{ name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' },
],
}; };
export const viewsEnvs = { export const viewsEnvs = {
......
// This file is generated by npm run build:icons // This file is generated by npm run build:icons
export type IconName = export type IconName =
| "ABI" | "ABI_slim"
| "ABI"
| "API" | "API"
| "apps" | "apps"
| "arrows/down-right" | "arrows/down-right"
...@@ -27,6 +28,7 @@ ...@@ -27,6 +28,7 @@
| "discussions" | "discussions"
| "docs" | "docs"
| "donate" | "donate"
| "dots"
| "edit" | "edit"
| "email-sent" | "email-sent"
| "email" | "email"
...@@ -125,10 +127,11 @@ ...@@ -125,10 +127,11 @@
| "txn_batches" | "txn_batches"
| "unfinalized" | "unfinalized"
| "uniswap" | "uniswap"
| "user_op_slim"
| "user_op"
| "verified_token" | "verified_token"
| "verified" | "verified"
| "verify-contract" | "verify-contract"
| "vertical_dots"
| "wallet" | "wallet"
| "wallets/coinbase" | "wallets/coinbase"
| "wallets/metamask" | "wallets/metamask"
......
import type { Chain, GetBlockReturnType, GetTransactionReturnType, TransactionReceipt, Withdrawal } from 'viem';
import { ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx';
export const WITHDRAWAL: Withdrawal = {
index: '0x1af95d9',
validatorIndex: '0x7d748',
address: '0x9b52b9033ecbb6635f1c31a646d5691b282878aa',
amount: '0x29e16b',
};
export const GET_TRANSACTION: GetTransactionReturnType<Chain, 'latest'> = {
blockHash: BLOCK_HASH,
blockNumber: BigInt(10361367),
from: ADDRESS_HASH,
gas: BigInt(800000),
maxPriorityFeePerGas: BigInt(2),
maxFeePerGas: BigInt(14),
hash: TX_HASH,
input: '0x7898e0',
nonce: 117694,
to: ADDRESS_HASH,
transactionIndex: 60,
value: BigInt(42),
type: 'eip1559',
accessList: [],
chainId: 5,
v: BigInt(0),
r: '0x2c5022ff7f78a22f1a99afbd568f75cb52812189ed8c264c8310e0b8dba2c8a8',
s: '0x50938f87c92b9eeb9777507ca8f7397840232d00d1dbac3edac6c115b4656763',
yParity: 1,
typeHex: '0x2',
};
export const GET_TRANSACTION_RECEIPT: TransactionReceipt = {
blockHash: BLOCK_HASH,
blockNumber: BigInt(10361367),
contractAddress: null,
cumulativeGasUsed: BigInt(39109),
effectiveGasPrice: BigInt(13),
from: ADDRESS_HASH,
gasUsed: BigInt(39109),
logs: [],
logsBloom: '0x0',
status: 'success',
to: ADDRESS_HASH,
transactionHash: TX_HASH,
transactionIndex: 60,
type: '0x2',
};
export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420);
export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = {
baseFeePerGas: BigInt(11),
difficulty: BigInt(111),
extraData: '0xd8830',
gasLimit: BigInt(800000),
gasUsed: BigInt(42000),
hash: BLOCK_HASH,
logsBloom: '0x008000',
miner: ADDRESS_HASH,
mixHash: BLOCK_HASH,
nonce: '0x0000000000000000',
number: BigInt(10361367),
parentHash: BLOCK_HASH,
receiptsRoot: BLOCK_HASH,
sha3Uncles: BLOCK_HASH,
size: BigInt(88),
stateRoot: BLOCK_HASH,
timestamp: BigInt(1628580000),
totalDifficulty: BigInt(10361367),
transactions: [
TX_HASH,
],
transactionsRoot: TX_HASH,
uncles: [],
withdrawals: Array(10).fill(WITHDRAWAL),
withdrawalsRoot: TX_HASH,
sealFields: [ '0x00' ],
};
export const GET_BLOCK_WITH_TRANSACTIONS: GetBlockReturnType<Chain, true, 'latest'> = {
...GET_BLOCK,
transactions: Array(50).fill(GET_TRANSACTION),
};
import type { UserOpsItem, UserOp, UserOpsAccount } from 'types/api/userOps';
import { ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx';
const USER_OP_HASH = '0xb94fab8f31f83001a23e20b2ce3cdcfb284c57a64b9a073e0e09c018bc701978';
export const USER_OPS_ITEM: UserOpsItem = {
hash: USER_OP_HASH,
block_number: '10356381',
transaction_hash: TX_HASH,
address: ADDRESS_HASH,
timestamp: '2023-12-18T10:48:49.000000Z',
status: true,
fee: '48285720012071430',
};
export const USER_OP: UserOp = {
hash: USER_OP_HASH,
sender: ADDRESS_HASH,
nonce: '0x00b',
call_data: '0x123',
call_gas_limit: '71316',
verification_gas_limit: '91551',
pre_verification_gas: '53627',
max_fee_per_gas: '100000020',
max_priority_fee_per_gas: '100000000',
signature: '0x000',
aggregator: null,
aggregator_signature: null,
entry_point: ADDRESS_HASH,
transaction_hash: TX_HASH,
block_number: '10358181',
block_hash: BLOCK_HASH,
bundler: ADDRESS_HASH,
factory: null,
paymaster: ADDRESS_HASH,
status: true,
revert_reason: null,
gas: '399596',
gas_price: '1575000898',
gas_used: '118810',
sponsor_type: 'paymaster_sponsor',
fee: '17927001792700',
timestamp: '2023-12-18T10:48:49.000000Z',
user_logs_count: 1,
user_logs_start_index: 2,
raw: {
sender: ADDRESS_HASH,
nonce: '1',
init_code: '0x',
call_data: '0x345',
call_gas_limit: '29491',
verification_gas_limit: '80734',
pre_verification_gas: '3276112',
max_fee_per_gas: '309847206',
max_priority_fee_per_gas: '100000000',
paymaster_and_data: '0x',
signature: '0x000',
},
};
export const USER_OPS_ACCOUNT: UserOpsAccount = {
total_ops: 1,
};
...@@ -56,10 +56,10 @@ const variantSubtle = definePartsStyle((props) => { ...@@ -56,10 +56,10 @@ const variantSubtle = definePartsStyle((props) => {
return { return {
container: { container: {
[$fg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.800' : `colors.${ colorScheme }.500`, [$fg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.800' : `colors.${ colorScheme }.500`,
[$bg.variable]: colorScheme === 'gray' ? 'colors.gray.100' : bg.light, [$bg.variable]: colorScheme === 'gray' ? 'colors.blackAlpha.100' : bg.light,
_dark: { _dark: {
[$fg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.800' : `colors.${ colorScheme }.200`, [$fg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.800' : `colors.${ colorScheme }.200`,
[$bg.variable]: colorScheme === 'gray' ? 'colors.gray.800' : bg.dark, [$bg.variable]: colorScheme === 'gray' ? 'colors.whiteAlpha.200' : bg.dark,
}, },
}, },
}; };
......
...@@ -15,7 +15,7 @@ export interface UserTags { ...@@ -15,7 +15,7 @@ export interface UserTags {
public_tags: Array<AddressTag> | null; public_tags: Array<AddressTag> | null;
} }
export interface AddressParam extends UserTags { export type AddressParamBasic = {
hash: string; hash: string;
implementation_name: string | null; implementation_name: string | null;
name: string | null; name: string | null;
...@@ -23,3 +23,5 @@ export interface AddressParam extends UserTags { ...@@ -23,3 +23,5 @@ export interface AddressParam extends UserTags {
is_verified: boolean | null; is_verified: boolean | null;
ens_domain_name: string | null; ens_domain_name: string | null;
} }
export type AddressParam = UserTags & AddressParamBasic;
...@@ -13,7 +13,7 @@ export interface Block { ...@@ -13,7 +13,7 @@ export interface Block {
hash: string; hash: string;
parent_hash: string; parent_hash: string;
difficulty: string; difficulty: string;
total_difficulty: string; total_difficulty: string | null;
gas_used: string | null; gas_used: string | null;
gas_limit: string; gas_limit: string;
nonce: string; nonce: string;
...@@ -69,7 +69,7 @@ export type BlockWithdrawalsResponse = { ...@@ -69,7 +69,7 @@ export type BlockWithdrawalsResponse = {
next_page_params: { next_page_params: {
index: number; index: number;
items_count: number; items_count: number;
}; } | null;
} }
export type BlockWithdrawalsItem = { export type BlockWithdrawalsItem = {
......
...@@ -55,7 +55,14 @@ export interface SearchResultTx { ...@@ -55,7 +55,14 @@ export interface SearchResultTx {
url?: string; // not used by the frontend, we build the url ourselves url?: string; // not used by the frontend, we build the url ourselves
} }
export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel; export interface SearchResultUserOp {
type: 'user_operation';
user_operation_hash: string;
timestamp: string;
url?: string; // not used by the frontend, we build the url ourselves
}
export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx | SearchResultLabel | SearchResultUserOp;
export interface SearchResult { export interface SearchResult {
items: Array<SearchResultItem>; items: Array<SearchResultItem>;
...@@ -79,5 +86,5 @@ export interface SearchResultFilters { ...@@ -79,5 +86,5 @@ export interface SearchResultFilters {
export interface SearchRedirectResult { export interface SearchRedirectResult {
parameter: string | null; parameter: string | null;
redirect: boolean; redirect: boolean;
type: 'address' | 'block' | 'transaction' | null; type: 'address' | 'block' | 'transaction' | 'user_operation' | null;
} }
...@@ -26,14 +26,14 @@ export type Transaction = { ...@@ -26,14 +26,14 @@ export type Transaction = {
hash: string; hash: string;
result: string; result: string;
confirmations: number; confirmations: number;
status: 'ok' | 'error' | null; status: 'ok' | 'error' | null | undefined;
block: number | null; block: number | null;
timestamp: string | null; timestamp: string | null;
confirmation_duration: Array<number>; confirmation_duration: Array<number> | null;
from: AddressParam; from: AddressParam;
value: string; value: string;
fee: Fee; fee: Fee;
gas_price: string; gas_price: string | null;
type: number | null; type: number | null;
gas_used: string | null; gas_used: string | null;
gas_limit: string; gas_limit: string;
...@@ -49,7 +49,7 @@ export type Transaction = { ...@@ -49,7 +49,7 @@ export type Transaction = {
decoded_input: DecodedInput | null; decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null; token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean; token_transfers_overflow: boolean;
exchange_rate: string; exchange_rate: string | null;
method: string | null; method: string | null;
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
......
import type { AddressParamBasic } from './addressParams';
export type UserOpsItem = {
hash: string;
block_number: string;
transaction_hash: string;
address: string | AddressParamBasic;
timestamp: string;
status: boolean;
fee: string;
}
export type UserOpsResponse = {
items: Array<UserOpsItem>;
next_page_params: {
page_token: string;
page_size: number;
} | null;
}
export type UserOpSponsorType = 'paymaster_hybrid' | 'paymaster_sponsor' | 'wallet_balance' | 'wallet_deposit';
export type UserOp = {
hash: string;
sender: string | AddressParamBasic;
status: boolean;
revert_reason: string | null;
timestamp: string | null;
fee: string;
gas: string;
transaction_hash: string;
block_number: string;
block_hash: string;
entry_point: string | AddressParamBasic;
call_gas_limit: string;
verification_gas_limit: string;
pre_verification_gas: string;
max_fee_per_gas: string;
max_priority_fee_per_gas: string;
aggregator: string | null;
aggregator_signature: string | null;
bundler: string | AddressParamBasic;
factory: string | null;
paymaster: string | AddressParamBasic | null;
sponsor_type: UserOpSponsorType;
signature: string;
nonce: string;
call_data: string;
user_logs_start_index: number;
user_logs_count: number;
raw: {
call_data: string;
call_gas_limit: string;
init_code: string;
max_fee_per_gas: string;
max_priority_fee_per_gas: string;
nonce: string;
paymaster_and_data: string;
pre_verification_gas: string;
sender: string;
signature: string;
verification_gas_limit: string;
};
gas_price: string;
gas_used: string;
}
export type UserOpsFilters = {
transaction_hash?: string;
sender?: string;
}
export type UserOpsAccount = {
total_ops: number;
}
export interface ContractCodeIde {
title: string;
url: string;
icon_url: string;
}
...@@ -10,6 +10,7 @@ import config from 'configs/app'; ...@@ -10,6 +10,7 @@ import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units';
import { BLOCK } from 'stubs/block'; import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
...@@ -95,7 +96,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -95,7 +96,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
<Th width="16%">Txn</Th> <Th width="16%">Txn</Th>
<Th width="25%">Gas used</Th> <Th width="25%">Gas used</Th>
{ !config.UI.views.block.hiddenFields?.total_reward && { !config.UI.views.block.hiddenFields?.total_reward &&
<Th width="25%" isNumeric>Reward { config.chain.currency.symbol }</Th> } <Th width="25%" isNumeric>Reward { currencyUnits.ether }</Th> }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address'; ...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address'; import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
...@@ -68,7 +69,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -68,7 +69,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422; const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) { if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error }); throwOnResourceLoadError(addressQuery);
} }
if (addressQuery.isError && !is404Error) { if (addressQuery.isError && !is404Error) {
......
import { useRouter } from 'next/router';
import React from 'react';
import getQueryParamString from 'lib/router/getQueryParamString';
import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import UserOpsContent from 'ui/userOps/UserOpsContent';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressUserOps = ({ scrollRef }: Props) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const userOpsQuery = useQueryWithPages({
resourceName: 'user_ops',
scrollRef,
options: {
enabled: Boolean(hash),
placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 50, { next_page_params: {
page_token: '10355938,0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
page_size: 50,
} }),
},
filters: { sender: hash },
});
return <UserOpsContent query={ userOpsQuery } showSender={ false }/>;
};
export default AddressUserOps;
...@@ -112,7 +112,7 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -112,7 +112,7 @@ const SolidityscanReport = ({ className, hash }: Props) => {
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData } borderRadius="base">
<Button <Button
className={ className } className={ className }
color={ scoreColor } color={ scoreColor }
...@@ -122,7 +122,7 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -122,7 +122,7 @@ const SolidityscanReport = ({ className, hash }: Props) => {
onClick={ onToggle } onClick={ onToggle }
aria-label="SolidityScan score" aria-label="SolidityScan score"
fontWeight={ 500 } fontWeight={ 500 }
px={ 2 } px="6px"
h="32px" h="32px"
flexShrink={ 0 } flexShrink={ 0 }
> >
......
...@@ -7,6 +7,7 @@ import type { Block } from 'types/api/block'; ...@@ -7,6 +7,7 @@ import type { Block } from 'types/api/block';
import config from 'configs/app'; import config from 'configs/app';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { currencyUnits } from 'lib/units';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
...@@ -50,7 +51,7 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -50,7 +51,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex> </Flex>
{ !config.UI.views.block.hiddenFields?.total_reward && ( { !config.UI.views.block.hiddenFields?.total_reward && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { config.chain.currency.symbol }</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
</Flex> </Flex>
) } ) }
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { currencyUnits } from 'lib/units';
import ChartWidget from 'ui/shared/chart/ChartWidget'; import ChartWidget from 'ui/shared/chart/ChartWidget';
interface Props { interface Props {
...@@ -26,7 +27,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { ...@@ -26,7 +27,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
items={ items } items={ items }
isLoading={ isPending } isLoading={ isPending }
h="300px" h="300px"
units={ config.chain.currency.symbol } units={ currencyUnits.ether }
/> />
); );
}; };
......
...@@ -5,8 +5,8 @@ import React from 'react'; ...@@ -5,8 +5,8 @@ import React from 'react';
import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import type { PaginationParams } from 'ui/shared/pagination/types'; import type { PaginationParams } from 'ui/shared/pagination/types';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import { currencyUnits } from 'lib/units';
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';
...@@ -32,7 +32,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -32,7 +32,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<Th width="20%">Block</Th> <Th width="20%">Block</Th>
<Th width="20%">Txn</Th> <Th width="20%">Txn</Th>
<Th width="20%">Age</Th> <Th width="20%">Age</Th>
<Th width="20%" isNumeric pr={ 1 }>Balance { config.chain.currency.symbol }</Th> <Th width="20%" isNumeric pr={ 1 }>Balance { currencyUnits.ether }</Th>
<Th width="20%" isNumeric>Delta</Th> <Th width="20%" isNumeric>Delta</Th>
</Tr> </Tr>
</Thead> </Thead>
......
...@@ -4,9 +4,9 @@ import React from 'react'; ...@@ -4,9 +4,9 @@ import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import config from 'configs/app';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { currencyUnits } from 'lib/units';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -25,7 +25,7 @@ const AddressCoinBalanceListItem = (props: Props) => { ...@@ -25,7 +25,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
<ListItemMobile rowGap={ 2 } isAnimated> <ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 600 }> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 600 }>
{ BigNumber(props.value).div(WEI).dp(8).toFormat() } { config.chain.currency.symbol } { BigNumber(props.value).div(WEI).dp(8).toFormat() } { currencyUnits.ether }
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !props.isLoading }> <Skeleton isLoaded={ !props.isLoading }>
<Stat flexGrow="0"> <Stat flexGrow="0">
......
...@@ -122,6 +122,9 @@ test('verified with multiple sources', async({ mount, page }) => { ...@@ -122,6 +122,9 @@ test('verified with multiple sources', async({ mount, page }) => {
await page.getByRole('button', { name: 'View external libraries' }).click(); await page.getByRole('button', { name: 'View external libraries' }).click();
await expect(section).toHaveScreenshot(); await expect(section).toHaveScreenshot();
await page.getByRole('button', { name: 'Open source code in IDE' }).click();
await expect(section).toHaveScreenshot();
}); });
test('verified via sourcify', async({ mount, page }) => { test('verified via sourcify', async({ mount, page }) => {
......
import { Flex, Button, chakra, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Image, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
interface Props {
className?: string;
hash: string;
}
const ContractCodeIde = ({ className, hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const defaultIconColor = useColorModeValue('gray.600', 'gray.500');
const ideLinks = React.useMemo(() => {
return config.UI.ides.items
.map((ide) => {
const url = decodeURIComponent(ide.url.replace('{hash}', hash).replace('{domain}', config.app.host || ''));
const icon = 'icon_url' in ide ?
<Image boxSize={ 5 } mr={ 2 } src={ ide.icon_url } alt={ `${ ide.title } icon` }/> :
<IconSvg name="ABI_slim" boxSize={ 5 } color={ defaultIconColor } mr={ 2 }/>;
return (
<LinkExternal key={ ide.title } href={ url } display="inline-flex" alignItems="center">
{ icon }
{ ide.title }
</LinkExternal>
);
});
}, [ defaultIconColor, hash ]);
if (ideLinks.length === 0) {
return null;
}
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Button
className={ className }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Open source code in IDE"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<span>Open in</span>
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/>
</Button>
</PopoverTrigger>
<PopoverContent w="240px">
<PopoverBody >
<chakra.span color="text_secondary" fontSize="xs">Redactors</chakra.span>
<Flex
flexDir="column"
alignItems="flex-start"
columnGap={ 6 }
rowGap={ 3 }
mt={ 3 }
>
{ ideLinks }
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(chakra(ContractCodeIde));
import { Alert, Button, Flex } from '@chakra-ui/react'; import { Alert, Button, Flex } from '@chakra-ui/react';
import { useWeb3Modal, useWeb3ModalState } from '@web3modal/wagmi/react';
import React from 'react'; import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import useWallet from 'ui/snippets/walletMenu/useWallet';
const ContractConnectWallet = () => { const ContractConnectWallet = () => {
const { open } = useWeb3Modal(); const { isModalOpening, isModalOpen, connect, disconnect, address, isWalletConnected } = useWallet({ source: 'Smart contracts' });
const { open: isOpen } = useWeb3ModalState();
const { disconnect } = useDisconnect();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
const handleConnect = React.useCallback(async() => {
setIsModalOpening(true);
await open();
setIsModalOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Smart contracts', Status: 'Started' });
}, [ open ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
!isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Smart contracts', Status: 'Connected' });
}, []);
const handleDisconnect = React.useCallback(() => {
disconnect();
}, [ disconnect ]);
const { address, isDisconnected } = useAccount({ onConnect: handleAccountConnected });
const content = (() => { const content = (() => {
if (isDisconnected || !address) { if (!isWalletConnected) {
return ( return (
<> <>
<span>Disconnected</span> <span>Disconnected</span>
<Button <Button
ml={ 3 } ml={ 3 }
onClick={ handleConnect } onClick={ connect }
size="sm" size="sm"
variant="outline" variant="outline"
isLoading={ isModalOpening || isOpen } isLoading={ isModalOpening || isModalOpen }
loadingText="Connect wallet" loadingText="Connect wallet"
> >
Connect wallet Connect wallet
...@@ -61,7 +39,7 @@ const ContractConnectWallet = () => { ...@@ -61,7 +39,7 @@ const ContractConnectWallet = () => {
ml={ 2 } ml={ 2 }
/> />
</Flex> </Flex>
<Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button> <Button onClick={ disconnect } size="sm" variant="outline">Disconnect</Button>
</Flex> </Flex>
); );
})(); })();
......
...@@ -39,7 +39,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ...@@ -39,7 +39,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
return [ return [
...('inputs' in data ? data.inputs : []), ...('inputs' in data ? data.inputs : []),
...('stateMutability' in data && data.stateMutability === 'payable' ? [ { ...('stateMutability' in data && data.stateMutability === 'payable' ? [ {
name: `Send native ${ config.chain.currency.symbol }`, name: `Send native ${ config.chain.currency.symbol || 'coin' }`,
type: 'uint256' as const, type: 'uint256' as const,
internalType: 'uint256' as const, internalType: 'uint256' as const,
fieldType: 'native_coin' as const, fieldType: 'native_coin' as const,
......
...@@ -5,8 +5,8 @@ import React from 'react'; ...@@ -5,8 +5,8 @@ import React from 'react';
import type { SmartContractMethodOutput } from 'types/api/contract'; import type { SmartContractMethodOutput } from 'types/api/contract';
import config from 'configs/app';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import { currencyUnits } from 'lib/units';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
function castValueToString(value: number | string | boolean | object | bigint | undefined): string { function castValueToString(value: number | string | boolean | object | bigint | undefined): string {
...@@ -32,17 +32,17 @@ interface Props { ...@@ -32,17 +32,17 @@ interface Props {
const ContractMethodStatic = ({ data }: Props) => { const ContractMethodStatic = ({ data }: Props) => {
const [ value, setValue ] = React.useState<string>(castValueToString(data.value)); const [ value, setValue ] = React.useState<string>(castValueToString(data.value));
const [ label, setLabel ] = React.useState('WEI'); const [ label, setLabel ] = React.useState(currencyUnits.wei.toUpperCase());
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const initialValue = castValueToString(data.value); const initialValue = castValueToString(data.value);
if (event.target.checked) { if (event.target.checked) {
setValue(BigNumber(initialValue).div(WEI).toFixed()); setValue(BigNumber(initialValue).div(WEI).toFixed());
setLabel(config.chain.currency.symbol || 'ETH'); setLabel(currencyUnits.ether.toUpperCase());
} else { } else {
setValue(BigNumber(initialValue).toFixed()); setValue(BigNumber(initialValue).toFixed());
setLabel('WEI'); setLabel(currencyUnits.wei.toUpperCase());
} }
}, [ data.value ]); }, [ data.value ]);
......
...@@ -13,6 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -13,6 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor'; import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath'; import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
import ContractCodeIdes from './ContractCodeIdes';
import ContractExternalLibraries from './ContractExternalLibraries'; import ContractExternalLibraries from './ContractExternalLibraries';
const SOURCE_CODE_OPTIONS = [ const SOURCE_CODE_OPTIONS = [
...@@ -116,6 +117,8 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { ...@@ -116,6 +117,8 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}/> : <CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}/> :
null; null;
const ides = sourceType === 'secondary' ? <ContractCodeIdes hash={ implementationAddress }/> : <ContractCodeIdes hash={ address }/>;
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSourceType(event.target.value as SourceCodeType); setSourceType(event.target.value as SourceCodeType);
}, []); }, []);
...@@ -188,6 +191,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => { ...@@ -188,6 +191,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
{ editorSourceTypeSelector } { editorSourceTypeSelector }
{ externalLibraries } { externalLibraries }
{ diagramLink } { diagramLink }
{ ides }
{ copyToClipboard } { copyToClipboard }
</Flex> </Flex>
{ content } { content }
......
...@@ -129,7 +129,8 @@ function castValue(value: string, type: SmartContractMethodArgType) { ...@@ -129,7 +129,8 @@ function castValue(value: string, type: SmartContractMethodArgType) {
} }
const isNestedArray = (type.match(/\[/g) || []).length > 1; const isNestedArray = (type.match(/\[/g) || []).length > 1;
if (isNestedArray) { const isNestedTuple = type.includes('tuple');
if (isNestedArray || isNestedTuple) {
return parseArrayValue(value) || value; return parseArrayValue(value) || value;
} }
......
...@@ -8,6 +8,7 @@ import config from 'configs/app'; ...@@ -8,6 +8,7 @@ import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
...@@ -66,7 +67,7 @@ const AddressBalance = ({ data, isLoading }: Props) => { ...@@ -66,7 +67,7 @@ const AddressBalance = ({ data, isLoading }: Props) => {
return ( return (
<DetailsInfoItem <DetailsInfoItem
title="Balance" title="Balance"
hint={ `Address balance in ${ config.chain.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens` } hint={ `Address balance in ${ currencyUnits.ether }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap" flexWrap="nowrap"
alignItems="flex-start" alignItems="flex-start"
isLoading={ isLoading } isLoading={ isLoading }
...@@ -75,7 +76,7 @@ const AddressBalance = ({ data, isLoading }: Props) => { ...@@ -75,7 +76,7 @@ const AddressBalance = ({ data, isLoading }: Props) => {
value={ data.coin_balance || '0' } value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate } exchangeRate={ data.exchange_rate }
decimals={ String(config.chain.currency.decimals) } decimals={ String(config.chain.currency.decimals) }
currency={ config.chain.currency.symbol } currency={ currencyUnits.ether }
accuracyUsd={ 2 } accuracyUsd={ 2 }
accuracy={ 8 } accuracy={ 8 }
flexWrap="wrap" flexWrap="wrap"
......
...@@ -31,7 +31,7 @@ test('base view', async({ mount, page }) => { ...@@ -31,7 +31,7 @@ test('base view', async({ mount, page }) => {
</TestApp>, </TestApp>,
); );
await component.getByText('4 domains').click(); await component.getByText('4').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 550, height: 350 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 550, height: 350 } });
}); });
import { Button, chakra, Flex, Grid, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Button, chakra, Flex, Grid, Hide, Popover, PopoverBody, PopoverContent, PopoverTrigger, Show, Skeleton, useDisclosure } from '@chakra-ui/react';
import _clamp from 'lodash/clamp'; import _clamp from 'lodash/clamp';
import React from 'react'; import React from 'react';
...@@ -50,7 +50,7 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => { ...@@ -50,7 +50,7 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
} }
if (isPending) { if (isPending) {
return <Skeleton h={ 8 } w={{ base: '60px', lg: '120px' }} borderRadius="base"/>; return <Skeleton h={ 8 } w={{ base: '50px', xl: '120px' }} borderRadius="base"/>;
} }
if (data.items.length === 0) { if (data.items.length === 0) {
...@@ -95,15 +95,19 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => { ...@@ -95,15 +95,19 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
variant="outline" variant="outline"
colorScheme="gray" colorScheme="gray"
onClick={ onToggle } onClick={ onToggle }
aria-label="Address ENS domains" aria-label="Address domains"
fontWeight={ 500 } fontWeight={ 500 }
px={ 2 } px={ 2 }
h="32px" h="32px"
flexShrink={ 0 } flexShrink={ 0 }
> >
<IconSvg name="ENS_slim" boxSize={ 5 }/> <IconSvg name="ENS_slim" boxSize={ 5 }/>
<chakra.span ml={ 1 } display={{ base: 'none', lg: 'block' }}>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span> <Show above="xl">
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/> <chakra.span ml={ 1 }>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span>
</Show>
<Hide above="xl">
<chakra.span ml={ 1 }>{ totalRecords }</chakra.span>
</Hide>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '500px' }}> <PopoverContent w={{ base: '100vw', lg: '500px' }}>
......
...@@ -6,6 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -6,6 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import config from 'configs/app'; import config from 'configs/app';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo'; import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
...@@ -68,7 +69,7 @@ const TxInternalsListItem = ({ ...@@ -68,7 +69,7 @@ const TxInternalsListItem = ({
w="100%" w="100%"
/> />
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { config.chain.currency.symbol }</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW={ 6 }> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW={ 6 }>
<span>{ BigNumber(value).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() }</span> <span>{ BigNumber(value).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() }</span>
</Skeleton> </Skeleton>
......
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.
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.
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.
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