Commit 77ecda6c authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into swap-button

parents 0dcad9df 504de39d
......@@ -59,7 +59,7 @@ jobs:
steps:
- name: Getting tags of the two latestest releases
id: tags
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
TAG: ${{ inputs.tag }}
with:
......@@ -108,7 +108,7 @@ jobs:
- name: Looking for commits between two releases
id: commits
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
PREVIOUS_TAG: ${{ steps.tags.outputs.previous }}
LATEST_TAG: ${{ steps.tags.outputs.latest }}
......@@ -137,7 +137,7 @@ jobs:
- name: Looking for issues linked to commits
id: linked_issues
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
COMMITS: ${{ steps.commits.outputs.result }}
with:
......@@ -225,7 +225,7 @@ jobs:
- name: Creating label
id: label_creating
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
LABEL_NAME: ${{ inputs.label_name }}
LABEL_COLOR: ${{ inputs.label_color }}
......@@ -253,7 +253,7 @@ jobs:
- name: Adding label to issues
id: labeling_issues
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
LABEL_NAME: ${{ inputs.label_name }}
ISSUES: ${{ steps.linked_issues.outputs.result }}
......
......@@ -32,7 +32,7 @@ jobs:
steps:
- name: Determine if it is the initial version of the pre-release
id: is_initial
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
TAG: ${{ github.ref_name }}
with:
......
......@@ -37,7 +37,7 @@ jobs:
steps:
- name: Fetching issues linked to pull request
id: linked_issues
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
with:
......
......@@ -12,7 +12,7 @@ jobs:
steps:
- name: Remove label
id: tags
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
LABEL_NAME: pre-release
with:
......
......@@ -48,7 +48,7 @@ jobs:
steps:
- name: Getting project info
id: project_info
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
PROJECT_NAME: ${{ inputs.project_name }}
FIELD_NAME: ${{ inputs.field_name }}
......@@ -131,7 +131,7 @@ jobs:
- name: Getting project items that linked to the issues
id: items
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
ISSUES: ${{ inputs.issues }}
with:
......@@ -193,7 +193,7 @@ jobs:
- name: Updating field value of the project items
id: updating_items
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
ITEMS: ${{ steps.items.outputs.result }}
PROJECT_ID: ${{ steps.project_info.outputs.id }}
......
......@@ -322,6 +322,7 @@
"eth",
"rootstock",
"polygon",
"zkevm",
"gnosis",
"localhost",
],
......
......@@ -29,7 +29,7 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
##views
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':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_NETWORK_EXPLORERS="[{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Blockchair','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/blockchair.png?raw=true','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/sentio.png?raw=true','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/tenderly.png?raw=true','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/0xPPl.png?raw=true','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/3xpl.png?raw=true','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}} ]"
# app features
NEXT_PUBLIC_APP_ENV=development
......
......@@ -14,10 +14,10 @@ NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://eth.llamarpc.com
NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com
# api configuration
NEXT_PUBLIC_API_HOST=65.109.173.70
NEXT_PUBLIC_API_HOST=zkevm.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
......@@ -26,7 +26,11 @@ NEXT_PUBLIC_API_BASE_PATH=/
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth.json
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/polygon.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/polygon-short.svg
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='linear-gradient(122deg, rgba(162, 41, 197, 1) 0%, rgba(123, 63, 228, 1) 100%)'
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(255, 255, 255, 1)'
## footer
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
......@@ -34,7 +38,6 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
......@@ -45,4 +48,4 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=http://65.109.173.70:81
NEXT_PUBLIC_L1_BASE_URL=https://polygon.blockscout.com
......@@ -259,6 +259,7 @@ const footerLinkGroupSchema: yup.ObjectSchema<CustomLinksGroup> = yup
const networkExplorerSchema: yup.ObjectSchema<NetworkExplorer> = yup
.object({
title: yup.string().required(),
logo: yup.string().test(urlTest),
baseUrl: yup.string().test(urlTest).required(),
paths: yup
.object()
......
......@@ -78,7 +78,9 @@ blockscout:
INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER: 'true'
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER: 'true'
INDEXER_RECEIPTS_BATCH_SIZE: 50
INDEXER_COIN_BALANCES_BATCH_SIZE: 50
INDEXER_COIN_BALANCES_BATCH_SIZE: 10
INDEXER_TOKEN_BALANCES_BATCH_SIZE: 15
INDEXER_TOKEN_BALANCES_CONCURRENCY: 4
DISABLE_EXCHANGE_RATES: 'true'
DISABLE_INDEXER: 'false'
FIRST_BLOCK: '8739119'
......
......@@ -62,7 +62,7 @@ frontend:
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
......
......@@ -270,6 +270,7 @@ Settings for meta tags and OG tags
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| logo | `string` | URL to explorer logo file. Should be at least 40x40. | - | - | `'https://foo.app/icon.png'` |
| title | `string` | Displayed name of the explorer | Required | - | `Anyblock` |
| baseUrl | `string` | Base url of the explorer | Required | - | `https://explorer.anyblock.tools` |
| paths | `Record<'tx' \| 'block' \| 'address' \| 'token', string>` | Map of explorer entities and their paths | Required | - | `{'tx':'/ethereum/poa/core/tx'}` |
......
......@@ -75,9 +75,9 @@ export default function useNavItems(): ReturnType {
blocks,
{
text: 'Txn batches',
nextRoute: { pathname: '/zkevm-l2-txn-batches' as const },
nextRoute: { pathname: '/batches' as const },
icon: 'txn_batches',
isActive: pathname === '/zkevm-l2-txn-batches' || pathname === '/zkevm-l2-txn-batch/[number]',
isActive: pathname === '/batches' || pathname === '/batches/[number]',
},
].filter(Boolean),
[
......@@ -91,16 +91,16 @@ export default function useNavItems(): ReturnType {
[
txs,
// eslint-disable-next-line max-len
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/l2-deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/l2-deposits' },
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/deposits' },
// eslint-disable-next-line max-len
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/l2-withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/l2-withdrawals' },
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/withdrawals' },
],
[
blocks,
// eslint-disable-next-line max-len
{ text: 'Txn batches', nextRoute: { pathname: '/l2-txn-batches' as const }, icon: 'txn_batches', isActive: pathname === '/l2-txn-batches' },
{ text: 'Txn batches', nextRoute: { pathname: '/batches' as const }, icon: 'txn_batches', isActive: pathname === '/batches' },
// eslint-disable-next-line max-len
{ text: 'Output roots', nextRoute: { pathname: '/l2-output-roots' as const }, icon: 'output_roots', isActive: pathname === '/l2-output-roots' },
{ text: 'Output roots', nextRoute: { pathname: '/output-roots' as const }, icon: 'output_roots', isActive: pathname === '/output-roots' },
],
[
userOps,
......
......@@ -33,12 +33,10 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/withdrawals': 'Root page',
'/visualize/sol2uml': 'Regular page',
'/csv-export': 'Regular page',
'/l2-deposits': 'Root page',
'/l2-output-roots': 'Root page',
'/l2-txn-batches': 'Root page',
'/l2-withdrawals': 'Root page',
'/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page',
'/deposits': 'Root page',
'/output-roots': 'Root page',
'/batches': 'Root page',
'/batches/[number]': 'Regular page',
'/ops': 'Root page',
'/op/[hash]': 'Regular page',
'/404': 'Regular page',
......
......@@ -36,12 +36,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/withdrawals': DEFAULT_TEMPLATE,
'/visualize/sol2uml': DEFAULT_TEMPLATE,
'/csv-export': DEFAULT_TEMPLATE,
'/l2-deposits': DEFAULT_TEMPLATE,
'/l2-output-roots': DEFAULT_TEMPLATE,
'/l2-txn-batches': DEFAULT_TEMPLATE,
'/l2-withdrawals': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/deposits': DEFAULT_TEMPLATE,
'/output-roots': DEFAULT_TEMPLATE,
'/batches': DEFAULT_TEMPLATE,
'/batches/[number]': DEFAULT_TEMPLATE,
'/ops': DEFAULT_TEMPLATE,
'/op/[hash]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE,
......
......@@ -31,12 +31,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/withdrawals': 'withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'export data to CSV',
'/l2-deposits': 'deposits (L1 > L2)',
'/l2-output-roots': 'output roots',
'/l2-txn-batches': 'tx batches (L2 blocks)',
'/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/deposits': 'deposits (L1 > L2)',
'/output-roots': 'output roots',
'/batches': 'tx batches (L2 blocks)',
'/batches/[number]': 'L2 tx batch %number%',
'/ops': 'user operations',
'/op/[hash]': 'user operation %hash%',
'/404': 'error - page not found',
......
......@@ -31,12 +31,10 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/withdrawals': 'Withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'Export data to CSV file',
'/l2-deposits': 'Deposits (L1 > L2)',
'/l2-output-roots': 'Output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-withdrawals': 'Withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
'/deposits': 'Deposits (L1 > L2)',
'/output-roots': 'Output roots',
'/batches': 'Tx batches (L2 blocks)',
'/batches/[number]': 'L2 tx batch details',
'/ops': 'User operations',
'/op/[hash]': 'User operation details',
'/404': '404',
......
......@@ -96,7 +96,7 @@ Type extends EventTypes.PAGE_WIDGET ? (
}
) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
'Type': 'Address click' | 'Token click';
'Type': 'Address click' | 'Token click' | 'Domain click';
} :
Type extends EventTypes.EXPERIMENT_STARTED ? {
'Experiment name': string;
......
......@@ -48,8 +48,8 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => {
return account(context);
};
export const beaconChain: GetServerSideProps<Props> = async(context) => {
if (!config.features.beaconChain.isEnabled) {
export const withdrawals: GetServerSideProps<Props> = async(context) => {
if (!config.features.beaconChain.isEnabled && !config.features.optimisticRollup.isEnabled) {
return {
notFound: true,
};
......@@ -58,7 +58,17 @@ export const beaconChain: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const L2: GetServerSideProps<Props> = async(context) => {
export const rollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.optimisticRollup.isEnabled && !config.features.zkEvmRollup.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.optimisticRollup.isEnabled) {
return {
notFound: true,
......@@ -68,7 +78,7 @@ export const L2: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const zkEvmL2: GetServerSideProps<Props> = async(context) => {
export const zkEvmRollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.zkEvmRollup.isEnabled) {
return {
notFound: true,
......
......@@ -26,19 +26,19 @@ declare module "nextjs-routes" {
| StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile">
| StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/batches/[number]", { "number": string }>
| StaticRoute<"/batches">
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
| StaticRoute<"/blocks">
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export">
| StaticRoute<"/deposits">
| StaticRoute<"/graphiql">
| StaticRoute<"/">
| StaticRoute<"/l2-deposits">
| StaticRoute<"/l2-output-roots">
| StaticRoute<"/l2-txn-batches">
| StaticRoute<"/l2-withdrawals">
| StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains">
| StaticRoute<"/output-roots">
| DynamicRoute<"/op/[hash]", { "hash": string }>
| StaticRoute<"/ops">
| StaticRoute<"/search-results">
......@@ -51,9 +51,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/txs/kettle/[hash]", { "hash": string }>
| StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">
| DynamicRoute<"/zkevm-l2-txn-batch/[number]", { "number": string }>
| StaticRoute<"/zkevm-l2-txn-batches">;
| StaticRoute<"/withdrawals">;
interface StaticRoute<Pathname> {
pathname: Pathname;
......
......@@ -242,6 +242,32 @@ const oldUrls = [
source: '/token/:hash/write-proxy',
destination: '/token/:hash?tab=write_proxy',
},
// ROLLUPs
{
source: '/l2-txn-batches',
destination: '/batches',
},
{
source: '/zkevm-l2-txn-batches',
destination: '/batches',
},
{
source: '/zkevm-l2-txn-batch/:path*',
destination: '/batches/:path*',
},
{
source: '/l2-deposits',
destination: '/deposits',
},
{
source: '/l2-withdrawals',
destination: '/withdrawals',
},
{
source: '/l2-output-roots',
destination: '/output-roots',
},
];
async function redirects() {
......
......@@ -23,6 +23,7 @@ import Layout from 'ui/shared/layout/Layout';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import 'lib/setLocale';
import 'focus-visible/dist/focus-visible';
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
......
......@@ -9,7 +9,7 @@ const ZkEvmL2TxnBatch = dynamic(() => import('ui/pages/ZkEvmL2TxnBatch'), { ssr:
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/zkevm-l2-txn-batch/[number]" query={ props }>
<PageNextJs pathname="/batches/[number]" query={ props }>
<ZkEvmL2TxnBatch/>
</PageNextJs>
);
......@@ -17,4 +17,4 @@ const Page: NextPage<Props> = (props: Props) => {
export default Page;
export { zkEvmL2 as getServerSideProps } from 'nextjs/getServerSideProps';
export { zkEvmRollup as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -4,16 +4,23 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const L2OutputRoots = dynamic(() => import('ui/pages/L2OutputRoots'), { ssr: false });
import config from 'configs/app';
const Batches = dynamic(() => {
if (config.features.zkEvmRollup.isEnabled) {
return import('ui/pages/ZkEvmL2TxnBatches');
}
return import('ui/pages/L2TxnBatches');
}, { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/l2-output-roots">
<L2OutputRoots/>
<PageNextJs pathname="/batches">
<Batches/>
</PageNextJs>
);
};
export default Page;
export { L2 as getServerSideProps } from 'nextjs/getServerSideProps';
export { rollup as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -4,16 +4,16 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const L2Deposits = dynamic(() => import('ui/pages/L2Deposits'), { ssr: false });
const Deposits = dynamic(() => import('ui/pages/L2Deposits'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/l2-deposits">
<L2Deposits/>
<PageNextJs pathname="/deposits">
<Deposits/>
</PageNextJs>
);
};
export default Page;
export { L2 as getServerSideProps } from 'nextjs/getServerSideProps';
export { optimisticRollup 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 L2Withdrawals = dynamic(() => import('ui/pages/L2Withdrawals'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/l2-withdrawals">
<L2Withdrawals/>
</PageNextJs>
);
};
export default Page;
export { L2 as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -4,16 +4,16 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const L2TxnBatches = dynamic(() => import('ui/pages/L2TxnBatches'), { ssr: false });
const OutputRoots = dynamic(() => import('ui/pages/L2OutputRoots'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/l2-txn-batches">
<L2TxnBatches/>
<PageNextJs pathname="/output-roots">
<OutputRoots/>
</PageNextJs>
);
};
export default Page;
export { L2 as getServerSideProps } from 'nextjs/getServerSideProps';
export { optimisticRollup as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -4,7 +4,14 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const Withdrawals = dynamic(() => import('ui/pages/Withdrawals'), { ssr: false });
import config from 'configs/app';
const Withdrawals = dynamic(() => {
if (config.features.optimisticRollup.isEnabled) {
return import('ui/pages/L2Withdrawals');
}
return import('ui/pages/Withdrawals');
}, { ssr: false });
const Page: NextPage = () => {
return (
......@@ -16,4 +23,4 @@ const Page: NextPage = () => {
export default Page;
export { beaconChain as getServerSideProps } from 'nextjs/getServerSideProps';
export { withdrawals 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 ZkEvmL2TxnBatches = dynamic(() => import('ui/pages/ZkEvmL2TxnBatches'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/zkevm-l2-txn-batches">
<ZkEvmL2TxnBatches/>
</PageNextJs>
);
};
export default Page;
export { zkEvmL2 as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -9,7 +9,7 @@ interface Feature {
export default function contextWithFeaturesFixture(envs: Array<Feature>): Parameters<typeof test.extend>[0]['context'] {
return async({ browser }, use) => {
const storageItems = envs.map(({ id, value }) => ({ name: `pw_feature:${ id }`, value: JSON.stringify({ value, type: typeof value }) }));
const storageItems = envs.map(({ id, value }) => ({ name: `pw_feature:${ id }`, value: JSON.stringify(value) }));
const context = await createContextWithStorage(browser, storageItems);
await use(context);
......
const useFeatureValue = (name, fallback) => {
try {
const { value, type } = JSON.parse(localStorage.getItem(`pw_feature:${ name }`));
const formattedValue = (() => {
switch (type) {
case 'boolean': {
return value === 'true';
}
case 'number': {
return Number(value);
}
default:
return value;
}
})();
return { isLoading: false, value: formattedValue };
const value = JSON.parse(localStorage.getItem(`pw_feature:${ name }`));
return { isLoading: false, value };
} catch (error) {
return { isLoading: false, value: fallback };
}
......
......@@ -7,13 +7,13 @@ export type Erc20TotalPayload = {
}
export type Erc721TotalPayload = {
token_id: string;
token_id: string | null;
}
export type Erc1155TotalPayload = {
decimals: string | null;
value: string;
token_id: string;
token_id: string | null;
}
export type TokenTransfer = (
......
......@@ -17,9 +17,10 @@ export type TxInterpretationVariable =
TxInterpretationVariableCurrency |
TxInterpretationVariableTimestamp |
TxInterpretationVariableToken |
TxInterpretationVariableAddress;
TxInterpretationVariableAddress |
TxInterpretationVariableDomain;
export type TxInterpretationVariableType = 'string' | 'currency' | 'timestamp' | 'token' | 'address';
export type TxInterpretationVariableType = 'string' | 'currency' | 'timestamp' | 'token' | 'address' | 'domain';
export type TxInterpretationVariableString = {
type: 'string';
......@@ -45,3 +46,8 @@ export type TxInterpretationVariableAddress = {
type: 'address';
value: AddressParam;
}
export type TxInterpretationVariableDomain = {
type: 'domain';
value: string;
}
......@@ -13,6 +13,7 @@ export interface FeaturedNetwork {
}
export interface NetworkExplorer {
logo?: string;
title: string;
baseUrl: string;
paths: {
......
......@@ -45,7 +45,7 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> ({ prefix }${ usd.toFormat(2) })</Text>
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/>
</Button>
{ isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> }
{ isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor } borderRadius="base"/> }
</Box>
);
};
......
......@@ -43,9 +43,9 @@ const AddressesListItem = ({
{ item.public_tags !== null && item.public_tags.length > 0 && item.public_tags.map(tag => (
<Tag key={ tag.label } isLoading={ isLoading }>{ tag.display_name }</Tag>
)) }
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>{ `Balance ${ currencyUnits.ether }` }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<HStack spacing={ 3 } maxW="100%" alignItems="flex-start">
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 } flexShrink={ 0 }>{ `Balance ${ currencyUnits.ether }` }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW="0" whiteSpace="pre-wrap">
<span>{ addressBalance.dp(8).toFormat() }</span>
</Skeleton>
</HStack>
......
......@@ -34,11 +34,12 @@ const AddressesTableItem = ({
{ index }
</Skeleton>
</Td>
<Td verticalAlign="middle">
<Td>
<AddressEntity
address={ item }
isLoading={ isLoading }
fontWeight={ 700 }
my="2px"
/>
</Td>
<Td pl={ 10 }>
......@@ -47,7 +48,7 @@ const AddressesTableItem = ({
)) : null }
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Skeleton isLoaded={ !isLoading } display="inline-block" maxW="100%">
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text>
{ addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> }
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
......
......@@ -58,7 +58,7 @@ const LatestDeposits = () => {
}
if (data) {
const depositsUrl = route({ pathname: '/l2-deposits' });
const depositsUrl = route({ pathname: '/deposits' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit" isLoading={ isPlaceholderData }/>
......
......@@ -73,7 +73,7 @@ const LatestZkEvmL2Batches = () => {
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ route({ pathname: '/zkevm-l2-txn-batches' }) }>View all batches</LinkInternal>
<LinkInternal fontSize="sm" href={ route({ pathname: '/batches' }) }>View all batches</LinkInternal>
</Flex>
</>
);
......
......@@ -57,7 +57,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
<Flex alignItems="center">
<Skeleton isLoaded={ !isLoading } mr={ 2 }>Txn</Skeleton>
<LinkInternal
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: batch.number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: batch.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
......
......@@ -73,7 +73,7 @@ const Stats = () => {
icon="txn_batches"
title="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/zkevm-l2-txn-batches' }) }
url={ route({ pathname: '/batches' }) }
isLoading={ zkEvmLatestBatchQuery.isPlaceholderData }
/>
) : (
......
......@@ -9,7 +9,6 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ENS_DOMAIN } from 'stubs/ENS';
import NameDomainDetails from 'ui/nameDomain/NameDomainDetails';
......@@ -25,7 +24,6 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
const NameDomain = () => {
const isMobile = useIsMobile();
const router = useRouter();
const domainName = getQueryParamString(router.query.name);
......@@ -48,31 +46,38 @@ const NameDomain = () => {
const isLoading = infoQuery.isPlaceholderData;
const titleSecondRow = (
<Flex columnGap={ 3 } rowGap={ 3 } fontFamily="heading" fontSize="lg" fontWeight={ 500 } alignItems="center" w="100%">
<Flex
columnGap={ 3 }
rowGap={ 3 }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
alignItems="center"
w="100%"
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
>
<EnsEntity
name={ domainName }
isLoading={ isLoading }
noLink
maxW={ infoQuery.data?.resolved_address ? '300px' : 'min-content' }
maxW={{ lg: infoQuery.data?.resolved_address ? '300px' : 'min-content' }}
/>
{ infoQuery.data?.resolved_address && (
<AddressEntity
address={ infoQuery.data?.resolved_address }
isLoading={ isLoading }
truncation={ isMobile ? 'constant' : 'dynamic' }
flexShrink={ 0 }
/>
) }
{ infoQuery.data?.resolved_address && (
<Tooltip label="Lookup for related domain names">
<LinkInternal
flexShrink={ 0 }
display="inline-flex"
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: infoQuery.data?.resolved_address?.hash } }) }
>
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
<Flex alignItems="center" maxW="100%" columnGap={ 3 }>
<AddressEntity
address={ infoQuery.data?.resolved_address }
isLoading={ isLoading }
/>
<Tooltip label="Lookup for related domain names">
<LinkInternal
flexShrink={ 0 }
display="inline-flex"
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: infoQuery.data?.resolved_address?.hash } }) }
>
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</Flex>
) }
</Flex>
);
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import NetworkExplorers from './NetworkExplorers';
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<NetworkExplorers type="tx" pathParam="0x123"/>
</TestApp>,
);
await component.getByText('2').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 300, height: 150 } });
});
import { Flex, Button, chakra, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Show, Hide } from '@chakra-ui/react';
import {
Image,
Button,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
Show,
Hide,
useColorModeValue,
chakra,
useDisclosure,
Grid,
} from '@chakra-ui/react';
import React from 'react';
import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
......@@ -16,15 +29,24 @@ interface Props {
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const defaultIconColor = useColorModeValue('gray.400', 'gray.500');
const explorersLinks = React.useMemo(() => {
return config.UI.explorers.items
.filter((explorer) => typeof explorer.paths[type] === 'string')
.map((explorer) => {
const url = new URL(stripTrailingSlash(explorer.paths[type] || '') + '/' + pathParam, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
return (
<LinkExternal h="34px" key={ explorer.baseUrl } href={ url.toString() } alignItems="center" display="inline-flex" minW="120px">
{ explorer.logo ?
<Image boxSize={ 5 } mr={ 2 } src={ explorer.logo } alt={ `${ explorer.title } icon` }/> :
<IconSvg name="explorer" boxSize={ 5 } color={ defaultIconColor } mr={ 2 }/>
}
{ explorer.title }
</LinkExternal>
);
});
}, [ pathParam, type ]);
}, [ pathParam, type, defaultIconColor ]);
if (explorersLinks.length === 0) {
return null;
......@@ -54,18 +76,18 @@ const NetworkExplorers = ({ className, type, pathParam }: Props) => {
</Hide>
</Button>
</PopoverTrigger>
<PopoverContent w="240px">
<PopoverContent w="auto">
<PopoverBody >
<chakra.span color="text_secondary" fontSize="xs">Verify with other explorers</chakra.span>
<Flex
<Grid
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
templateColumns={ explorersLinks.length > 1 ? 'auto auto' : '1fr' }
columnGap={ 4 }
rowGap={ 2 }
mt={ 3 }
>
{ explorersLinks }
</Flex>
</Grid>
</PopoverBody>
</PopoverContent>
</Popover>
......
......@@ -61,7 +61,7 @@ const TokenTransferListItem = ({
<TxAdditionalInfo hash={ txHash } isMobile isLoading={ isLoading }/>
) }
</Flex>
{ 'token_id' in total && <NftEntity hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
{ 'token_id' in total && total.token_id !== null && <NftEntity hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
{ showTxInfo && txHash && (
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<TxEntity
......
......@@ -65,7 +65,7 @@ const TokenTransferTableItem = ({
</Flex>
</Td>
<Td>
{ 'token_id' in total && <NftEntity hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
{ 'token_id' in total && total.token_id !== null && <NftEntity hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
</Td>
{ showTxInfo && txHash && (
<Td>
......
......@@ -23,7 +23,7 @@ const ZkEvmBatchEntityL2 = (props: BlockEntity.EntityProps) => {
<BlockEntity.Icon { ...partsProps } name="txn_batches_slim"/>
<BlockEntity.Link
{ ...linkProps }
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: props.number.toString() } }) }
href={ route({ pathname: '/batches/[number]', query: { number: props.number.toString() } }) }
>
<BlockEntity.Content { ...partsProps }/>
</BlockEntity.Link>
......
......@@ -16,7 +16,7 @@ const LayoutHome = ({ children }: Props) => {
<Layout.MainArea>
<Layout.SideBar/>
<Layout.MainColumn
paddingTop={{ base: 6, lg: 9 }}
paddingTop={{ base: 3, lg: 6 }}
>
<HeaderAlert/>
<AppErrorBoundary>
......
......@@ -14,7 +14,7 @@ const MainColumn = ({ children, className }: Props) => {
flexGrow={ 1 }
w={{ base: '100%', lg: 'auto' }}
paddingX={{ base: 4, lg: 12 }}
paddingTop={{ base: `${ 32 + 60 }px`, lg: 9 }} // 32px is top padding of content area, 60px is search bar height
paddingTop={{ base: `${ 12 + 52 }px`, lg: 6 }} // 12px is top padding of content area, 52px is search bar height
paddingBottom={ 10 }
>
{ children }
......
......@@ -9,7 +9,7 @@ interface Props {
onClose: () => void;
}
const NftHtmlWithFullscreen = ({ src, isOpen, onClose }: Props) => {
const NftHtmlFullscreen = ({ src, isOpen, onClose }: Props) => {
return (
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.iframe
......@@ -22,4 +22,4 @@ const NftHtmlWithFullscreen = ({ src, isOpen, onClose }: Props) => {
);
};
export default NftHtmlWithFullscreen;
export default NftHtmlFullscreen;
import { chakra, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import NftHtml from './NftHtml';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
interface Props {
src: string;
onLoad: () => void;
onError: () => void;
}
const NftHtmlWithFullscreen = ({ src, onLoad, onError }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<NftHtml src={ src } onLoad={ onLoad } onError={ onError } onClick={ onOpen }/>
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.iframe
w="90vw"
h="90vh"
src={ src }
sandbox="allow-scripts"
onLoad={ onLoad }
onError={ onError }
/>
</NftMediaFullscreenModal>
</>
);
};
export default NftHtmlWithFullscreen;
......@@ -11,12 +11,29 @@ interface Props {
onClose: () => void;
}
const NftImageWithFullscreen = ({ src, isOpen, onClose }: Props) => {
const NftImageFullscreen = ({ src, isOpen, onClose }: Props) => {
const imgRef = React.useRef<HTMLImageElement>(null);
const [ hasDimentions, setHasDimentions ] = React.useState<boolean>(true);
const checkWidth = React.useCallback(() => {
if (imgRef.current?.getBoundingClientRect().width === 0) {
setHasDimentions(false);
}
}, [ ]);
return (
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<Image src={ src } alt="Token instance image" maxH="90vh" maxW="90vw"/>
<Image
src={ src }
alt="Token instance image"
maxH="90vh"
maxW="90vw"
ref={ imgRef }
onLoad={ checkWidth }
sx={ hasDimentions ? {} : { width: '90vw', height: '90vh' } }
/>
</NftMediaFullscreenModal>
);
};
export default NftImageWithFullscreen;
export default NftImageFullscreen;
import {
Image,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import NftImage from './NftImage';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
interface Props {
src: string;
onLoad: () => void;
onError: () => void;
}
const NftImageWithFullscreen = ({ src, onLoad, onError }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<NftImage src={ src } onLoad={ onLoad } onError={ onError } onClick={ onOpen }/>
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<Image src={ src } alt="Token instance image" maxH="90vh" maxW="90vw"/>
</NftMediaFullscreenModal>
</>
);
};
export default NftImageWithFullscreen;
......@@ -10,7 +10,7 @@ interface Props {
onClose: () => void;
}
const NftVideoWithFullscreen = ({ src, isOpen, onClose }: Props) => {
const NftVideoFullscreen = ({ src, isOpen, onClose }: Props) => {
return (
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.video
......@@ -23,4 +23,4 @@ const NftVideoWithFullscreen = ({ src, isOpen, onClose }: Props) => {
);
};
export default NftVideoWithFullscreen;
export default NftVideoFullscreen;
import {
chakra,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
import NftVideo from './NftVideo';
import { videoPlayProps } from './utils';
interface Props {
src: string;
onLoad: () => void;
onError: () => void;
}
const NftVideoWithFullscreen = ({ src, onLoad, onError }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<NftVideo src={ src } onLoad={ onLoad } onError={ onError } onClick={ onOpen }/>
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.video
{ ...videoPlayProps }
src={ src }
onCanPlayThrough={ onLoad }
onError={ onError }
maxH="90vh"
maxW="90vw"
/>
</NftMediaFullscreenModal>
</>
);
};
export default NftVideoWithFullscreen;
......@@ -11,6 +11,10 @@ import WalletMenuMobile from 'ui/snippets/walletMenu/WalletMenuMobile';
import Burger from './Burger';
const LOGO_IMAGE_PROPS = {
margin: '0 auto',
};
type Props = {
isHomePage?: boolean;
renderSearchBar?: () => React.ReactNode;
......@@ -47,7 +51,7 @@ const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => {
boxShadow={ !inView && scrollDirection === 'down' ? 'md' : 'none' }
>
<Burger/>
<NetworkLogo/>
<NetworkLogo imageProps={ LOGO_IMAGE_PROPS }/>
<Flex columnGap={ 2 }>
{ config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuMobile/> }
......
import type { StyleProps } from '@chakra-ui/react';
import { Box, Image, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react';
......@@ -9,9 +10,10 @@ import IconSvg from 'ui/shared/IconSvg';
interface Props {
isCollapsed?: boolean;
onClick?: (event: React.SyntheticEvent) => void;
imageProps?: StyleProps;
}
const LogoFallback = ({ isCollapsed, isSmall }: { isCollapsed?: boolean; isSmall?: boolean }) => {
const LogoFallback = ({ isCollapsed, isSmall, imageProps }: { isCollapsed?: boolean; isSmall?: boolean; imageProps?: StyleProps }) => {
const field = isSmall ? 'icon' : 'logo';
const logoColor = useColorModeValue('blue.600', 'white');
......@@ -36,11 +38,12 @@ const LogoFallback = ({ isCollapsed, isSmall }: { isCollapsed?: boolean; isSmall
height="100%"
color={ logoColor }
display={ display }
{ ...imageProps }
/>
);
};
const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const NetworkLogo = ({ isCollapsed, onClick, imageProps }: Props) => {
const logoSrc = useColorModeValue(config.UI.sidebar.logo.default, config.UI.sidebar.logo.dark || config.UI.sidebar.logo.default);
const iconSrc = useColorModeValue(config.UI.sidebar.icon.default, config.UI.sidebar.icon.dark || config.UI.sidebar.icon.default);
......@@ -66,9 +69,10 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
h="100%"
src={ logoSrc }
alt={ `${ config.chain.name } network logo` }
fallback={ <LogoFallback isCollapsed={ isCollapsed }/> }
fallback={ <LogoFallback isCollapsed={ isCollapsed } imageProps={ imageProps }/> }
display={{ base: 'block', lg: isCollapsed === false ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}
style={ logoStyle }
{ ...imageProps }
/>
{ /* small logo */ }
<Image
......@@ -76,9 +80,10 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
h="100%"
src={ iconSrc }
alt={ `${ config.chain.name } network logo` }
fallback={ <LogoFallback isCollapsed={ isCollapsed } isSmall/> }
fallback={ <LogoFallback isCollapsed={ isCollapsed } imageProps={ imageProps } isSmall/> }
display={{ base: 'none', lg: isCollapsed === false ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }}
style={ iconStyle }
{ ...imageProps }
/>
</Box>
);
......
......@@ -111,7 +111,7 @@ const SearchBar = ({ isHomepage }: Props) => {
autoFocus={ false }
onClose={ onClose }
placement="bottom-start"
offset={ isMobile && !isHomepage ? [ 16, -12 ] : undefined }
offset={ isMobile && !isHomepage ? [ 16, -4 ] : undefined }
isLazy
>
<PopoverTrigger>
......
......@@ -77,7 +77,7 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
zIndex={{ base: isHomepage ? 'auto' : '-1', lg: 'auto' }}
paddingX={{ base: isHomepage ? 0 : 4, lg: 0 }}
paddingTop={{ base: isHomepage ? 0 : 1, lg: 0 }}
paddingBottom={{ base: isHomepage ? 0 : 4, lg: 0 }}
paddingBottom={{ base: isHomepage ? 0 : 2, lg: 0 }}
boxShadow={ scrollDirection !== 'down' && isSticky ? 'md' : 'none' }
transform={{ base: isHomepage ? 'none' : transformMobile, lg: 'none' }}
transitionProperty="transform,box-shadow,background-color,color,border-color"
......
......@@ -87,7 +87,7 @@ const TokenTransferListItem = ({
) }
</Grid>
) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && total.token_id !== null && (
<NftEntity
hash={ token.address }
id={ total.token_id }
......
......@@ -70,7 +70,7 @@ const TokenTransferTableItem = ({
</Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td>
{ 'token_id' in total ? (
{ 'token_id' in total && total.token_id !== null ? (
<NftEntity
hash={ token.address }
id={ total.token_id }
......
......@@ -9,7 +9,7 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity';
interface Props {
token: TokenInfo;
value: string;
tokenId: string;
tokenId: string | null;
}
const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
......@@ -26,15 +26,18 @@ const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
) : (
<chakra.span color="text_secondary">for token ID</chakra.span>
) }
<NftEntity
hash={ token.address }
id={ tokenId }
fontWeight={ 600 }
iconSize="md"
maxW={{ base: '100%', lg: '150px' }}
w="auto"
flexShrink={ 0 }
/>
{ tokenId !== null ? (
<NftEntity
hash={ token.address }
id={ tokenId }
fontWeight={ 600 }
iconSize="md"
maxW={{ base: '100%', lg: '150px' }}
w="auto"
flexShrink={ 0 }
/>
) : <chakra.span color="text_secondary"> N/A </chakra.span>
}
<chakra.span color="text_secondary">of</chakra.span>
<TokenEntity
token={ token }
......
......@@ -10,6 +10,7 @@ import dayjs from 'lib/date/dayjs';
import hexToDecimal from 'lib/hexToDecimal';
import { publicClient } from 'lib/web3/client';
import { GET_BLOCK, GET_TRANSACTION, GET_TRANSACTION_RECEIPT, GET_TRANSACTION_CONFIRMATIONS } from 'stubs/RPC';
import { unknownAddress } from 'ui/shared/address/utils';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
import TestnetWarning from 'ui/shared/alerts/TestnetWarning';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -66,16 +67,6 @@ const TxDetailsDegraded = ({ hash, txQuery }: Props) => {
})();
const gasPrice = txReceipt?.effectiveGasPrice ?? tx.gasPrice;
const unknownAddress = {
is_contract: false,
is_verified: false,
implementation_name: '',
name: '',
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
};
return {
from: { ...unknownAddress, hash: tx.from as string },
......@@ -86,7 +77,7 @@ const TxDetailsDegraded = ({ hash, txQuery }: Props) => {
status,
block: tx.blockNumber ? Number(tx.blockNumber) : null,
value: tx.value.toString(),
gas_price: txReceipt?.effectiveGasPrice.toString() ?? tx.gasPrice?.toString() ?? null,
gas_price: gasPrice?.toString() ?? null,
base_fee_per_gas: block?.baseFeePerGas?.toString() ?? null,
max_fee_per_gas: tx.maxFeePerGas?.toString() ?? null,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas?.toString() ?? null,
......
import { Skeleton, Flex, Text, chakra } from '@chakra-ui/react';
import { Skeleton, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TxInterpretationSummary, TxInterpretationVariable } from 'types/api/txInterpretation';
import type {
TxInterpretationSummary,
TxInterpretationVariable,
TxInterpretationVariableString,
} from 'types/api/txInterpretation';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import * as mixpanel from 'lib/mixpanel/index';
import { currencyUnits } from 'lib/units';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import IconSvg from 'ui/shared/IconSvg';
import { extractVariables, getStringChunks, NATIVE_COIN_SYMBOL_VAR_NAME } from './utils';
import { extractVariables, getStringChunks, fillStringVariables, NATIVE_COIN_SYMBOL_VAR_NAME } from './utils';
type Props = {
summary?: TxInterpretationSummary;
......@@ -19,7 +25,9 @@ type Props = {
className?: string;
}
const TxInterpretationElementByType = ({ variable }: { variable?: TxInterpretationVariable }) => {
type NonStringTxInterpretationVariable = Exclude<TxInterpretationVariable, TxInterpretationVariableString>
const TxInterpretationElementByType = ({ variable }: { variable?: NonStringTxInterpretationVariable }) => {
const onAddressClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.TX_INTERPRETATION_INTERACTION, { Type: 'Address click' });
}, []);
......@@ -28,6 +36,10 @@ const TxInterpretationElementByType = ({ variable }: { variable?: TxInterpretati
mixpanel.logEvent(mixpanel.EventTypes.TX_INTERPRETATION_INTERACTION, { Type: 'Token click' });
}, []);
const onDomainClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.TX_INTERPRETATION_INTERACTION, { Type: 'Domain click' });
}, []);
if (!variable) {
return null;
}
......@@ -36,28 +48,47 @@ const TxInterpretationElementByType = ({ variable }: { variable?: TxInterpretati
switch (type) {
case 'address': {
return (
<AddressEntity
address={ value }
truncation="constant"
sx={{ ':not(:first-child)': { marginLeft: 1 } }}
whiteSpace="initial"
onClick={ onAddressClick }
/>
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
<AddressEntity
address={ value }
truncation="constant"
onClick={ onAddressClick }
whiteSpace="initial"
/>
</chakra.span>
);
}
case 'token':
return (
<TokenEntity
token={ value }
onlySymbol
noCopy
width="fit-content"
sx={{ ':not(:first-child)': { marginLeft: 1 } }}
mr={ 2 }
whiteSpace="initial"
onClick={ onTokenClick }
/>
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
<TokenEntity
token={ value }
onlySymbol
noCopy
width="fit-content"
_notFirst={{ marginLeft: 1 }}
mr={ 2 }
whiteSpace="initial"
onClick={ onTokenClick }
/>
</chakra.span>
);
case 'domain': {
if (config.features.nameService.isEnabled) {
return (
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
<EnsEntity
name={ value }
width="fit-content"
_notFirst={{ marginLeft: 1 }}
whiteSpace="initial"
onClick={ onDomainClick }
/>
</chakra.span>
);
}
return <chakra.span color="text_secondary" whiteSpace="pre">{ value + ' ' }</chakra.span>;
}
case 'currency': {
let numberString = '';
if (BigNumber(value).isLessThan(0.1)) {
......@@ -69,14 +100,10 @@ const TxInterpretationElementByType = ({ variable }: { variable?: TxInterpretati
} else {
numberString = BigNumber(value).dividedBy(1000000).toFormat(2) + 'M';
}
return <Text>{ numberString + ' ' }</Text>;
return <chakra.span>{ numberString + ' ' }</chakra.span>;
}
case 'timestamp':
// timestamp is in unix format
return <Text color="text_secondary">{ dayjs(Number(value) * 1000).format('llll') + ' ' }</Text>;
case 'string':
default: {
return <Text color="text_secondary">{ value.toString() + ' ' }</Text>;
case 'timestamp': {
return <chakra.span color="text_secondary" whiteSpace="pre">{ dayjs(Number(value) * 1000).format('MMM DD YYYY') }</chakra.span>;
}
}
};
......@@ -89,23 +116,24 @@ const TxInterpretation = ({ summary, isLoading, className }: Props) => {
const template = summary.summary_template;
const variables = summary.summary_template_variables;
const variablesNames = extractVariables(template);
const intermediateResult = fillStringVariables(template, variables);
const chunks = getStringChunks(template);
const variablesNames = extractVariables(intermediateResult);
const chunks = getStringChunks(intermediateResult);
return (
<Skeleton display="flex" flexWrap="wrap" alignItems="center" isLoaded={ !isLoading } className={ className }>
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 2 }/>
<Skeleton isLoaded={ !isLoading } className={ className } fontWeight={ 500 } whiteSpace="pre-wrap" >
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 2 } verticalAlign="text-top"/>
{ chunks.map((chunk, index) => {
return (
<Flex whiteSpace="pre" key={ chunk + index } fontWeight={ 500 }>
<Text color="text_secondary">{ chunk.trim() + (chunk.trim() && variablesNames[index] ? ' ' : '') }</Text>
<chakra.span key={ chunk + index }>
<chakra.span color="text_secondary">{ chunk.trim() + (chunk.trim() && variablesNames[index] ? ' ' : '') }</chakra.span>
{ index < variablesNames.length && (
variablesNames[index] === NATIVE_COIN_SYMBOL_VAR_NAME ?
<Text>{ currencyUnits.ether + ' ' }</Text> :
<TxInterpretationElementByType variable={ variables[variablesNames[index]] }/>
<chakra.span>{ currencyUnits.ether + ' ' }</chakra.span> :
<TxInterpretationElementByType variable={ variables[variablesNames[index]] as NonStringTxInterpretationVariable }/>
) }
</Flex>
</chakra.span>
);
}) }
</Skeleton>
......
// we use that regex as a separator when splitting template and dont want to capture variables
import type { TxInterpretationVariable } from 'types/api/txInterpretation';
// eslint-disable-next-line regexp/no-useless-non-capturing-group
export const VAR_REGEXP = /\{(?:[^}]+)\}/g;
......@@ -16,3 +19,16 @@ export function extractVariables(templateString: string) {
export function getStringChunks(template: string) {
return template.split(VAR_REGEXP);
}
export function fillStringVariables(template: string, variables: Record<string, TxInterpretationVariable>) {
const variablesNames = extractVariables(template);
let result = template;
variablesNames.forEach(name => {
if (variables[name] && variables[name].type === 'string') {
result = result.replace(`{${ name }}`, variables[name].value as string);
}
});
return result;
}
import { Flex, Link, useBoolean } from '@chakra-ui/react';
import { Flex, Text, Link, useBoolean } from '@chakra-ui/react';
import React from 'react';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
interface Props {
items: Array<{total: { token_id: string} }>;
items: Array<{total: { token_id: string | null} }>;
tokenAddress: string;
isLoading?: boolean;
}
......@@ -14,14 +14,20 @@ const TxStateTokenIdList = ({ items, tokenAddress, isLoading }: Props) => {
return (
<Flex flexDir="column" rowGap={ 2 }>
{ items.slice(0, isCut ? 3 : items.length).map((item, index) => (
<NftEntity
key={ index }
hash={ tokenAddress }
id={ item.total.token_id }
isLoading={ isLoading }
/>
)) }
{ items.slice(0, isCut ? 3 : items.length).map((item, index) => {
if (item.total.token_id !== null) {
return (
<NftEntity
key={ index }
hash={ tokenAddress }
id={ item.total.token_id }
isLoading={ isLoading }
/>
);
} else {
return <Text key={ index } color="text_secondary">N/A</Text>;
}
}) }
{ items.length > 3 && (
<Link
fontWeight={ 400 }
......
......@@ -26,7 +26,7 @@ const UserOpsContent = ({ query, showTx = true, showSender = true }: Props) => {
<Hide below="lg" ssr={ false }>
<UserOpsTable
items={ query.data.items }
top={ query.pagination.isVisible ? 0 : 80 }
top={ query.pagination.isVisible ? 80 : 0 }
isLoading={ query.isPlaceholderData }
showTx={ showTx }
showSender={ showSender }
......
......@@ -38,7 +38,7 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => {
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.number + increment);
router.push({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: nextId } }, undefined);
router.push({ pathname: '/batches/[number]', query: { number: nextId } }, undefined);
}, [ data, router ]);
if (isError) {
......@@ -105,7 +105,7 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => {
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ data.transactions.length } transaction{ data.transactions.length === 1 ? '' : 's' }
</LinkInternal>
</Skeleton>
......
......@@ -51,7 +51,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
fontWeight={ 600 }
>
......
......@@ -44,7 +44,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td verticalAlign="middle">
<LinkInternal
href={ route({ pathname: '/zkevm-l2-txn-batch/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
......
......@@ -10365,6 +10365,11 @@ focus-lock@^0.11.6:
dependencies:
tslib "^2.0.3"
focus-visible@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
......
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