Merge branch 'main' into category-tabs
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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: |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *****************************
|
||||
# *** 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';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
@@ -1,3 +1,4 @@
|
||||
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` |
|
||||
|
||||
|
||||
|
||||
## App features
|
||||
@@ -345,6 +356,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
|
||||
|
||||
|
||||
|
||||
### 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` |
|
||||
|
||||
|
||||
|
||||
### Optimistic rollup (L2) chain
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value |
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -1,3 +1,3 @@
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,3 @@
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 477 B |
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function throwOnAbsentParamError(param: unknown) {
|
||||
if (!param) {
|
||||
throw new Error('Required param not provided', { cause: { status: 404 } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function hetToDecimal(hex: string) {
|
||||
const strippedHex = hex.startsWith('0x') ? hex.slice(2) : hex;
|
||||
return parseInt(strippedHex, 16);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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',
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createPublicClient, http } from 'viem';
|
||||
|
||||
import currentChain from './currentChain';
|
||||
|
||||
export const publicClient = createPublicClient({
|
||||
chain: currentChain,
|
||||
transport: http(),
|
||||
batch: {
|
||||
multicall: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
@@ -1,4 +1,12 @@
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
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",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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';
|
||||
@@ -0,0 +1,19 @@
|
||||
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 = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 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"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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),
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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 }
|
||||
>
|
||||
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -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 }) => {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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));
|
||||
@@ -1,47 +1,25 @@
|
||||
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 }
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
@@ -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 } });
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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' }}>
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
@@ -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>
|
||||
|
||||