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

Merge branch 'main' into category-tabs

parents b352e18e 878cb938
{
"name": "blockscout dev",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
"image": "mcr.microsoft.com/devcontainers/typescript-node:20",
"forwardPorts": [ 3000 ],
"customizations": {
"vscode": {
......
......@@ -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') }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
......@@ -57,16 +57,16 @@ jobs:
needs: [ code_quality ]
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
......@@ -94,16 +94,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
......@@ -122,7 +122,7 @@ jobs:
needs: [ code_quality, envs_validation ]
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.35.1-focal
image: mcr.microsoft.com/playwright:v1.41.1-focal
strategy:
fail-fast: false
......@@ -134,18 +134,18 @@ jobs:
run: apt-get update && apt-get install git-lfs
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
lfs: 'true'
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
......@@ -164,7 +164,7 @@ jobs:
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.project }}
path: playwright-report
......
......@@ -16,16 +16,16 @@ jobs:
if: ${{ github.ref_type == 'tag' }}
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
......
# *****************************
# *** 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.
RUN apk add --no-cache libc6-compat
......@@ -10,7 +10,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN apk add git
RUN yarn --frozen-lockfile --ignore-optional
RUN yarn --frozen-lockfile
### FEATURE REPORTER
......@@ -30,7 +30,7 @@ RUN yarn --frozen-lockfile
# *****************************
# ****** 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
# pass commit sha and git tag to the app image
......@@ -78,7 +78,7 @@ RUN cd ./deploy/tools/envs-validator && yarn build
# ******* STAGE 3: Run ********
# *****************************
# 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
### APP
......
......@@ -8,6 +8,7 @@ const chain = Object.freeze({
shortName: getEnvValue('NEXT_PUBLIC_NETWORK_SHORT_NAME'),
currency: {
name: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_NAME'),
weiName: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME'),
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'),
decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS,
},
......
......@@ -20,6 +20,7 @@ export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats';
export { default as suave } from './suave';
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 web3Wallet } from './web3Wallet';
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 type { ChainIndicatorId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks';
......@@ -66,6 +67,9 @@ const UI = Object.freeze({
explorers: {
items: parseEnvJson<Array<NetworkExplorer>>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [],
},
ides: {
items: parseEnvJson<Array<ContractCodeIde>>(getEnvValue('NEXT_PUBLIC_CONTRACT_CODE_IDES')) || [],
},
});
export default UI;
......@@ -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_WEB3_WALLETS=['token_pocket','metamask']
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
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
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
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=
# app features
......
......@@ -11,6 +11,7 @@ import * as yup from 'yup';
import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } 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 { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
......@@ -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
.object({
name: yup.string().required(),
......@@ -331,6 +339,7 @@ const schema = yup
NEXT_PUBLIC_NETWORK_ID: yup.number().positive().integer().required(),
NEXT_PUBLIC_NETWORK_RPC_URL: yup.string().test(urlTest),
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_DECIMALS: yup.number().integer().positive(),
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL: yup.string(),
......@@ -417,6 +426,11 @@ const schema = yup
.transform(replaceQuotes)
.json()
.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_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
......@@ -447,6 +461,7 @@ const schema = yup
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
// 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
......@@ -9,6 +9,7 @@ NEXT_PUBLIC_APP_PORT=3000
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_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_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_FOOTER_LINKS=https://example.com
......
......@@ -161,6 +161,7 @@ frontend:
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
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_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:
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
......
......@@ -73,6 +73,8 @@ frontend:
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']"
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_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:
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
......
......@@ -14,7 +14,7 @@ Thanks for showing interest to contribute to Blockscout. The following steps wil
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
node -v
npm -v
......
......@@ -33,6 +33,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Banner ads](ENVS.md#banner-ads)
- [Text ads](ENVS.md#text-ads)
- [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)
- [ZkEvm rollup (L2) chain](NVS.md#zkevm-rollup-l2-chain)
- [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
| 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_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_DECIMALS | `string` | Network currency decimals | - | `18` | `6` |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` |
......@@ -258,6 +260,7 @@ Settings for meta tags and OG tags
| 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_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_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! 🤪` |
......@@ -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>`
#### 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;
## App features
......@@ -345,6 +356,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&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
| 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">
<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 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 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 {
import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters } from 'types/api/txsFilters';
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 { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
......@@ -579,6 +580,20 @@ export const RESOURCES = {
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
config_backend_version: {
path: '/api/v2/config/backend-version',
......@@ -651,7 +666,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'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>;
......@@ -754,6 +769,9 @@ Q extends 'addresses_lookup' ? EnsAddressLookupResponse :
Q extends 'domain_info' ? EnsDomainDetailed :
Q extends 'domain_events' ? EnsDomainEventsResponse :
Q extends 'domains_lookup' ? EnsDomainLookupResponse :
Q extends 'user_ops' ? UserOpsResponse :
Q extends 'user_op' ? UserOp :
Q extends 'user_ops_account' ? UserOpsAccount :
never;
/* eslint-enable @typescript-eslint/indent */
......@@ -775,6 +793,7 @@ Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters :
Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters :
Q extends 'user_ops' ? UserOpsFilters :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -57,6 +57,7 @@ export default function useApiFetch() {
},
{
resource: resource.path,
omitSentryErrorLog: true, // disable logging of API errors to Sentry
},
);
}, [ fetch, csrfToken ]);
......
......@@ -4,20 +4,22 @@ import React from 'react';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
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() {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
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;
},
retry,
throwOnError: (error) => {
const status = getErrorObjStatusCode(error);
// 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 {
icon: 'transactions',
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 =
{
text: 'Verified contracts',
......@@ -54,7 +61,7 @@ export default function useNavItems(): ReturnType {
isActive: pathname === '/verified-contracts',
};
const ensLookup = config.features.nameService.isEnabled ? {
text: 'ENS lookup',
text: 'Name services lookup',
nextRoute: { pathname: '/name-domains' as const },
icon: 'ENS',
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]',
......@@ -64,6 +71,7 @@ export default function useNavItems(): ReturnType {
blockchainNavItems = [
[
txs,
userOps,
blocks,
{
text: 'Txn batches',
......@@ -71,7 +79,7 @@ export default function useNavItems(): ReturnType {
icon: 'txn_batches',
isActive: pathname === '/zkevm-l2-txn-batches' || pathname === '/zkevm-l2-txn-batch/[number]',
},
],
].filter(Boolean),
[
topAccounts,
verifiedContracts,
......@@ -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' },
],
[
userOps,
topAccounts,
verifiedContracts,
ensLookup,
......@@ -103,6 +112,7 @@ export default function useNavItems(): ReturnType {
} else {
blockchainNavItems = [
txs,
userOps,
blocks,
topAccounts,
verifiedContracts,
......
......@@ -39,6 +39,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/l2-withdrawals': 'Root page',
'/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page',
'/ops': 'Root page',
'/op/[hash]': 'Regular page',
'/404': 'Regular page',
'/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page',
......
......@@ -42,6 +42,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-withdrawals': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/ops': DEFAULT_TEMPLATE,
'/op/[hash]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE,
'/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE,
......
......@@ -37,6 +37,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/ops': 'user operations',
'/op/[hash]': 'user operation %hash%',
'/404': 'error - page not found',
'/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details',
......
......@@ -37,6 +37,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/l2-withdrawals': 'Withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
'/ops': 'User operations',
'/op/[hash]': 'User operation details',
'/404': '404',
'/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details',
......
......@@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app';
import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError';
const feature = appConfig.features.sentry;
......@@ -59,6 +60,9 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
'The quota has been exceeded',
'Attempt to connect to relay via',
'WebSocket connection failed for URL: wss://relay.walletconnect.com',
// API errors
RESOURCE_LOAD_ERROR_MESSAGE,
],
denyUrls: [
// 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 = {
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
......@@ -101,6 +109,13 @@ export const tx1: SearchResultTx = {
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 = {
items: [
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) => {
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" {
| StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains">
| DynamicRoute<"/op/[hash]", { "hash": string }>
| StaticRoute<"/ops">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
......
......@@ -4,8 +4,8 @@
"private": false,
"homepage": "https://github.com/blockscout/frontend#readme",
"engines": {
"node": "18",
"npm": "8"
"node": "20.11.0",
"npm": "10.2.4"
},
"scripts": {
"dev": "./tools/scripts/dev.sh",
......@@ -24,7 +24,7 @@
"svg:build-sprite": "icons build -i ./icons -o ./public/icons --optimize",
"test:pw": "./tools/scripts/pw.sh",
"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:jest": "jest",
"test:jest:watch": "jest --watch",
......@@ -99,8 +99,8 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@playwright/experimental-ct-react": "1.35.1",
"@playwright/test": "^1.35.1",
"@playwright/experimental-ct-react": "1.41.1",
"@playwright/test": "1.41.1",
"@svgr/webpack": "^6.5.1",
"@tanstack/eslint-plugin-query": "^5.0.5",
"@testing-library/react": "^14.0.0",
......@@ -112,7 +112,7 @@
"@types/jest": "^29.2.0",
"@types/js-cookie": "^3.0.2",
"@types/mixpanel-browser": "^2.38.1",
"@types/node": "18.11.18",
"@types/node": "20.11.0",
"@types/phoenix": "^1.5.4",
"@types/qrcode": "^1.5.0",
"@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 = {
{ name: 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
],
userOps: [
{ name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' },
],
};
export const viewsEnvs = {
......
// This file is generated by npm run build:icons
export type IconName =
| "ABI"
| "ABI_slim"
| "ABI"
| "API"
| "apps"
| "arrows/down-right"
......@@ -27,6 +28,7 @@
| "discussions"
| "docs"
| "donate"
| "dots"
| "edit"
| "email-sent"
| "email"
......@@ -125,10 +127,11 @@
| "txn_batches"
| "unfinalized"
| "uniswap"
| "user_op_slim"
| "user_op"
| "verified_token"
| "verified"
| "verify-contract"
| "vertical_dots"
| "wallet"
| "wallets/coinbase"
| "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) => {
return {
container: {
[$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: {
[$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 {
public_tags: Array<AddressTag> | null;
}
export interface AddressParam extends UserTags {
export type AddressParamBasic = {
hash: string;
implementation_name: string | null;
name: string | null;
......@@ -23,3 +23,5 @@ export interface AddressParam extends UserTags {
is_verified: boolean | null;
ens_domain_name: string | null;
}
export type AddressParam = UserTags & AddressParamBasic;
......@@ -13,7 +13,7 @@ export interface Block {
hash: string;
parent_hash: string;
difficulty: string;
total_difficulty: string;
total_difficulty: string | null;
gas_used: string | null;
gas_limit: string;
nonce: string;
......@@ -69,7 +69,7 @@ export type BlockWithdrawalsResponse = {
next_page_params: {
index: number;
items_count: number;
};
} | null;
}
export type BlockWithdrawalsItem = {
......
......@@ -55,7 +55,14 @@ export interface SearchResultTx {
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 {
items: Array<SearchResultItem>;
......@@ -79,5 +86,5 @@ export interface SearchResultFilters {
export interface SearchRedirectResult {
parameter: string | null;
redirect: boolean;
type: 'address' | 'block' | 'transaction' | null;
type: 'address' | 'block' | 'transaction' | 'user_operation' | null;
}
......@@ -26,14 +26,14 @@ export type Transaction = {
hash: string;
result: string;
confirmations: number;
status: 'ok' | 'error' | null;
status: 'ok' | 'error' | null | undefined;
block: number | null;
timestamp: string | null;
confirmation_duration: Array<number>;
confirmation_duration: Array<number> | null;
from: AddressParam;
value: string;
fee: Fee;
gas_price: string;
gas_price: string | null;
type: number | null;
gas_used: string | null;
gas_limit: string;
......@@ -49,7 +49,7 @@ export type Transaction = {
decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string;
exchange_rate: string | null;
method: string | null;
tx_types: Array<TransactionType>;
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';
import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units';
import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
......@@ -95,7 +96,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
<Th width="16%">Txn</Th>
<Th width="25%">Gas used</Th>
{ !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>
</Thead>
<Tbody>
......
......@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
......@@ -68,7 +69,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
throwOnResourceLoadError(addressQuery);
}
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) => {
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton isLoaded={ !isPlaceholderData } borderRadius="base">
<Button
className={ className }
color={ scoreColor }
......@@ -122,7 +122,7 @@ const SolidityscanReport = ({ className, hash }: Props) => {
onClick={ onToggle }
aria-label="SolidityScan score"
fontWeight={ 500 }
px={ 2 }
px="6px"
h="32px"
flexShrink={ 0 }
>
......
......@@ -7,6 +7,7 @@ import type { Block } from 'types/api/block';
import config from 'configs/app';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { currencyUnits } from 'lib/units';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -50,7 +51,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex>
{ !config.UI.views.block.hiddenFields?.total_reward && (
<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>
</Flex>
) }
......
......@@ -3,6 +3,7 @@ import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { currencyUnits } from 'lib/units';
import ChartWidget from 'ui/shared/chart/ChartWidget';
interface Props {
......@@ -26,7 +27,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
items={ items }
isLoading={ isPending }
h="300px"
units={ config.chain.currency.symbol }
units={ currencyUnits.ether }
/>
);
};
......
......@@ -5,8 +5,8 @@ import React from 'react';
import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import type { PaginationParams } from 'ui/shared/pagination/types';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import { currencyUnits } from 'lib/units';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -32,7 +32,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<Th width="20%">Block</Th>
<Th width="20%">Txn</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>
</Tr>
</Thead>
......
......@@ -4,9 +4,9 @@ import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import config from 'configs/app';
import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { currencyUnits } from 'lib/units';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -25,7 +25,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<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 isLoaded={ !props.isLoading }>
<Stat flexGrow="0">
......
......@@ -122,6 +122,9 @@ test('verified with multiple sources', async({ mount, page }) => {
await page.getByRole('button', { name: 'View external libraries' }).click();
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 }) => {
......
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 { useWeb3Modal, useWeb3ModalState } from '@web3modal/wagmi/react';
import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import useWallet from 'ui/snippets/walletMenu/useWallet';
const ContractConnectWallet = () => {
const { open } = useWeb3Modal();
const { open: isOpen } = useWeb3ModalState();
const { disconnect } = useDisconnect();
const { isModalOpening, isModalOpen, connect, disconnect, address, isWalletConnected } = useWallet({ source: 'Smart contracts' });
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 = (() => {
if (isDisconnected || !address) {
if (!isWalletConnected) {
return (
<>
<span>Disconnected</span>
<Button
ml={ 3 }
onClick={ handleConnect }
onClick={ connect }
size="sm"
variant="outline"
isLoading={ isModalOpening || isOpen }
isLoading={ isModalOpening || isModalOpen }
loadingText="Connect wallet"
>
Connect wallet
......@@ -61,7 +39,7 @@ const ContractConnectWallet = () => {
ml={ 2 }
/>
</Flex>
<Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button>
<Button onClick={ disconnect } size="sm" variant="outline">Disconnect</Button>
</Flex>
);
})();
......
......@@ -39,7 +39,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
return [
...('inputs' in data ? data.inputs : []),
...('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,
internalType: 'uint256' as const,
fieldType: 'native_coin' as const,
......
......@@ -5,8 +5,8 @@ import React from 'react';
import type { SmartContractMethodOutput } from 'types/api/contract';
import config from 'configs/app';
import { WEI } from 'lib/consts';
import { currencyUnits } from 'lib/units';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
function castValueToString(value: number | string | boolean | object | bigint | undefined): string {
......@@ -32,17 +32,17 @@ interface Props {
const ContractMethodStatic = ({ data }: Props) => {
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 initialValue = castValueToString(data.value);
if (event.target.checked) {
setValue(BigNumber(initialValue).div(WEI).toFixed());
setLabel(config.chain.currency.symbol || 'ETH');
setLabel(currencyUnits.ether.toUpperCase());
} else {
setValue(BigNumber(initialValue).toFixed());
setLabel('WEI');
setLabel(currencyUnits.wei.toUpperCase());
}
}, [ data.value ]);
......
......@@ -13,6 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
import ContractCodeIdes from './ContractCodeIdes';
import ContractExternalLibraries from './ContractExternalLibraries';
const SOURCE_CODE_OPTIONS = [
......@@ -116,6 +117,8 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={{ base: 'auto', lg: diagramLink ? '0' : 'auto' }}/> :
null;
const ides = sourceType === 'secondary' ? <ContractCodeIdes hash={ implementationAddress }/> : <ContractCodeIdes hash={ address }/>;
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSourceType(event.target.value as SourceCodeType);
}, []);
......@@ -188,6 +191,7 @@ const ContractSourceCode = ({ address, implementationAddress }: Props) => {
{ editorSourceTypeSelector }
{ externalLibraries }
{ diagramLink }
{ ides }
{ copyToClipboard }
</Flex>
{ content }
......
......@@ -129,7 +129,8 @@ function castValue(value: string, type: SmartContractMethodArgType) {
}
const isNestedArray = (type.match(/\[/g) || []).length > 1;
if (isNestedArray) {
const isNestedTuple = type.includes('tuple');
if (isNestedArray || isNestedTuple) {
return parseArrayValue(value) || value;
}
......
......@@ -8,6 +8,7 @@ import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
......@@ -66,7 +67,7 @@ const AddressBalance = ({ data, isLoading }: Props) => {
return (
<DetailsInfoItem
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"
alignItems="flex-start"
isLoading={ isLoading }
......@@ -75,7 +76,7 @@ const AddressBalance = ({ data, isLoading }: Props) => {
value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate }
decimals={ String(config.chain.currency.decimals) }
currency={ config.chain.currency.symbol }
currency={ currencyUnits.ether }
accuracyUsd={ 2 }
accuracy={ 8 }
flexWrap="wrap"
......
......@@ -31,7 +31,7 @@ test('base view', async({ mount, page }) => {
</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 } });
});
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 React from 'react';
......@@ -50,7 +50,7 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
}
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) {
......@@ -95,15 +95,19 @@ const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Address ENS domains"
aria-label="Address domains"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<IconSvg name="ENS_slim" boxSize={ 5 }/>
<chakra.span ml={ 1 } display={{ base: 'none', lg: 'block' }}>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span>
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/>
<Show above="xl">
<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>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '500px' }}>
......
......@@ -6,6 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
......@@ -68,7 +69,7 @@ const TxInternalsListItem = ({
w="100%"
/>
<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 }>
<span>{ BigNumber(value).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() }</span>
</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