Commit c087a678 authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'marketplace-admin-api' into suggest-ideas-button

parents 9f012489 1ee81f5b
......@@ -115,11 +115,58 @@ jobs:
run: yarn --frozen-lockfile --ignore-optional
- name: Run Jest
run: yarn test:jest
run: yarn test:jest --onlyChanged=${{ github.event_name == 'pull_request' }} --passWithNoTests
pw_affected_tests:
name: Resolve affected Playwright tests
runs-on: ubuntu-latest
needs: [ code_quality, envs_validation ]
if: github.event_name == 'pull_request'
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Install script dependencies
run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile
- name: Run script
run: yarn test:pw:detect-affected
- name: Upload result file
uses: actions/upload-artifact@v4
with:
name: playwright-affected-tests
path: ./playwright/affected-tests.txt
retention-days: 3
pw_tests:
name: 'Playwright tests / Project: ${{ matrix.project }}'
needs: [ code_quality, envs_validation ]
needs: [ code_quality, envs_validation, pw_affected_tests ]
if: |
always() &&
needs.code_quality.result == 'success' &&
needs.envs_validation.result == 'success' &&
(needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.41.1-focal
......@@ -156,8 +203,15 @@ jobs:
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile --ignore-optional
- name: Download affected tests list
if: ${{ needs.pw_affected_tests.result == 'success' }}
uses: actions/download-artifact@v4
with:
name: playwright-affected-tests
path: ./playwright
- name: Run PlayWright
run: yarn test:pw:ci
run: yarn test:pw:ci --affected=${{ github.event_name == 'pull_request' }} --pass-with-no-tests
env:
HOME: /root
PW_PROJECT: ${{ matrix.project }}
......
......@@ -51,5 +51,6 @@ yarn-error.log*
/playwright/.cache/
/playwright/.browser/
/playwright/envs.js
/playwright/affected-tests.txt
**.dec**
\ No newline at end of file
......@@ -155,6 +155,27 @@
"instanceLimit": 1
}
},
{
"type": "shell",
"command": "yarn test:pw:detect-affected",
"problemMatcher": [],
"label": "pw: detect affected",
"detail": "detect PW tests affected by changes in current branch",
"presentation": {
"reveal": "always",
"panel": "shared",
"focus": true,
"close": false,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiBlue",
"id": "diff"
},
"runOptions": {
"instanceLimit": 1
},
},
// JEST TESTS
{
......@@ -305,6 +326,7 @@
"options": [
"",
"--update-snapshots",
"--update-snapshots --affected",
"--ui",
],
"default": ""
......
import type { Feature } from './types';
import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders';
import { SUPPORTED_AD_TEXT_PROVIDERS } from 'types/client/adProviders';
import type { AdTextProviders } from 'types/client/adProviders';
import { getEnvValue } from '../utils';
const provider: AdTextProviders = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_AD_TEXT_PROVIDER') as AdTextProviders;
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'coinzilla';
return envValue && SUPPORTED_AD_TEXT_PROVIDERS.includes(envValue) ? envValue : 'coinzilla';
})();
const title = 'Text ads';
......
......@@ -13,7 +13,7 @@ export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel';
export { default as nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs';
export { default as optimisticRollup } from './optimisticRollup';
export { default as rollup } from './rollup';
export { default as safe } from './safe';
export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml';
......@@ -23,4 +23,3 @@ export { default as txInterpretation } from './txInterpretation';
export { default as userOps } from './userOps';
export { default as verifiedTokens } from './verifiedTokens';
export { default as web3Wallet } from './web3Wallet';
export { default as zkEvmRollup } from './zkEvmRollup';
import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import { getEnvValue } from '../utils';
const type = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE');
return ROLLUP_TYPES.find((type) => type === envValue);
})();
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL');
const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL');
const title = 'Rollup (L2) chain';
const config: Feature<{ L1BaseUrl: string; withdrawalUrl: string }> = (() => {
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_L1_BASE_URL');
const withdrawalUrl = getEnvValue('NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL');
const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string }> = (() => {
if (
getEnvValue('NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK') === 'true' &&
L1BaseUrl &&
withdrawalUrl
) {
if (type && L1BaseUrl) {
return Object.freeze({
title,
isEnabled: true,
type,
L1BaseUrl,
withdrawalUrl,
L2WithdrawalUrl,
});
}
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const title = 'ZkEVM rollup (L2) chain';
const config: Feature<{ L1BaseUrl: string; withdrawalUrl?: string }> = (() => {
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_L1_BASE_URL');
const isZkEvm = getEnvValue('NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK') === 'true';
if (isZkEvm && L1BaseUrl) {
return Object.freeze({
title,
isEnabled: true,
L1BaseUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -43,6 +43,7 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
......@@ -44,8 +44,6 @@ NEXT_PUBLIC_APP_INSTANCE=jest
NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=false
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=false
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
......
......@@ -47,6 +47,6 @@ NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
# Set of ENVs for zkevm (dev only)
# https://eth.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME='OP Goerli'
NEXT_PUBLIC_NETWORK_SHORT_NAME='OP Goerli'
NEXT_PUBLIC_NETWORK_ID=420
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://goerli.optimism.io
# api configuration
NEXT_PUBLIC_API_HOST=optimism-goerli.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json
## footer
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=true
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_L1_BASE_URL=https://eth-goerli.blockscout.com/
......@@ -41,8 +41,6 @@ NEXT_PUBLIC_APP_ENV=testing
NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=false
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=false
NEXT_PUBLIC_AD_BANNER_PROVIDER=slise
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100
......
......@@ -47,5 +47,5 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=https://polygon.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE=zkEvm
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://polygon.blockscout.com
/node_modules
\ No newline at end of file
/* eslint-disable no-console */
const { execSync } = require('child_process');
const dependencyTree = require('dependency-tree');
const fs = require('fs');
const path = require('path');
const ROOT_DIR = path.resolve(__dirname, '../../../');
const TARGET_FILE = path.resolve(ROOT_DIR, './playwright/affected-tests.txt');
const NON_EXISTENT_DEPS = [];
const DIRECTORIES_WITH_TESTS = [
path.resolve(ROOT_DIR, './ui'),
];
function getAllPwFilesInDirectory(directory) {
const files = fs.readdirSync(directory, { recursive: true });
return files
.filter((file) => file.endsWith('.pw.tsx'))
.map((file) => path.join(directory, file));
}
function getFileDeps(filename, changedNpmModules) {
return dependencyTree.toList({
filename,
directory: ROOT_DIR,
filter: (path) => {
if (path.indexOf('node_modules') === -1) {
return true;
}
if (changedNpmModules.some((module) => path.startsWith(module))) {
return true;
}
return false;
},
tsConfig: path.resolve(ROOT_DIR, './tsconfig.json'),
nonExistent: NON_EXISTENT_DEPS,
});
}
async function getChangedFiles() {
const command = process.env.CI ?
`git diff --name-only origin/${ process.env.GITHUB_BASE_REF } ${ process.env.GITHUB_SHA } -- ${ ROOT_DIR }` :
`git diff --name-only main $(git branch --show-current) -- ${ ROOT_DIR }`;
console.log('Executing command: ', command);
const files = execSync(command)
.toString()
.trim()
.split('\n')
.filter(Boolean);
return files.map((file) => path.join(ROOT_DIR, file));
}
function checkChangesInChakraTheme(changedFiles) {
const themeDir = path.resolve(ROOT_DIR, './theme');
return changedFiles.some((file) => file.startsWith(themeDir));
}
function checkChangesInSvgSprite(changedFiles) {
const iconDir = path.resolve(ROOT_DIR, './icons');
const areIconsChanged = changedFiles.some((file) => file.startsWith(iconDir));
if (!areIconsChanged) {
return false;
}
const svgNamesFile = path.resolve(ROOT_DIR, './public/icons/name.d.ts');
const areSvgNamesChanged = changedFiles.some((file) => file === svgNamesFile);
if (!areSvgNamesChanged) {
// If only the icons have changed and not the names in the SVG file, we will need to run all tests.
// This is because we cannot correctly identify the test files that depend on these changes.
return true;
}
// If the icon names have changed, then there should be changes in the components that use them.
// Otherwise, typescript would complain about that.
return false;
}
function createTargetFile(content) {
fs.writeFileSync(TARGET_FILE, content);
}
function getPackageJsonUpdatedProps(packageJsonFile) {
const command = process.env.CI ?
`git diff --unified=0 origin/${ process.env.GITHUB_BASE_REF } ${ process.env.GITHUB_SHA } -- ${ packageJsonFile }` :
`git diff --unified=0 main $(git branch --show-current) -- ${ packageJsonFile }`;
console.log('Executing command: ', command);
const changedLines = execSync(command)
.toString()
.trim()
.split('\n')
.filter(Boolean)
.filter((line) => line.startsWith('+ ') || line.startsWith('- '));
const changedProps = [ ...new Set(
changedLines
.map((line) => line.replaceAll(' ', '').replaceAll('+', '').replaceAll('-', ''))
.map((line) => line.split(':')[0].replaceAll('"', '')),
) ];
return changedProps;
}
function getUpdatedNpmModules(changedFiles) {
const packageJsonFile = path.resolve(ROOT_DIR, './package.json');
if (!changedFiles.includes(packageJsonFile)) {
return [];
}
try {
const packageJsonContent = JSON.parse(fs.readFileSync(packageJsonFile, 'utf-8'));
const usedNpmModules = [
...Object.keys(packageJsonContent.dependencies || {}),
...Object.keys(packageJsonContent.devDependencies || {}),
];
const updatedProps = getPackageJsonUpdatedProps(packageJsonFile);
return updatedProps.filter((prop) => usedNpmModules.includes(prop));
} catch (error) {}
}
async function run() {
// NOTES:
// - The absence of TARGET_FILE implies that all tests should be run.
// - The empty TARGET_FILE implies that no tests should be run.
const start = Date.now();
fs.unlink(TARGET_FILE, () => {});
const changedFiles = await getChangedFiles();
if (!changedFiles.length) {
createTargetFile('');
console.log('No changed files found. Exiting...');
return;
}
console.log('Changed files in the branch: ', changedFiles);
if (checkChangesInChakraTheme(changedFiles)) {
console.log('Changes in Chakra theme detected. It is advisable to run all test suites. Exiting...');
return;
}
if (checkChangesInSvgSprite(changedFiles)) {
console.log('There are some changes in the SVG sprite that cannot be linked to a specific component. It is advisable to run all test suites. Exiting...');
return;
}
let changedNpmModules = getUpdatedNpmModules(changedFiles);
if (!changedNpmModules) {
console.log('Some error occurred while detecting changed NPM modules. It is advisable to run all test suites. Exiting...');
return;
}
console.log('Changed NPM modules in the branch: ', changedNpmModules);
changedNpmModules = [
...changedNpmModules,
...changedNpmModules.map((module) => `@types/${ module }`), // there are some deps that are resolved to .d.ts files
].map((module) => path.resolve(ROOT_DIR, `./node_modules/${ module }`));
const allTestFiles = DIRECTORIES_WITH_TESTS.reduce((acc, dir) => {
return acc.concat(getAllPwFilesInDirectory(dir));
}, []);
const isDepChanged = (dep) => changedFiles.includes(dep) || changedNpmModules.some((module) => dep.startsWith(module));
const testFilesToRun = allTestFiles
.map((file) => ({ file, deps: getFileDeps(file, changedNpmModules) }))
.filter(({ deps }) => deps.some(isDepChanged));
const testFileNamesToRun = testFilesToRun.map(({ file }) => path.relative(ROOT_DIR, file));
if (!testFileNamesToRun.length) {
createTargetFile('');
console.log('No tests to run. Exiting...');
return;
}
createTargetFile(testFileNamesToRun.join('\n'));
const end = Date.now();
const testFilesToRunWithFilteredDeps = testFilesToRun.map(({ file, deps }) => ({
file,
deps: deps.filter(isDepChanged),
}));
console.log('Total time: ', ((end - start) / 1_000).toLocaleString());
console.log('Total test to run: ', testFileNamesToRun.length);
console.log('Tests to run with changed deps: ', testFilesToRunWithFilteredDeps);
console.log('Non existent deps: ', NON_EXISTENT_DEPS);
}
run();
{
"name": "affected-tests",
"version": "1.0.0",
"main": "index.js",
"author": "Vasilii (tom) Goriunov <tom@ohhhh.me>",
"license": "MIT",
"dependencies": {
"dependency-tree": "10.0.9"
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/parser@^7.21.8":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b"
integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==
"@dependents/detective-less@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@dependents/detective-less/-/detective-less-4.1.0.tgz#4a979ee7a6a79eb33602862d6a1263e30f98002e"
integrity sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg==
dependencies:
gonzales-pe "^4.3.0"
node-source-walk "^6.0.1"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@typescript-eslint/types@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/typescript-estree@^5.59.5":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
dependencies:
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
app-module-path@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"
integrity sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
ast-module-types@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-5.0.0.tgz#32b2b05c56067ff38e95df66f11d6afd6c9ba16b"
integrity sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
color-name@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
commander@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
dependency-tree@10.0.9:
version "10.0.9"
resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-10.0.9.tgz#0c6c0dbeb0c5ec2cf83bf755f30e9cb12e7b4ac7"
integrity sha512-dwc59FRIsht+HfnTVM0BCjJaEWxdq2YAvEDy4/Hn6CwS3CBWMtFnL3aZGAkQn3XCYxk/YcTDE4jX2Q7bFTwCjA==
dependencies:
commander "^10.0.1"
filing-cabinet "^4.1.6"
precinct "^11.0.5"
typescript "^5.0.4"
detective-amd@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/detective-amd/-/detective-amd-5.0.2.tgz#579900f301c160efe037a6377ec7e937434b2793"
integrity sha512-XFd/VEQ76HSpym80zxM68ieB77unNuoMwopU2TFT/ErUk5n4KvUTwW4beafAVUugrjV48l4BmmR0rh2MglBaiA==
dependencies:
ast-module-types "^5.0.0"
escodegen "^2.0.0"
get-amd-module-type "^5.0.1"
node-source-walk "^6.0.1"
detective-cjs@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/detective-cjs/-/detective-cjs-5.0.1.tgz#836ad51c6de4863efc7c419ec243694f760ff8b2"
integrity sha512-6nTvAZtpomyz/2pmEmGX1sXNjaqgMplhQkskq2MLrar0ZAIkHMrDhLXkRiK2mvbu9wSWr0V5/IfiTrZqAQMrmQ==
dependencies:
ast-module-types "^5.0.0"
node-source-walk "^6.0.0"
detective-es6@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/detective-es6/-/detective-es6-4.0.1.tgz#38d5d49a6d966e992ef8f2d9bffcfe861a58a88a"
integrity sha512-k3Z5tB4LQ8UVHkuMrFOlvb3GgFWdJ9NqAa2YLUU/jTaWJIm+JJnEh4PsMc+6dfT223Y8ACKOaC0qcj7diIhBKw==
dependencies:
node-source-walk "^6.0.1"
detective-postcss@^6.1.3:
version "6.1.3"
resolved "https://registry.yarnpkg.com/detective-postcss/-/detective-postcss-6.1.3.tgz#51a2d4419327ad85d0af071c7054c79fafca7e73"
integrity sha512-7BRVvE5pPEvk2ukUWNQ+H2XOq43xENWbH0LcdCE14mwgTBEAMoAx+Fc1rdp76SmyZ4Sp48HlV7VedUnP6GA1Tw==
dependencies:
is-url "^1.2.4"
postcss "^8.4.23"
postcss-values-parser "^6.0.2"
detective-sass@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/detective-sass/-/detective-sass-5.0.3.tgz#63e54bc9b32f4bdbd9d5002308f9592a3d3a508f"
integrity sha512-YsYT2WuA8YIafp2RVF5CEfGhhyIVdPzlwQgxSjK+TUm3JoHP+Tcorbk3SfG0cNZ7D7+cYWa0ZBcvOaR0O8+LlA==
dependencies:
gonzales-pe "^4.3.0"
node-source-walk "^6.0.1"
detective-scss@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-4.0.3.tgz#79758baa0158f72bfc4481eb7e21cc3b5f1ea6eb"
integrity sha512-VYI6cHcD0fLokwqqPFFtDQhhSnlFWvU614J42eY6G0s8c+MBhi9QAWycLwIOGxlmD8I/XvGSOUV1kIDhJ70ZPg==
dependencies:
gonzales-pe "^4.3.0"
node-source-walk "^6.0.1"
detective-stylus@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/detective-stylus/-/detective-stylus-4.0.0.tgz#ce97b6499becdc291de7b3c11df8c352c1eee46e"
integrity sha512-TfPotjhszKLgFBzBhTOxNHDsutIxx9GTWjrL5Wh7Qx/ydxKhwUrlSFeLIn+ZaHPF+h0siVBkAQSuy6CADyTxgQ==
detective-typescript@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/detective-typescript/-/detective-typescript-11.1.0.tgz#2deea5364cae1f0d9d3688bc596e662b049438cc"
integrity sha512-Mq8egjnW2NSCkzEb/Az15/JnBI/Ryyl6Po0Y+0mABTFvOS6DAyUGRZqz1nyhu4QJmWWe0zaGs/ITIBeWkvCkGw==
dependencies:
"@typescript-eslint/typescript-estree" "^5.59.5"
ast-module-types "^5.0.0"
node-source-walk "^6.0.1"
typescript "^5.0.4"
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
enhanced-resolve@^5.14.1:
version "5.15.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
escodegen@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
dependencies:
esprima "^4.0.1"
estraverse "^5.2.0"
esutils "^2.0.2"
optionalDependencies:
source-map "~0.6.1"
eslint-visitor-keys@^3.3.0:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
fast-glob@^3.2.9:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.0.tgz#ca5e1a90b5e68f97fc8b61330d5819b82f5fab03"
integrity sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==
dependencies:
reusify "^1.0.4"
filing-cabinet@^4.1.6:
version "4.1.6"
resolved "https://registry.yarnpkg.com/filing-cabinet/-/filing-cabinet-4.1.6.tgz#8d6d12cf3a84365bbd94e1cbf07d71c113420dd2"
integrity sha512-C+HZbuQTER36sKzGtUhrAPAoK6+/PrrUhYDBQEh3kBRdsyEhkLbp1ML8S0+6e6gCUrUlid+XmubxJrhvL2g/Zw==
dependencies:
app-module-path "^2.2.0"
commander "^10.0.1"
enhanced-resolve "^5.14.1"
is-relative-path "^1.0.2"
module-definition "^5.0.1"
module-lookup-amd "^8.0.5"
resolve "^1.22.3"
resolve-dependency-path "^3.0.2"
sass-lookup "^5.0.1"
stylus-lookup "^5.0.1"
tsconfig-paths "^4.2.0"
typescript "^5.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-amd-module-type@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/get-amd-module-type/-/get-amd-module-type-5.0.1.tgz#bef38ea3674e1aa1bda9c59c8b0da598582f73f2"
integrity sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw==
dependencies:
ast-module-types "^5.0.0"
node-source-walk "^6.0.1"
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob@^7.2.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.2.9"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^3.0.0"
gonzales-pe@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
dependencies:
minimist "^1.2.5"
graceful-fs@^4.2.4:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
hasown@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
dependencies:
function-bind "^1.1.2"
ignore@^5.2.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
is-core-module@^2.13.0:
version "2.13.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
hasown "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
is-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==
is-relative-path@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-relative-path/-/is-relative-path-1.0.2.tgz#091b46a0d67c1ed0fe85f1f8cfdde006bb251d46"
integrity sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA==
is-url-superb@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2"
integrity sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==
is-url@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
json5@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies:
braces "^3.0.2"
picomatch "^2.3.1"
minimatch@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
module-definition@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-5.0.1.tgz#62d1194e5d5ea6176b7dc7730f818f466aefa32f"
integrity sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA==
dependencies:
ast-module-types "^5.0.0"
node-source-walk "^6.0.1"
module-lookup-amd@^8.0.5:
version "8.0.5"
resolved "https://registry.yarnpkg.com/module-lookup-amd/-/module-lookup-amd-8.0.5.tgz#aaeea41979105b49339380ca3f7d573db78c32a5"
integrity sha512-vc3rYLjDo5Frjox8NZpiyLXsNWJ5BWshztc/5KSOMzpg9k5cHH652YsJ7VKKmtM4SvaxuE9RkrYGhiSjH3Ehow==
dependencies:
commander "^10.0.1"
glob "^7.2.3"
requirejs "^2.3.6"
requirejs-config-file "^4.0.0"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
node-source-walk@^6.0.0, node-source-walk@^6.0.1, node-source-walk@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-6.0.2.tgz#ba81bc4bc0f6f05559b084bea10be84c3f87f211"
integrity sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==
dependencies:
"@babel/parser" "^7.21.8"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
postcss-values-parser@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz#636edc5b86c953896f1bb0d7a7a6615df00fb76f"
integrity sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==
dependencies:
color-name "^1.1.4"
is-url-superb "^4.0.0"
quote-unquote "^1.0.0"
postcss@^8.4.23:
version "8.4.33"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
precinct@^11.0.5:
version "11.0.5"
resolved "https://registry.yarnpkg.com/precinct/-/precinct-11.0.5.tgz#3e15b3486670806f18addb54b8533e23596399ff"
integrity sha512-oHSWLC8cL/0znFhvln26D14KfCQFFn4KOLSw6hmLhd+LQ2SKt9Ljm89but76Pc7flM9Ty1TnXyrA2u16MfRV3w==
dependencies:
"@dependents/detective-less" "^4.1.0"
commander "^10.0.1"
detective-amd "^5.0.2"
detective-cjs "^5.0.1"
detective-es6 "^4.0.1"
detective-postcss "^6.1.3"
detective-sass "^5.0.3"
detective-scss "^4.0.3"
detective-stylus "^4.0.0"
detective-typescript "^11.1.0"
module-definition "^5.0.1"
node-source-walk "^6.0.2"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quote-unquote@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b"
integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==
requirejs-config-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc"
integrity sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==
dependencies:
esprima "^4.0.0"
stringify-object "^3.2.1"
requirejs@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
resolve-dependency-path@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-3.0.2.tgz#012816717bcbe8b846835da11af9d2beb5acef50"
integrity sha512-Tz7zfjhLfsvR39ADOSk9us4421J/1ztVBo4rWUkF38hgHK5m0OCZ3NxFVpqHRkjctnwVa15igEUHFJp8MCS7vA==
resolve@^1.22.3:
version "1.22.8"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
sass-lookup@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-5.0.1.tgz#1f01d7ff21e09d8c9dcf8d05b3fca28f2f96e6ed"
integrity sha512-t0X5PaizPc2H4+rCwszAqHZRtr4bugo4pgiCvrBFvIX0XFxnr29g77LJcpyj9A0DcKf7gXMLcgvRjsonYI6x4g==
dependencies:
commander "^10.0.1"
semver@^7.3.7:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
stringify-object@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629"
integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==
dependencies:
get-own-enumerable-property-symbols "^3.0.0"
is-obj "^1.0.1"
is-regexp "^1.0.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
stylus-lookup@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-5.0.1.tgz#3c4d116c3b1e8e1a8169c0d9cd20e608595560f4"
integrity sha512-tLtJEd5AGvnVy4f9UHQMw4bkJJtaAcmo54N+ovQBjDY3DuWyK9Eltxzr5+KG0q4ew6v2EHyuWWNnHeiw/Eo7rQ==
dependencies:
commander "^10.0.1"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tsconfig-paths@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c"
integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==
dependencies:
json5 "^2.2.2"
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
typescript@^5.0.4:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
......@@ -15,6 +15,7 @@ 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';
import { ROLLUP_TYPES } from '../../../types/client/rollup';
import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token';
import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation';
import type { WalletType } from '../../../types/client/wallets';
......@@ -123,23 +124,20 @@ const beaconChainSchema = yup
const rollupSchema = yup
.object()
.shape({
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: yup.boolean(),
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: yup
NEXT_PUBLIC_ROLLUP_TYPE: yup.string().oneOf(ROLLUP_TYPES),
NEXT_PUBLIC_ROLLUP_L1_BASE_URL: yup
.string()
.when('NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', {
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value,
then: (schema) => schema.test(urlTest).required(),
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL cannot not be used if NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK is not set to "true"'),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined'),
}),
NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK: yup.boolean(),
NEXT_PUBLIC_L1_BASE_URL: yup
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL: yup
.string()
.when([ 'NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK' ], {
is: (isOptimistic?: boolean, isZk?: boolean) => isOptimistic || isZk,
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value === 'optimistic',
then: (schema) => schema.test(urlTest).required(),
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_L1_BASE_URL cannot not be used if NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK or NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK is not set to "true"'),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined'),
}),
});
......
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=https://example.com
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL=https://example.com
\ No newline at end of file
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com
\ No newline at end of file
......@@ -192,9 +192,9 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer-test.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: "true"
NEXT_PUBLIC_L1_BASE_URL: https://eth-goerli.blockscout.com/
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_ROLLUP_TYPE: optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL: https://eth-goerli.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
envFromSecret:
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
......
......@@ -66,9 +66,9 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK: "true"
NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_L1_BASE_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
envFromSecret:
......
# Deprecated environment variables
| Variable | Type | Description | Compulsoriness | Default value | Example value | Deprecated in version | Comment |
| --- | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK | `boolean` | Set to true for optimistic L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE |
| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE |
| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL |
......@@ -34,8 +34,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [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)
- [Rollup chain](ENVS.md#rollup-chain)
- [Export data to CSV file](ENVS.md#export-data-to-csv-file)
- [Google analytics](ENVS.md#google-analytics)
- [Mixpanel analytics](ENVS.md#mixpanel-analytics)
......@@ -332,7 +331,7 @@ This feature is **enabled by default** with the `slise` ads provider. To switch
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `none` | Ads provider | - | `slise` | `coinzilla` |
| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `hype` \| `none` | Ads provider | - | `slise` | `coinzilla` |
| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` |
| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` |
......@@ -365,22 +364,13 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp;
### Optimistic rollup (L2) chain
### Rollup chain
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK | `boolean` | Set to true for optimistic L2 solutions | Required | - | `true` |
| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` |
&nbsp;
### ZkEvm rollup (L2) chain
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` |
| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'zkEvm' ` | Rollup chain type | Required | - | `'optimistic'` |
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | - | - | `https://app.optimism.io/bridge/withdraw` |
&nbsp;
......@@ -438,7 +428,8 @@ This feature is **always enabled**, but you can configure its behavior by passin
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app | Required | - | `https://example.com/marketplace_config.json` |
| NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app. Can be replaced with NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | Required | - | `https://example.com/marketplace_config.json` |
| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url. Can be used instead of NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | - | - | `https://admin-rs.services.blockscout.com` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | Required | - | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM | `string` | Link to form where users can suggest ideas for the marketplace | - | - | `https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form` |
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` | See in [Blockchain parameters](ENVS.md#blockchain-parameters) section | Required | - | `https://core.poa.network` |
......
......@@ -34,7 +34,7 @@ describe('falsy query parameters', () => {
test('builds URL with array-like query parameters', () => {
const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: [ '0x11', '0x22' ], sort: 'asc' });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx%5B0%5D=0x11&includeTx%5B1%5D=0x22&sort=asc');
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=0x11%2C0x22&sort=asc');
});
test('builds URL for resource with custom API endpoint', () => {
......
......@@ -19,13 +19,7 @@ export default function buildUrl<R extends ResourceName>(
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
// there are some pagination params that can be null or false for the next page
if (value !== undefined && value !== '') {
if (Array.isArray(value)) {
value.forEach((v, i) => url.searchParams.append(`${ key }[${ i }]`, String(v)));
} else {
url.searchParams.append(key, String(value));
}
}
value !== undefined && value !== '' && url.searchParams.append(key, String(value));
});
return url.toString();
......
......@@ -47,11 +47,14 @@ import type {
} from 'types/api/ens';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits';
import type { L2OutputRootsResponse } from 'types/api/l2OutputRoots';
import type { L2TxnBatchesResponse } from 'types/api/l2TxnBatches';
import type { L2WithdrawalsResponse } from 'types/api/l2Withdrawals';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type {
OptimisticL2DepositsResponse,
OptimisticL2DepositsItem,
OptimisticL2OutputRootsResponse,
OptimisticL2TxnBatchesResponse,
OptimisticL2WithdrawalsResponse,
} from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
......@@ -81,11 +84,15 @@ import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'ty
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import type { ArrayElement } from 'types/utils';
import config from 'configs/app';
const marketplaceFeature = getFeaturePayload(config.features.marketplace);
const marketplaceApi = marketplaceFeature && 'api' in marketplaceFeature ? marketplaceFeature.api : undefined;
export interface ApiResource {
path: ResourcePath;
endpoint?: string;
......@@ -221,6 +228,20 @@ export const RESOURCES = {
basePath: getFeaturePayload(config.features.sol2uml)?.api.basePath,
},
// MARKETPLACE
marketplace_dapps: {
path: '/api/v1/chains/:chainId/marketplace/dapps',
pathParams: [ 'chainId' as const ],
endpoint: marketplaceApi?.endpoint,
basePath: marketplaceApi?.basePath,
},
marketplace_dapp: {
path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId',
pathParams: [ 'chainId' as const, 'dappId' as const ],
endpoint: marketplaceApi?.endpoint,
basePath: marketplaceApi?.basePath,
},
// BLOCKS, TXS
blocks: {
path: '/api/v2/blocks',
......@@ -688,7 +709,7 @@ Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
Q extends 'homepage_deposits' ? Array<OptimisticL2DepositsItem> :
Q extends 'homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'homepage_zkevm_latest_batch' ? number :
......@@ -752,10 +773,10 @@ Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'l2_output_roots' ? L2OutputRootsResponse :
Q extends 'l2_withdrawals' ? L2WithdrawalsResponse :
Q extends 'l2_deposits' ? L2DepositsResponse :
Q extends 'l2_txn_batches' ? L2TxnBatchesResponse :
Q extends 'l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'l2_output_roots_count' ? number :
Q extends 'l2_withdrawals_count' ? number :
Q extends 'l2_deposits_count' ? number :
......@@ -772,6 +793,8 @@ Q extends 'domains_lookup' ? EnsDomainLookupResponse :
Q extends 'user_ops' ? UserOpsResponse :
Q extends 'user_op' ? UserOp :
Q extends 'user_ops_account' ? UserOpsAccount :
Q extends 'marketplace_dapps' ? Array<MarketplaceAppOverview> :
Q extends 'marketplace_dapp' ? MarketplaceAppOverview :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -67,7 +67,9 @@ export default function useNavItems(): ReturnType {
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]',
} : null;
if (config.features.zkEvmRollup.isEnabled) {
const rollupFeature = config.features.rollup;
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
blockchainNavItems = [
[
txs,
......@@ -86,7 +88,7 @@ export default function useNavItems(): ReturnType {
ensLookup,
].filter(Boolean),
];
} else if (config.features.optimisticRollup.isEnabled) {
} else if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') {
blockchainNavItems = [
[
txs,
......
......@@ -5,8 +5,11 @@ import type { Route } from 'nextjs-routes';
import generate from './generate';
export default function update<R extends Route>(route: R, apiData: ApiData<R>) {
const { title, description } = generate(route, apiData);
const { title, description, opengraph } = generate(route, apiData);
window.document.title = title;
window.document.querySelector('meta[name="description"]')?.setAttribute('content', description);
window.document.querySelector('meta[property="og:title"]')?.setAttribute('content', opengraph.title);
opengraph.description &&
window.document.querySelector('meta[property="og:description"]')?.setAttribute('content', opengraph.description);
}
export default function shortenString(string: string | null) {
export default function shortenString(string: string | null, charNumber: number | undefined = 8) {
if (!string) {
return '';
}
if (string.length <= 7) {
if (string.length <= charNumber) {
return string;
}
return string.slice(0, 4) + '...' + string.slice(-4);
return string.slice(0, charNumber - 4) + '...' + string.slice(-4);
}
......@@ -6,7 +6,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2TxnBatches';
import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2';
export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
......
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2';
export const txnBatchData: ZkEvmL2TxnBatch = {
acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3',
......
import type { ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2';
export const txnBatchesData: ZkEvmL2TxnBatchesResponse = {
items: [
......
......@@ -3,30 +3,52 @@ import sha256 from 'crypto-js/sha256';
import type CspDev from 'csp-dev';
import { connectAdbutler, placeAd } from 'ui/shared/ad/adbutlerScript';
import { hypeInit } from 'ui/shared/ad/hypeBannerScript';
export function ad(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
// coinzilla
'coinzilla.com',
'*.coinzilla.com',
'https://request-global.czilladx.com',
// slise
'*.slise.xyz',
// hype
'api.hypelab.com',
],
'frame-src': [
// coinzilla
'https://request-global.czilladx.com',
],
'script-src': [
// coinzilla
'coinzillatag.com',
// adbutler
'servedbyadbutler.com',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd ?? '')) }'`,
// slise
'*.slise.xyz',
//hype
`'sha256-${ Base64.stringify(sha256(hypeInit ?? '')) }'`,
'https://api.hypelab.com',
'd1q98dzwj6s2rb.cloudfront.net',
],
'img-src': [
'servedbyadbutler.com',
// coinzilla
'cdn.coinzilla.io',
// adbutler
'servedbyadbutler.com',
],
'font-src': [
// coinzilla
'https://request-global.czilladx.com',
],
};
......
import type { GetServerSideProps } from 'next';
import config from 'configs/app';
const rollupFeature = config.features.rollup;
export type Props = {
cookies: string;
......@@ -49,7 +50,10 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => {
};
export const withdrawals: GetServerSideProps<Props> = async(context) => {
if (!config.features.beaconChain.isEnabled && !config.features.optimisticRollup.isEnabled) {
if (
!config.features.beaconChain.isEnabled &&
!(rollupFeature.isEnabled && rollupFeature.type === 'optimistic')
) {
return {
notFound: true,
};
......@@ -59,7 +63,7 @@ export const withdrawals: GetServerSideProps<Props> = async(context) => {
};
export const rollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.optimisticRollup.isEnabled && !config.features.zkEvmRollup.isEnabled) {
if (!config.features.rollup.isEnabled) {
return {
notFound: true,
};
......@@ -69,7 +73,7 @@ export const rollup: GetServerSideProps<Props> = async(context) => {
};
export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.optimisticRollup.isEnabled) {
if (!(rollupFeature.isEnabled && rollupFeature.type === 'optimistic')) {
return {
notFound: true,
};
......@@ -79,7 +83,7 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
};
export const zkEvmRollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.zkEvmRollup.isEnabled) {
if (!(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm')) {
return {
notFound: true,
};
......
......@@ -38,9 +38,9 @@ declare module "nextjs-routes" {
| StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains">
| StaticRoute<"/output-roots">
| DynamicRoute<"/op/[hash]", { "hash": string }>
| StaticRoute<"/ops">
| StaticRoute<"/output-roots">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
......
......@@ -26,6 +26,7 @@
"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.41.1-focal ./tools/scripts/pw.docker.sh",
"test:pw:ci": "yarn test:pw --project=$PW_PROJECT",
"test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js",
"test:jest": "jest",
"test:jest:watch": "jest --watch",
"favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh"
......@@ -36,6 +37,7 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@growthbook/growthbook-react": "0.21.0",
"@hypelab/sdk-react": "^1.0.0",
"@metamask/post-message-stream": "^7.0.0",
"@metamask/providers": "^10.2.1",
"@monaco-editor/react": "^4.4.6",
......
......@@ -5,12 +5,19 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
const rollupFeature = config.features.rollup;
const Batches = dynamic(() => {
if (config.features.zkEvmRollup.isEnabled) {
return import('ui/pages/ZkEvmL2TxnBatches');
if (!rollupFeature.isEnabled) {
throw new Error('Rollup feature is not enabled.');
}
switch (rollupFeature.type) {
case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatches');
case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatches');
}
return import('ui/pages/L2TxnBatches');
}, { ssr: false });
const Page: NextPage = () => {
......
......@@ -4,7 +4,7 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const Deposits = dynamic(() => import('ui/pages/L2Deposits'), { ssr: false });
const Deposits = dynamic(() => import('ui/pages/OptimisticL2Deposits'), { ssr: false });
const Page: NextPage = () => {
return (
......
......@@ -4,7 +4,7 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const OutputRoots = dynamic(() => import('ui/pages/L2OutputRoots'), { ssr: false });
const OutputRoots = dynamic(() => import('ui/pages/OptimisticL2OutputRoots'), { ssr: false });
const Page: NextPage = () => {
return (
......
......@@ -5,12 +5,18 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
const rollupFeature = config.features.rollup;
const beaconChainFeature = config.features.beaconChain;
const Withdrawals = dynamic(() => {
if (config.features.optimisticRollup.isEnabled) {
return import('ui/pages/L2Withdrawals');
if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') {
return import('ui/pages/OptimisticL2Withdrawals');
}
return import('ui/pages/Withdrawals');
if (beaconChainFeature.isEnabled) {
return import('ui/pages/BeaconChainWithdrawals');
}
throw new Error('Withdrawals feature is not enabled.');
}, { ssr: false });
const Page: NextPage = () => {
......
......@@ -15,9 +15,9 @@ export const featureEnvs = {
{ name: 'NEXT_PUBLIC_HAS_BEACON_CHAIN', value: 'true' },
],
optimisticRollup: [
{ name: 'NEXT_PUBLIC_IS_OPTIMISTIC_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
{ name: 'NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' },
{ name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'optimistic' },
{ name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' },
{ name: 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' },
],
bridgedTokens: [
{
......@@ -32,9 +32,9 @@ export const featureEnvs = {
txInterpretation: [
{ name: 'NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER', value: 'blockscout' },
],
zkRollup: [
{ name: 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK', value: 'true' },
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
zkEvmRollup: [
{ name: 'NEXT_PUBLIC_ROLLUP_TYPE', value: 'zkEvm' },
{ name: 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', value: 'https://localhost:3101' },
],
userOps: [
{ name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' },
......
import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { L2OutputRootsItem } from 'types/api/l2OutputRoots';
import type { L2TxnBatchesItem } from 'types/api/l2TxnBatches';
import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals';
import type {
OptimisticL2DepositsItem,
OptimisticL2OutputRootsItem,
OptimisticL2TxnBatchesItem,
OptimisticL2WithdrawalsItem,
} from 'types/api/optimisticL2';
import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
import { TX_HASH } from './tx';
export const L2_DEPOSIT_ITEM: L2DepositsItem = {
export const L2_DEPOSIT_ITEM: OptimisticL2DepositsItem = {
l1_block_number: 9045233,
l1_block_timestamp: '2023-05-22T18:00:36.000000Z',
l1_tx_hash: TX_HASH,
......@@ -15,7 +17,7 @@ export const L2_DEPOSIT_ITEM: L2DepositsItem = {
l2_tx_hash: TX_HASH,
};
export const L2_WITHDRAWAL_ITEM: L2WithdrawalsItem = {
export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = {
challenge_period_end: null,
from: ADDRESS_PARAMS,
l1_tx_hash: TX_HASH,
......@@ -26,7 +28,7 @@ export const L2_WITHDRAWAL_ITEM: L2WithdrawalsItem = {
status: 'Ready to prove',
};
export const L2_TXN_BATCHES_ITEM: L2TxnBatchesItem = {
export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = {
epoch_number: 9103513,
l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [
......@@ -36,7 +38,7 @@ export const L2_TXN_BATCHES_ITEM: L2TxnBatchesItem = {
tx_count: 9,
};
export const L2_OUTPUT_ROOTS_ITEM: L2OutputRootsItem = {
export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = {
l1_block_number: 9103684,
l1_timestamp: '2023-06-01T15:26:12.000000Z',
l1_tx_hash: TX_HASH,
......
......@@ -53,6 +53,10 @@ export const GET_TRANSACTION_RECEIPT: TransactionReceipt = {
export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420);
export const GET_BALANCE = BigInt(42_000_000_000_000);
export const GET_TRANSACTIONS_COUNT = 42;
export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = {
baseFeePerGas: BigInt(11),
difficulty: BigInt(111),
......
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { TX_HASH } from './tx';
......
......@@ -10,7 +10,73 @@ dotenv \
yarn svg:build-sprite
# Check if the "--affected" argument is present in the script args
check_affected_flag() {
local affected_flag=false
for arg in "$@"; do
if [[ "$arg" = "--affected"* ]]; then
# Extract the value after the equals sign
is_affected_value=${is_affected_arg#*=}
if [ "$is_affected_value" != "false" ]; then
affected_flag=true
fi
break
fi
done
echo "$affected_flag"
}
# Remove the "--affected" argument from the script args
filter_arguments() {
local args=()
for arg in "$@"; do
if [[ "$arg" != "--affected"* ]]; then
args+=("$arg")
fi
done
echo "${args[@]}"
}
get_files_to_run() {
local is_affected=$1
local files_to_run=""
if [ "$is_affected" = true ]; then
affected_tests_file="./playwright/affected-tests.txt"
if [ -f "$affected_tests_file" ]; then
file_content=$(<"$affected_tests_file")
files_to_run="${file_content//$'\n'/$' '}"
if [ -z "$files_to_run" ]; then
exit 1
fi
fi
fi
echo "$files_to_run"
}
args=$(filter_arguments "$@")
affected_flag=$(check_affected_flag "$@")
files_to_run=$(get_files_to_run "$affected_flag")
if [ $? -eq 1 ]; then
echo "No affected tests found in the file. Exiting..."
exit 0
fi
echo "Running Playwright tests with the following arguments: $args"
echo "Affected flag: $affected_flag"
echo "Files to run: $files_to_run"
dotenv \
-v NODE_OPTIONS=\"--max-old-space-size=4096\" \
-e $config_file \
-- playwright test -c playwright-ct.config.ts "$@"
-- playwright test -c playwright-ct.config.ts $files_to_run $args
......@@ -39,7 +39,7 @@ export interface Address extends UserTags {
export interface AddressCounters {
transactions_count: string;
token_transfers_count: string;
gas_usage_count: string;
gas_usage_count: string | null;
validations_count: string | null;
}
......
export type L2DepositsItem = {
l1_block_number: number;
l1_tx_hash: string;
l1_block_timestamp: string;
l1_tx_origin: string;
l2_tx_gas_limit: string;
l2_tx_hash: string;
}
export type L2DepositsResponse = {
items: Array<L2DepositsItem>;
next_page_params: {
items_count: number;
l1_block_number: number;
tx_hash: string;
};
}
export type L2OutputRootsItem = {
l1_block_number: number;
l1_timestamp: string;
l1_tx_hash: string;
l2_block_number: number;
l2_output_index: number;
output_root: string;
}
export type L2OutputRootsResponse = {
items: Array<L2OutputRootsItem>;
next_page_params: {
index: number;
items_count: number;
};
}
export type L2TxnBatchesItem = {
epoch_number: number;
l1_tx_hashes: Array<string>;
l1_timestamp: string;
l2_block_number: number;
tx_count: number;
}
export type L2TxnBatchesResponse = {
items: Array<L2TxnBatchesItem>;
next_page_params: {
block_number: number;
items_count: number;
};
}
import type { AddressParam } from './addressParams';
export type L2WithdrawalsItem = {
'challenge_period_end': string | null;
'from': AddressParam | null;
'l1_tx_hash': string | null;
'l2_timestamp': string | null;
'l2_tx_hash': string;
'msg_nonce': number;
'msg_nonce_version': number;
'status': string;
}
export const WITHDRAWAL_STATUSES = [
'Waiting for state root',
'Ready to prove',
'In challenge period',
'Ready for relay',
'Relayed',
] as const;
export type L2WithdrawalStatus = typeof WITHDRAWAL_STATUSES[number];
export type L2WithdrawalsResponse = {
items: Array<L2WithdrawalsItem>;
'next_page_params': {
'items_count': number;
'nonce': string;
};
}
import type { AddressParam } from './addressParams';
export type OptimisticL2DepositsItem = {
l1_block_number: number;
l1_tx_hash: string;
l1_block_timestamp: string;
l1_tx_origin: string;
l2_tx_gas_limit: string;
l2_tx_hash: string;
}
export type OptimisticL2DepositsResponse = {
items: Array<OptimisticL2DepositsItem>;
next_page_params: {
items_count: number;
l1_block_number: number;
tx_hash: string;
};
}
export type OptimisticL2OutputRootsItem = {
l1_block_number: number;
l1_timestamp: string;
l1_tx_hash: string;
l2_block_number: number;
l2_output_index: number;
output_root: string;
}
export type OptimisticL2OutputRootsResponse = {
items: Array<OptimisticL2OutputRootsItem>;
next_page_params: {
index: number;
items_count: number;
};
}
export type OptimisticL2TxnBatchesItem = {
epoch_number: number;
l1_tx_hashes: Array<string>;
l1_timestamp: string;
l2_block_number: number;
tx_count: number;
}
export type OptimisticL2TxnBatchesResponse = {
items: Array<OptimisticL2TxnBatchesItem>;
next_page_params: {
block_number: number;
items_count: number;
};
}
export type OptimisticL2WithdrawalsItem = {
'challenge_period_end': string | null;
'from': AddressParam | null;
'l1_tx_hash': string | null;
'l2_timestamp': string | null;
'l2_tx_hash': string;
'msg_nonce': number;
'msg_nonce_version': number;
'status': string;
}
export const WITHDRAWAL_STATUSES = [
'Waiting for state root',
'Ready to prove',
'In challenge period',
'Ready for relay',
'Relayed',
] as const;
export type OptimisticL2WithdrawalStatus = typeof WITHDRAWAL_STATUSES[number];
export type OptimisticL2WithdrawalsResponse = {
items: Array<OptimisticL2WithdrawalsItem>;
'next_page_params': {
'items_count': number;
'nonce': string;
};
}
......@@ -2,7 +2,7 @@ import type { AddressParam } from './addressParams';
import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { L2WithdrawalStatus } from './l2Withdrawals';
import type { OptimisticL2WithdrawalStatus } from './optimisticL2';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';
......@@ -17,7 +17,7 @@ type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_pri
export interface OpWithdrawal {
l1_transaction_hash: string;
nonce: number;
status: L2WithdrawalStatus;
status: OptimisticL2WithdrawalStatus;
}
export type Transaction = {
......
import type { ArrayElement } from 'types/utils';
export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'none' ] as const;
export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'hype', 'none' ] as const;
export type AdBannerProviders = ArrayElement<typeof SUPPORTED_AD_BANNER_PROVIDERS>;
export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const;
......
import type { ArrayElement } from 'types/utils';
export const ROLLUP_TYPES = [
'optimistic',
'zkEvm',
] as const;
export type RollupType = ArrayElement<typeof ROLLUP_TYPES>;
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { WindowProvider } from 'wagmi';
import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters';
import * as tokensMock from 'mocks/address/tokens';
......@@ -15,6 +11,7 @@ import * as configs from 'playwright/utils/configs';
import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
import type { AddressQuery } from './utils/useAddressQuery';
const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
......@@ -40,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, ResourceError>}/>
<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>
</TestApp>,
{ hooksConfig },
);
......@@ -82,7 +79,7 @@ test('token', async({ mount, page }) => {
const component = await mount(
<TestApp>
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, ResourceError>}/>
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
......@@ -106,7 +103,7 @@ test('validator +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, ResourceError>}/>
<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>
</TestApp>,
{ hooksConfig },
);
......
import { Box, Text, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
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';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
......@@ -21,9 +16,11 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import AddressBalance from './details/AddressBalance';
import AddressNameInfo from './details/AddressNameInfo';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
interface Props {
addressQuery: UseQueryResult<TAddress, ResourceError>;
addressQuery: AddressQuery;
scrollRef?: React.RefObject<HTMLDivElement>;
}
......@@ -32,12 +29,9 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const addressHash = getQueryParamString(router.query.hash);
const countersQuery = useApiQuery('address_counters', {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash) && Boolean(addressQuery.data),
placeholderData: ADDRESS_COUNTERS,
},
const countersQuery = useAddressCountersQuery({
hash: addressHash,
addressQuery,
});
const handleCounterItemClick = React.useCallback(() => {
......@@ -47,7 +41,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
}, 500);
}, [ scrollRef ]);
const errorData = React.useMemo(() => ({
const error404Data = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
implementation_name: null,
......@@ -76,142 +70,151 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return <DataFetchAlert/>;
}
const data = addressQuery.isError ? errorData : addressQuery.data;
const data = addressQuery.isError ? error404Data : addressQuery.data;
if (!data) {
return null;
}
return (
<Grid
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressEntity
address={{ hash: data.creator_address_hash }}
truncation="constant"
noIcon
/>
<Text whiteSpace="pre"> at txn </Text>
<TxEntity hash={ data.creation_tx_hash } truncation="constant" noIcon noCopy={ false }/>
</DetailsInfoItem>
) }
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract"
columnGap={ 1 }
>
<AddressEntity
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
isLoading={ addressQuery.isPlaceholderData }
noIcon
/>
</DetailsInfoItem>
) }
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value"
alignSelf="center"
py={ 0 }
>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
<>
{ addressQuery.isDegradedData && <ServiceDegradationWarning isLoading={ addressQuery.isPlaceholderData } mb={ 6 }/> }
<Grid
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
{ addressQuery.data ? (
<AddressCounterItem
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
<AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressEntity
address={{ hash: data.creator_address_hash }}
truncation="constant"
noIcon
/>
) :
0 }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_validated_blocks && (
<Text whiteSpace="pre"> at txn </Text>
<TxEntity hash={ data.creation_tx_hash } truncation="constant" noIcon noCopy={ false }/>
</DetailsInfoItem>
) }
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract"
columnGap={ 1 }
>
<AddressEntity
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
isLoading={ addressQuery.isPlaceholderData }
noIcon
/>
</DetailsInfoItem>
) }
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value"
alignSelf="center"
py={ 0 }
>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator"
title="Transactions"
hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
) }
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
>
<BlockEntity
number={ data.block_number_balance_updated_at }
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
) }
{ countersQuery.data?.gas_usage_count && (
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
) }
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
) }
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
/>
</DetailsInfoItem>
) }
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
>
<BlockEntity
number={ data.block_number_balance_updated_at }
isLoading={ addressQuery.isPlaceholderData }
/>
</DetailsInfoItem>
) }
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
</>
);
};
......
......@@ -108,7 +108,7 @@ test.describe('socket', () => {
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS });
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
......@@ -144,7 +144,7 @@ test.describe('socket', () => {
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS });
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
......
......@@ -13,8 +13,8 @@ import AddressTokens from './AddressTokens';
const ADDRESS_HASH = addressMock.withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const API_URL_NFT = buildApiUrl('address_nfts', { hash: ADDRESS_HASH });
const API_URL_COLLECTIONS = buildApiUrl('address_collections', { hash: ADDRESS_HASH });
const API_URL_NFT = buildApiUrl('address_nfts', { hash: ADDRESS_HASH }) + '?type=';
const API_URL_COLLECTIONS = buildApiUrl('address_collections', { hash: ADDRESS_HASH }) + '?type=';
const nextPageParams = {
items_count: 50,
......
......@@ -9,8 +9,8 @@ import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem';
import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable';
const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
......@@ -32,7 +32,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
<>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => (
<WithdrawalsListItem
<BeaconChainWithdrawalsListItem
key={ item.index + Number(isPlaceholderData ? index : '') }
item={ item }
view="address"
......@@ -41,7 +41,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
)) }
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="address" top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
<BeaconChainWithdrawalsTable items={ data.items } view="address" top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null ;
......
......@@ -2,6 +2,7 @@ import { Checkbox, Flex, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import type { ChangeEvent } from 'react';
import React from 'react';
import { getAddress } from 'viem';
import type { SmartContractMethodOutput } from 'types/api/contract';
......@@ -50,7 +51,7 @@ const ContractMethodStatic = ({ data }: Props) => {
if (typeof data.value === 'string' && data.type === 'address' && data.value) {
return (
<AddressEntity
address={{ hash: data.value }}
address={{ hash: getAddress(data.value) }}
noIcon
/>
);
......
......@@ -16,6 +16,7 @@ interface Props {
address: string;
onClick: () => void;
isAddressQueryLoading: boolean;
isDegradedData: boolean;
}
const PROP_TO_TAB = {
......@@ -24,7 +25,7 @@ const PROP_TO_TAB = {
validations_count: 'blocks_validated',
};
const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading }: Props) => {
const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading, isDegradedData }: Props) => {
if (query.isPlaceholderData || isAddressQueryLoading) {
return <Skeleton h={ 5 } w="80px" borderRadius="full"/>;
}
......@@ -44,6 +45,11 @@ const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoadi
if (data === '0') {
return <span>0</span>;
}
if (isDegradedData) {
return <span>{ Number(data).toLocaleString() }</span>;
}
return (
<LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) } onClick={ onClick }>
{ Number(data).toLocaleString() }
......
......@@ -45,7 +45,7 @@ const TxInternalsListItem = ({
hash={ txnHash }
isLoading={ isLoading }
fontWeight={ 700 }
truncation="constant"
truncation="constant_long"
/>
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ dayjs(timestamp).fromNow() }</span>
......
......@@ -43,6 +43,7 @@ const AddressIntTxsTableItem = ({
isLoading={ isLoading }
fontWeight={ 700 }
noIcon
truncation="constant_long"
/>
{ timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
......
......@@ -43,6 +43,7 @@ const ERC20TokensTableItem = ({
<AddressEntity
address={{ hash: token.address }}
isLoading={ isLoading }
truncation="constant"
noIcon
/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading } opacity="0"/>
......
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { AddressCounters } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { publicClient } from 'lib/web3/client';
import { ADDRESS_COUNTERS } from 'stubs/address';
import { GET_TRANSACTIONS_COUNT } from 'stubs/RPC';
import type { AddressQuery } from './useAddressQuery';
type RpcResponseType = [
number | null,
];
export type AddressCountersQuery = UseQueryResult<AddressCounters, ResourceError<{ status: number }>> & {
isDegradedData: boolean;
};
interface Params {
hash: string;
addressQuery: AddressQuery;
}
export default function useAddressQuery({ hash, addressQuery }: Params): AddressCountersQuery {
const enabled = Boolean(hash) && !addressQuery.isPlaceholderData;
const apiQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
pathParams: { hash },
queryOptions: {
enabled: enabled && !addressQuery.isDegradedData,
placeholderData: ADDRESS_COUNTERS,
refetchOnMount: false,
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, AddressCounters | null>({
queryKey: [ 'RPC', 'address_counters', { hash } ],
queryFn: async() => {
const txCount = publicClient.getTransactionCount({ address: hash as `0x${ string }` }).catch(() => null);
return Promise.all([
txCount,
]);
},
select: (response) => {
const [ txCount ] = response;
return {
transactions_count: txCount?.toString() ?? '0',
token_transfers_count: '0',
gas_usage_count: null,
validations_count: null,
};
},
placeholderData: [ GET_TRANSACTIONS_COUNT ],
enabled: enabled && (addressQuery.isDegradedData || apiQuery.isError),
retry: false,
refetchOnMount: false,
});
const isRpcQuery = Boolean((addressQuery.isDegradedData || apiQuery.isError) && rpcQuery.data);
const query = isRpcQuery ? rpcQuery as UseQueryResult<AddressCounters, ResourceError<{ status: number }>> : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import { publicClient } from 'lib/web3/client';
import { ADDRESS_INFO } from 'stubs/address';
import { GET_BALANCE } from 'stubs/RPC';
type RpcResponseType = [
bigint | null,
];
export type AddressQuery = UseQueryResult<Address, ResourceError<{ status: number }>> & {
isDegradedData: boolean;
};
interface Params {
hash: string;
}
export default function useAddressQuery({ hash }: Params): AddressQuery {
const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false);
const apiQuery = useApiQuery<'address', { status: number }>('address', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
refetchOnMount: false,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
}
return retry(failureCount, error);
},
refetchInterval: (): number | false => {
return isRefetchEnabled ? 15 * SECOND : false;
},
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, Address | null>({
queryKey: [ 'RPC', 'address', { hash } ],
queryFn: async() => {
if (!publicClient) {
throw new Error('No public RPC client');
}
const balance = publicClient.getBalance({ address: hash as `0x${ string }` }).catch(() => null);
return Promise.all([
balance,
]);
},
select: (response) => {
const [ balance ] = response;
if (!balance) {
return null;
}
return {
hash,
block_number_balance_updated_at: null,
coin_balance: balance.toString(),
creator_address_hash: null,
creation_tx_hash: null,
exchange_rate: null,
ens_domain_name: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
token: null,
watchlist_address_id: null,
private_tags: null,
public_tags: null,
watchlist_names: null,
};
},
placeholderData: [ GET_BALANCE ],
enabled: apiQuery.isError || apiQuery.errorUpdateCount > 0,
retry: false,
refetchOnMount: false,
});
React.useEffect(() => {
if (apiQuery.isPlaceholderData || !publicClient) {
return;
}
if (apiQuery.isError && apiQuery.errorUpdateCount === 1) {
setRefetchEnabled(true);
} else if (!apiQuery.isError) {
setRefetchEnabled(false);
}
}, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]);
React.useEffect(() => {
if (!rpcQuery.isPlaceholderData && !rpcQuery.data) {
setRefetchEnabled(false);
}
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0 && rpcQuery.data && publicClient);
const query = isRpcQuery ? rpcQuery as UseQueryResult<Address, ResourceError<{ status: number }>> : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
......@@ -157,7 +157,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
}
case 'INVALID_SIGNER_ERROR': {
const signer = shortenString(formState.errors.root.message || '');
const expectedSigners = [ contractCreator, contractOwner ].filter(Boolean).map(shortenString).join(', ');
const expectedSigners = [ contractCreator, contractOwner ].filter(Boolean).map(s => shortenString(s)).join(', ');
return (
<Box>
<span>This address </span>
......
......@@ -35,6 +35,7 @@ const AddressesListItem = ({
isLoading={ isLoading }
fontWeight={ 700 }
mr={ 2 }
truncation="constant"
/>
<Skeleton isLoaded={ !isLoading } fontSize="sm" ml="auto" minW={ 6 } color="text_secondary">
<span>{ index }</span>
......
......@@ -34,7 +34,7 @@ interface Props {
query: BlockQuery;
}
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const rollupFeature = config.features.rollup;
const BlockDetails = ({ query }: Props) => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
......@@ -73,7 +73,7 @@ const BlockDetails = ({ query }: Props) => {
const validatorTitle = getNetworkValidatorTitle();
const rewardBreakDown = (() => {
if (isRollup || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) {
if (rollupFeature.isEnabled || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) {
return null;
}
......@@ -107,7 +107,7 @@ const BlockDetails = ({ query }: Props) => {
})();
const verificationTitle = (() => {
if (config.features.zkEvmRollup.isEnabled) {
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
return 'Sequenced by';
}
......@@ -205,7 +205,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
) }
{ !isRollup && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && (
{ !rollupFeature.isEnabled && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && (
<DetailsInfoItem
title="Block reward"
hint={
......
......@@ -3,8 +3,8 @@ import React from 'react';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import WithdrawalsList from 'ui/withdrawals/WithdrawalsList';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
import BeaconChainWithdrawalsList from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsList';
import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable';
type Props = {
blockWithdrawalsQuery: QueryWithPagesResult<'block_withdrawals'>;
......@@ -14,14 +14,14 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
const content = blockWithdrawalsQuery.data?.items ? (
<>
<Show below="lg" ssr={ false }>
<WithdrawalsList
<BeaconChainWithdrawalsList
items={ blockWithdrawalsQuery.data.items }
isLoading={ blockWithdrawalsQuery.isPlaceholderData }
view="block"
/>
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable
<BeaconChainWithdrawalsTable
items={ blockWithdrawalsQuery.data.items }
isLoading={ blockWithdrawalsQuery.isPlaceholderData }
top={ blockWithdrawalsQuery.pagination.isVisible ? 80 : 0 }
......
......@@ -28,7 +28,7 @@ interface Props {
enableTimeIncrement?: boolean;
}
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const isRollup = config.features.rollup.isEnabled;
const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
......@@ -63,6 +63,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<AddressEntity
address={ data.miner }
isLoading={ isLoading }
truncation="constant"
/>
</Flex>
) }
......
......@@ -28,7 +28,7 @@ const GAS_COL_WEIGHT = 33;
const REWARD_COL_WEIGHT = 22;
const FEES_COL_WEIGHT = 22;
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const isRollup = config.features.rollup.isEnabled;
const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, socketInfoAlert }: Props) => {
......
......@@ -25,7 +25,7 @@ interface Props {
enableTimeIncrement?: boolean;
}
const isRollup = config.features.optimisticRollup.isEnabled || config.features.zkEvmRollup.isEnabled;
const isRollup = config.features.rollup.isEnabled;
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
......@@ -70,6 +70,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<AddressEntity
address={ data.miner }
isLoading={ isLoading }
truncation="constant"
/>
</Td>
) }
......
......@@ -2,7 +2,7 @@ import { Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
......@@ -12,14 +12,14 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2DepositsItem; isLoading?: boolean };
type Props = { item: OptimisticL2DepositsItem; isLoading?: boolean };
const DepositsListItem = ({ item, isLoading }: Props) => {
const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -44,6 +44,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
hash={ item.l2_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
......@@ -59,6 +60,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
hash={ item.l1_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
......@@ -68,6 +70,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
isLoading={ isLoading }
noCopy
truncation="constant"
/>
</ListItemMobileGrid.Value>
......@@ -80,4 +83,4 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
);
};
export default DepositsListItem;
export default OptimisticDepositsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import DepositsTableItem from './DepositsTableItem';
import OptimisticDepositsTableItem from './OptimisticDepositsTableItem';
type Props = {
items: Array<L2DepositsItem>;
items: Array<OptimisticL2DepositsItem>;
top: number;
isLoading?: boolean;
}
const DepositsTable = ({ items, top, isLoading }: Props) => {
const OptimisticDepositsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
......@@ -28,11 +28,11 @@ const DepositsTable = ({ items, top, isLoading }: Props) => {
</Thead>
<Tbody>
{ items.map((item, index) => (
<DepositsTableItem key={ item.l2_tx_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
<OptimisticDepositsTableItem key={ item.l2_tx_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default DepositsTable;
export default OptimisticDepositsTable;
......@@ -2,7 +2,7 @@ import { Td, Tr, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
......@@ -11,14 +11,14 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2DepositsItem; isLoading?: boolean };
type Props = { item: OptimisticL2DepositsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -31,6 +31,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
......@@ -39,7 +40,8 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
hash={ item.l2_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant"
truncation="constant_long"
noIcon
/>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
......@@ -49,7 +51,8 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1_tx_hash }
truncation="constant"
truncation="constant_long"
noIcon
fontSize="sm"
lineHeight={ 5 }
/>
......@@ -71,4 +74,4 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
);
};
export default WithdrawalsTableItem;
export default OptimisticDepositsTableItem;
......@@ -24,7 +24,7 @@ const LatestBlocks = () => {
const isMobile = useIsMobile();
// const blocksMaxCount = isMobile ? 2 : 3;
let blocksMaxCount: number;
if (config.features.optimisticRollup.isEnabled || config.UI.views.block.hiddenFields?.total_reward) {
if (config.features.rollup.isEnabled || config.UI.views.block.hiddenFields?.total_reward) {
blocksMaxCount = isMobile ? 4 : 5;
} else {
blocksMaxCount = isMobile ? 2 : 3;
......
......@@ -59,14 +59,14 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
<Skeleton isLoaded={ !isLoading }>Txn</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ block.tx_count }</span></Skeleton>
{ !config.features.optimisticRollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
<>
<Skeleton isLoaded={ !isLoading }>Reward</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ totalReward.dp(10).toFixed() }</span></Skeleton>
</>
) }
{ !config.features.optimisticRollup.isEnabled && !config.UI.views.block.hiddenFields?.miner && (
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.miner && (
<>
<Skeleton isLoaded={ !isLoading } textTransform="capitalize">{ getNetworkValidatorTitle() }</Skeleton>
<AddressEntity
......@@ -74,6 +74,7 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
isLoading={ isLoading }
noIcon
noCopy
truncation="constant"
/>
</>
) }
......
......@@ -6,7 +6,7 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
......@@ -15,18 +15,18 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const feature = config.features.optimisticRollup;
const feature = config.features.rollup;
type Props = {
item: L2DepositsItem;
item: OptimisticL2DepositsItem;
isLoading?: boolean;
}
const LatestTxsItem = ({ item, isLoading }: Props) => {
const LatestDepositsItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile();
if (!feature.isEnabled) {
if (!feature.isEnabled || feature.type !== 'optimistic') {
return null;
}
......@@ -46,6 +46,7 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
hash={ item.l1_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
);
......@@ -55,6 +56,7 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
hash={ item.l2_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation={ isMobile ? 'constant_long' : 'dynamic' }
/>
);
......@@ -116,4 +118,4 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
);
};
export default React.memo(LatestTxsItem);
export default React.memo(LatestDepositsItem);
......@@ -58,7 +58,7 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
isLoading={ isLoading }
hash={ tx.hash }
fontWeight="700"
truncation="constant"
truncation="constant_long"
/>
{ tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm" ml={ 3 }>
......
......@@ -13,7 +13,7 @@ const BATCHES_API_URL = buildApiUrl('homepage_zkevm_l2_batches');
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkRollup) as any,
context: contextWithEnvs(configs.featureEnvs.zkEvmRollup) as any,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
......
......@@ -4,7 +4,7 @@ import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
......
......@@ -6,12 +6,12 @@ import {
import { motion } from 'framer-motion';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
......@@ -35,7 +35,7 @@ const LatestZkevmL2BatchItem = ({ batch, isLoading }: Props) => {
p={ 6 }
>
<Flex alignItems="center" overflow="hidden" w="100%" mb={ 3 }>
<ZkEvmBatchEntityL2
<BatchEntityL2
isLoading={ isLoading }
number={ batch.number }
tailLength={ 2 }
......
......@@ -15,6 +15,7 @@ import StatsItem from './StatsItem';
const hasGasTracker = config.UI.homepage.showGasTracker;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup;
const Stats = () => {
const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('homepage_stats', {
......@@ -32,7 +33,7 @@ const Stats = () => {
const zkEvmLatestBatchQuery = useApiQuery('homepage_zkevm_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: config.features.zkEvmRollup.isEnabled,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkEvm',
},
});
......@@ -68,7 +69,7 @@ const Stats = () => {
content = (
<>
{ config.features.zkEvmRollup.isEnabled ? (
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? (
<StatsItem
icon="txn_batches"
title="Latest batch"
......
......@@ -7,13 +7,14 @@ import LatestDeposits from 'ui/home/LatestDeposits';
import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
const rollupFeature = config.features.rollup;
const TransactionsHome = () => {
const hasAccount = useHasAccount();
if (config.features.optimisticRollup.isEnabled || hasAccount) {
if ((rollupFeature.isEnabled && rollupFeature.type === 'optimistic') || hasAccount) {
const tabs = [
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
config.features.optimisticRollup.isEnabled && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
].filter(Boolean);
return (
......
......@@ -6,8 +6,9 @@ import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useApiFetch from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { MARKETPLACE_APP } from 'stubs/marketplace';
const feature = config.features.marketplace;
......@@ -47,6 +48,7 @@ function sortApps(apps: Array<MarketplaceAppOverview>, isExperiment: boolean) {
}
export default function useMarketplaceApps(filter: string, selectedCategoryId: string = MarketplaceCategory.ALL, favoriteApps: Array<string> = []) {
const fetch = useFetch();
const apiFetch = useApiFetch();
const { value: isExperiment } = useFeatureValue('marketplace_exp', false);
......@@ -55,12 +57,11 @@ export default function useMarketplaceApps(filter: string, selectedCategoryId: s
queryFn: async() => {
if (!feature.isEnabled) {
return [];
} else if ('configUrl' in feature) {
return fetch<Array<MarketplaceAppOverview>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
} else {
return apiFetch('marketplace_dapps', { pathParams: { chainId: config.chain.id } });
}
const url = 'configUrl' in feature ?
feature.configUrl :
feature.api.endpoint + `/api/v1/chains/${ config.chain.id }/marketplace/dapps`;
return apiFetch<Array<MarketplaceAppOverview>, unknown>(url, undefined, { resource: 'marketplace-dapps' });
},
select: (data) => sortApps(data as Array<MarketplaceAppOverview>, isExperiment),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
......
import { useQuery } from '@tanstack/react-query';
import _groudBy from 'lodash/groupBy';
import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
......@@ -31,7 +30,16 @@ export default function useMarketplaceCategories(apps: Array<MarketplaceAppOverv
}
let categoryNames: Array<string> = [];
const grouped = _groudBy(apps, app => app.categories);
const grouped: { [key: string]: number } = {};
apps?.forEach(app => {
app.categories.forEach(category => {
if (grouped[category] === undefined) {
grouped[category] = 0;
}
grouped[category]++;
});
});
if (data?.length && !isPlaceholderData && isExperiment) {
categoryNames = data;
......@@ -40,7 +48,7 @@ export default function useMarketplaceCategories(apps: Array<MarketplaceAppOverv
}
return categoryNames
.map(category => ({ name: category, count: grouped[category]?.length || 0 }))
.map(category => ({ name: category, count: grouped[category] || 0 }))
.filter(c => c.count > 0);
}, [ apps, isAppsPlaceholderData, data, isPlaceholderData, isExperiment ]);
......
......@@ -18,7 +18,7 @@ const NameDomainHistoryListItem = ({ isLoading, transaction_hash: transactionHas
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntity hash={ transactionHash } isLoading={ isLoading } fontWeight={ 500 }/>
<TxEntity hash={ transactionHash } isLoading={ isLoading } fontWeight={ 500 } truncation="constant_long"/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
......@@ -32,7 +32,7 @@ const NameDomainHistoryListItem = ({ isLoading, transaction_hash: transactionHas
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity address={ fromAddress } isLoading={ isLoading }/>
<AddressEntity address={ fromAddress } isLoading={ isLoading } truncation="constant"/>
</ListItemMobileGrid.Value>
</>
) }
......
......@@ -17,7 +17,13 @@ const NameDomainHistoryTableItem = ({ isLoading, transaction_hash: transactionHa
return (
<Tr>
<Td verticalAlign="middle">
<TxEntity hash={ transactionHash } isLoading={ isLoading } fontWeight={ 700 }/>
<TxEntity
hash={ transactionHash }
isLoading={ isLoading }
fontWeight={ 700 }
noIcon
truncation="constant_long"
/>
</Td>
<Td pl={ 9 } verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
......@@ -25,7 +31,7 @@ const NameDomainHistoryTableItem = ({ isLoading, transaction_hash: transactionHa
</Skeleton>
</Td>
<Td verticalAlign="middle">
{ fromAddress && <AddressEntity address={ fromAddress } isLoading={ isLoading }/> }
{ fromAddress && <AddressEntity address={ fromAddress } isLoading={ isLoading } truncation="constant"/> }
</Td>
<Td verticalAlign="middle">
{ action && <Tag colorScheme="gray" isLoading={ isLoading }>{ action }</Tag> }
......
import { Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { L2OutputRootsItem } from 'types/api/l2OutputRoots';
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean };
type Props = { item: OptimisticL2OutputRootsItem; isLoading?: boolean };
const OutputRootsListItem = ({ item, isLoading }: Props) => {
const OptimisticL2OutputRootsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -55,14 +55,15 @@ const OutputRootsListItem = ({ item, isLoading }: Props) => {
hash={ item.l1_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Output root</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="space-between">
<Skeleton isLoaded={ !isLoading } color="text_secondary" w="calc(100% - 24px)">
<HashStringShortenDynamic hash={ item.output_root }/>
<Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="start">
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<HashStringShorten hash={ item.output_root } type="long"/>
</Skeleton>
<CopyToClipboard text={ item.output_root } isLoading={ isLoading }/>
</Flex>
......@@ -72,4 +73,4 @@ const OutputRootsListItem = ({ item, isLoading }: Props) => {
);
};
export default OutputRootsListItem;
export default OptimisticL2OutputRootsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { L2OutputRootsItem } from 'types/api/l2OutputRoots';
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import OutputRootsTableItem from './OutputRootsTableItem';
import OptimisticL2OutputRootsTableItem from './OptimisticL2OutputRootsTableItem';
type Props = {
items: Array<L2OutputRootsItem>;
items: Array<OptimisticL2OutputRootsItem>;
top: number;
isLoading?: boolean;
}
const OutputRootsTable = ({ items, top, isLoading }: Props) => {
const OptimisticL2OutputRootsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="900px">
<Thead top={ top }>
<Tr>
<Th width="140px">L2 output index</Th>
<Th width="160px">L2 output index</Th>
<Th width="20%">Age</Th>
<Th width="20%">L2 block #</Th>
<Th width="30%">L1 txn hash</Th>
......@@ -27,11 +27,15 @@ const OutputRootsTable = ({ items, top, isLoading }: Props) => {
</Thead>
<Tbody>
{ items.map((item, index) => (
<OutputRootsTableItem key={ item.l2_output_index + (Number(isLoading ? index : '') ? String(index) : '') } item={ item } isLoading={ isLoading }/>
<OptimisticL2OutputRootsTableItem
key={ item.l2_output_index + (Number(isLoading ? index : '') ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
);
};
export default OutputRootsTable;
export default OptimisticL2OutputRootsTable;
import { Flex, Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { L2OutputRootsItem } from 'types/api/l2OutputRoots';
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import HashStringShorten from 'ui/shared/HashStringShorten';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean };
type Props = { item: OptimisticL2OutputRootsItem; isLoading?: boolean };
const OutputRootsTableItem = ({ item, isLoading }: Props) => {
const OptimisticL2OutputRootsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -36,6 +36,7 @@ const OutputRootsTableItem = ({ item, isLoading }: Props) => {
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
......@@ -44,12 +45,14 @@ const OutputRootsTableItem = ({ item, isLoading }: Props) => {
hash={ item.l1_tx_hash }
fontSize="sm"
lineHeight={ 5 }
noIcon
truncation="constant_long"
/>
</Td>
<Td verticalAlign="middle">
<Flex overflow="hidden" whiteSpace="nowrap" w="100%" alignItems="center">
<Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)">
<HashStringShortenDynamic hash={ item.output_root }/>
<Flex overflow="hidden" w="100%" alignItems="center">
<Skeleton isLoaded={ !isLoading }>
<HashStringShorten hash={ item.output_root } type="long"/>
</Skeleton>
<CopyToClipboard text={ item.output_root } ml={ 2 } isLoading={ isLoading }/>
</Flex>
......@@ -58,4 +61,4 @@ const OutputRootsTableItem = ({ item, isLoading }: Props) => {
);
};
export default OutputRootsTableItem;
export default OptimisticL2OutputRootsTableItem;
......@@ -10,7 +10,7 @@ import { useAppContext } from 'lib/contexts/app';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address';
import { ADDRESS_TABS_COUNTERS } from 'stubs/address';
import { USER_OPS_ACCOUNT } from 'stubs/userOps';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
......@@ -27,6 +27,7 @@ import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport';
import useAddressQuery from 'ui/address/utils/useAddressQuery';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
......@@ -48,13 +49,7 @@ const AddressPageContent = () => {
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
});
const addressQuery = useAddressQuery({ hash });
const addressTabsCountersQuery = useApiQuery('address_tabs_counters', {
pathParams: { hash },
......@@ -176,7 +171,7 @@ const AddressPageContent = () => {
/>
);
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
const content = (addressQuery.isError || addressQuery.isDegradedData) ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
......
......@@ -7,7 +7,7 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import Withdrawals from './Withdrawals';
import BeaconChainWithdrawals from './BeaconChainWithdrawals';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -35,7 +35,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Withdrawals/>
<BeaconChainWithdrawals/>
</TestApp>,
);
......
......@@ -12,8 +12,8 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsListItem';
import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable';
const feature = config.features.beaconChain;
......@@ -41,7 +41,7 @@ const Withdrawals = () => {
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<WithdrawalsListItem
<BeaconChainWithdrawalsListItem
key={ item.index + (isPlaceholderData ? String(index) : '') }
item={ item }
view="list"
......@@ -50,7 +50,7 @@ const Withdrawals = () => {
))) }
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="list" top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
<BeaconChainWithdrawalsTable items={ data.items } view="list" top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
......
......@@ -12,6 +12,8 @@ import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const rollupFeature = config.features.rollup;
const Home = () => {
return (
<Box as="main">
......@@ -44,7 +46,7 @@ const Home = () => {
<ChainIndicators/>
<AdBanner mt={{ base: 6, lg: 8 }} mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
{ config.features.zkEvmRollup.isEnabled ? <LatestZkEvmL2Batches/> : <LatestBlocks/> }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? <LatestZkEvmL2Batches/> : <LatestBlocks/> }
<Box flexGrow={ 1 }>
<Transactions/>
</Box>
......
......@@ -10,8 +10,9 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useApiFetch from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContentLoader from 'ui/shared/ContentLoader';
......@@ -96,6 +97,7 @@ const MarketplaceAppContent = ({ address, data, isPending }: Props) => {
const MarketplaceApp = () => {
const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet();
const fetch = useFetch();
const apiFetch = useApiFetch();
const router = useRouter();
const id = getQueryParamString(router.query.id);
......@@ -105,15 +107,8 @@ const MarketplaceApp = () => {
queryFn: async() => {
if (!feature.isEnabled) {
return null;
}
const isConfigFile = 'configUrl' in feature;
const url = isConfigFile ?
feature.configUrl :
feature.api.endpoint + `/api/v1/chains/${ config.chain.id }/marketplace/dapps/${ id }`;
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(url, undefined, { resource: 'marketplace-dapps' });
if (isConfigFile) {
} else if ('configUrl' in feature) {
const result = await fetch<Array<MarketplaceAppOverview>, unknown>(feature.configUrl, undefined, { resource: 'marketplace-dapps' });
if (!Array.isArray(result)) {
throw result;
}
......@@ -122,9 +117,9 @@ const MarketplaceApp = () => {
throw { status: 404 };
}
return item;
} else {
return apiFetch('marketplace_dapp', { pathParams: { chainId: config.chain.id, dappId: id } });
}
return result;
},
enabled: feature.isEnabled,
});
......
......@@ -7,7 +7,7 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import L2Deposits from './L2Deposits';
import OptimisticL2Deposits from './OptimisticL2Deposits';
const DEPOSITS_API_URL = buildApiUrl('l2_deposits');
const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count');
......@@ -40,7 +40,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<L2Deposits/>
<OptimisticL2Deposits/>
</TestApp>,
);
......
......@@ -5,14 +5,14 @@ import useApiQuery from 'lib/api/useApiQuery';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import { L2_DEPOSIT_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import DepositsListItem from 'ui/l2Deposits/DepositsListItem';
import DepositsTable from 'ui/l2Deposits/DepositsTable';
import OptimisticDepositsListItem from 'ui/deposits/optimisticL2/OptimisticDepositsListItem';
import OptimisticDepositsTable from 'ui/deposits/optimisticL2/OptimisticDepositsTable';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const L2Deposits = () => {
const OptimisticL2Deposits = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_deposits',
options: {
......@@ -40,7 +40,7 @@ const L2Deposits = () => {
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<DepositsListItem
<OptimisticDepositsListItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
......@@ -48,7 +48,7 @@ const L2Deposits = () => {
))) }
</Show>
<Hide below="lg" ssr={ false }>
<DepositsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
<OptimisticDepositsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
......@@ -84,4 +84,4 @@ const L2Deposits = () => {
);
};
export default L2Deposits;
export default OptimisticL2Deposits;
......@@ -7,7 +7,7 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import OutputRoots from './L2OutputRoots';
import OptimisticL2OutputRoots from './OptimisticL2OutputRoots';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -40,7 +40,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<OutputRoots/>
<OptimisticL2OutputRoots/>
</TestApp>,
);
......
......@@ -4,14 +4,14 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { L2_OUTPUT_ROOTS_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import OutputRootsListItem from 'ui/l2OutputRoots/OutputRootsListItem';
import OutputRootsTable from 'ui/l2OutputRoots/OutputRootsTable';
import OptimisticL2OutputRootsListItem from 'ui/outputRoots/optimisticL2/OptimisticL2OutputRootsListItem';
import OptimisticL2OutputRootsTable from 'ui/outputRoots/optimisticL2/OptimisticL2OutputRootsTable';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const L2OutputRoots = () => {
const OptimisticL2OutputRoots = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_output_roots',
options: {
......@@ -38,7 +38,7 @@ const L2OutputRoots = () => {
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<OutputRootsListItem
<OptimisticL2OutputRootsListItem
key={ item.l2_output_index + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
......@@ -46,7 +46,7 @@ const L2OutputRoots = () => {
))) }
</Show>
<Hide below="lg" ssr={ false }>
<OutputRootsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
<OptimisticL2OutputRootsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
......@@ -82,4 +82,4 @@ const L2OutputRoots = () => {
);
};
export default L2OutputRoots;
export default OptimisticL2OutputRoots;
......@@ -7,7 +7,7 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import L2TxnBatches from './L2TxnBatches';
import OptimisticL2TxnBatches from './OptimisticL2TxnBatches';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -40,7 +40,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<L2TxnBatches/>
<OptimisticL2TxnBatches/>
</TestApp>,
);
......
......@@ -5,14 +5,14 @@ import useApiQuery from 'lib/api/useApiQuery';
import { nbsp } from 'lib/html-entities';
import { L2_TXN_BATCHES_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import TxnBatchesListItem from 'ui/l2TxnBatches/TxnBatchesListItem';
import TxnBatchesTable from 'ui/l2TxnBatches/TxnBatchesTable';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import OptimisticL2TxnBatchesListItem from 'ui/txnBatches/optimisticL2/OptimisticL2TxnBatchesListItem';
import OptimisticL2TxnBatchesTable from 'ui/txnBatches/optimisticL2/OptimisticL2TxnBatchesTable';
const L2TxnBatches = () => {
const OptimisticL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_txn_batches',
options: {
......@@ -39,14 +39,16 @@ const L2TxnBatches = () => {
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<TxnBatchesListItem
<OptimisticL2TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }><TxnBatchesTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/></Hide>
<Hide below="lg" ssr={ false }>
<OptimisticL2TxnBatchesTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
......@@ -81,4 +83,4 @@ const L2TxnBatches = () => {
);
};
export default L2TxnBatches;
export default OptimisticL2TxnBatches;
......@@ -7,7 +7,7 @@ import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import L2Withdrawals from './L2Withdrawals';
import OptimisticL2Withdrawals from './OptimisticL2Withdrawals';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -40,7 +40,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<L2Withdrawals/>
<OptimisticL2Withdrawals/>
</TestApp>,
);
......
......@@ -5,14 +5,14 @@ import useApiQuery from 'lib/api/useApiQuery';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import { L2_WITHDRAWAL_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import WithdrawalsListItem from 'ui/l2Withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/l2Withdrawals/WithdrawalsTable';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import OptimisticL2WithdrawalsListItem from 'ui/withdrawals/optimisticL2/OptimisticL2WithdrawalsListItem';
import OptimisticL2WithdrawalsTable from 'ui/withdrawals/optimisticL2/OptimisticL2WithdrawalsTable';
const L2Withdrawals = () => {
const OptimisticL2Withdrawals = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_withdrawals',
options: {
......@@ -38,14 +38,14 @@ const L2Withdrawals = () => {
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map(((item, index) => (
<WithdrawalsListItem
<OptimisticL2WithdrawalsListItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
<OptimisticL2WithdrawalsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
......@@ -81,4 +81,4 @@ const L2Withdrawals = () => {
);
};
export default L2Withdrawals;
export default OptimisticL2Withdrawals;
......@@ -111,7 +111,7 @@ base.describe('bridged tokens', async() => {
});
test('base view', async({ mount, page }) => {
await page.route(BRIDGED_TOKENS_API_URL + '?chain_ids%5B0%5D=99', (route) => route.fulfill({
await page.route(BRIDGED_TOKENS_API_URL + '?chain_ids=99', (route) => route.fulfill({
status: 200,
body: JSON.stringify(bridgedFilteredTokens),
}));
......
......@@ -11,7 +11,7 @@ import ZkEvmL2TxnBatch from './ZkEvmL2TxnBatch';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkRollup) as any,
context: contextWithEnvs(configs.featureEnvs.zkEvmRollup) as any,
});
const hooksConfig = {
......
......@@ -16,8 +16,8 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import ZkEvmL2TxnBatchDetails from 'ui/txnBatches/zkEvmL2/ZkEvmL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
import ZkEvmL2TxnBatchDetails from 'ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails';
const ZkEvmL2TxnBatch = () => {
const router = useRouter();
......
......@@ -11,7 +11,7 @@ import ZkEvmL2TxnBatches from './ZkEvmL2TxnBatches';
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.zkRollup) as any,
context: contextWithEnvs(configs.featureEnvs.zkEvmRollup) as any,
});
const BATCHES_API_URL = buildApiUrl('zkevm_l2_txn_batches');
......
......@@ -8,8 +8,8 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import ZkEvmTxnBatchesListItem from 'ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesListItem';
import ZkEvmTxnBatchesTable from 'ui/zkEvmL2TxnBatches/ZkEvmTxnBatchesTable';
import ZkEvmTxnBatchesListItem from 'ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesListItem';
import ZkEvmTxnBatchesTable from 'ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesTable';
const ZkEvmL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import AccountActionsMenu from './AccountActionsMenu';
test.use({ viewport: { width: 200, height: 200 } });
test.describe('with multiple items', async() => {
const hooksConfig = {
router: {
query: { hash: '<hash>' },
pathname: '/token/[hash]',
isReady: true,
},
};
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').click();
await expect(page).toHaveScreenshot();
});
test('base view with styles', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu m={ 2 } outline="1px solid lightpink"/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').click();
await expect(page).toHaveScreenshot();
});
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu isLoading/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('loading with styles', async({ mount }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu isLoading m={ 2 } outline="1px solid lightpink"/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
test.describe('with one item', async() => {
const hooksConfig = {
router: {
query: { hash: '<hash>' },
pathname: '/tx/[hash]',
isReady: true,
},
};
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').hover();
await expect(page).toHaveScreenshot();
});
test('base view with styles', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu m={ 2 } outline="1px solid lightpink"/>
</TestApp>,
{ hooksConfig },
);
await component.getByRole('button').hover();
await expect(page).toHaveScreenshot();
});
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<AccountActionsMenu isLoading/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
import { IconButton, Menu, MenuButton, MenuList, Skeleton, chakra } from '@chakra-ui/react';
import { Box, IconButton, Menu, MenuButton, MenuList, Skeleton, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemProps } from './types';
import config from 'configs/app';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -33,30 +35,55 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
return null;
}
const items = [
{
render: (props: ItemProps) => <TokenInfoMenuItem { ...props }/>,
enabled: isTokenPage && config.features.addressVerification.isEnabled,
},
{
render: (props: ItemProps) => <PrivateTagMenuItem { ...props } entityType={ isTxPage ? 'tx' : 'address' }/>,
enabled: true,
},
{
render: (props: ItemProps) => <PublicTagMenuItem { ...props }/>,
enabled: !isTxPage,
},
].filter(({ enabled }) => enabled);
if (items.length === 0) {
return null;
}
if (isLoading) {
return <Skeleton w="36px" h="32px" borderRadius="base" className={ className }/>;
}
if (items.length === 1) {
return (
<Box className={ className }>
{ items[0].render({ type: 'button', hash, onBeforeClick: isAccountActionAllowed }) }
</Box>
);
}
return (
<Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base" className={ className }>
<MenuButton
as={ IconButton }
size="sm"
variant="outline"
colorScheme="gray"
px="7px"
onClick={ handleButtonClick }
icon={ <IconSvg name="dots" boxSize="18px"/> }
/>
</Skeleton>
<MenuButton
as={ IconButton }
className={ className }
size="sm"
variant="outline"
colorScheme="gray"
px="7px"
onClick={ handleButtonClick }
icon={ <IconSvg name="dots" boxSize="18px"/> }
/>
<MenuList minWidth="180px" zIndex="popover">
{ isTokenPage && config.features.addressVerification.isEnabled &&
<TokenInfoMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
<PrivateTagMenuItem
py={ 2 }
px={ 4 }
hash={ hash }
onBeforeClick={ isAccountActionAllowed }
type={ isTxPage ? 'tx' : 'address' }
/>
{ !isTxPage && <PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
{ items.map(({ render }, index) => (
<React.Fragment key={ index }>
{ render({ type: 'menu_item', hash, onBeforeClick: isAccountActionAllowed }) }
</React.Fragment>
)) }
</MenuList>
</Menu>
);
......
import { MenuItem, chakra, useDisclosure } from '@chakra-ui/react';
import { useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemType } from '../types';
import type { Address } from 'types/api/address';
import type { Transaction } from 'types/api/transaction';
......@@ -12,19 +13,23 @@ import AddressModal from 'ui/privateTags/AddressModal/AddressModal';
import TransactionModal from 'ui/privateTags/TransactionModal/TransactionModal';
import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props {
className?: string;
hash: string;
onBeforeClick: () => boolean;
type?: 'address' | 'tx';
entityType?: 'address' | 'tx';
type: ItemType;
}
const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }: Props) => {
const PrivateTagMenuItem = ({ className, hash, onBeforeClick, entityType = 'address', type }: Props) => {
const modal = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const queryKey = getResourceKey(type === 'tx' ? 'tx' : 'address', { pathParams: { hash } });
const queryKey = getResourceKey(entityType === 'tx' ? 'tx' : 'address', { pathParams: { hash } });
const queryData = queryClient.getQueryData<Address | Transaction>(queryKey);
const handleClick = React.useCallback(() => {
......@@ -58,13 +63,26 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }
pageType,
};
const element = (() => {
switch (type) {
case 'button': {
return <ButtonItem label="Add private tag" icon="privattags" onClick={ handleClick } className={ className }/>;
}
case 'menu_item': {
return (
<MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
);
}
}
})();
return (
<>
<MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
{ type === 'tx' ?
{ element }
{ entityType === 'tx' ?
<TransactionModal { ...modalProps } data={{ transaction_hash: hash }}/> :
<AddressModal { ...modalProps } data={{ address_hash: hash }}/>
}
......@@ -72,4 +90,4 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }
);
};
export default React.memo(chakra(PrivateTagMenuItem));
export default React.memo(PrivateTagMenuItem);
import { MenuItem, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemType } from '../types';
import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props {
className?: string;
hash: string;
onBeforeClick: () => boolean;
type: ItemType;
}
const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const PublicTagMenuItem = ({ className, hash, onBeforeClick, type }: Props) => {
const router = useRouter();
const handleClick = React.useCallback(() => {
......@@ -21,12 +26,23 @@ const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
router.push({ pathname: '/account/public-tags-request', query: { address: hash } });
}, [ hash, onBeforeClick, router ]);
return (
<MenuItem className={ className }onClick={ handleClick }>
<IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/>
<span>Add public tag</span>
</MenuItem>
);
const element = (() => {
switch (type) {
case 'button': {
return <ButtonItem label="Add public tag" icon="publictags" onClick={ handleClick } className={ className }/>;
}
case 'menu_item': {
return (
<MenuItem className={ className } onClick={ handleClick }>
<IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
);
}
}
})();
return element;
};
export default React.memo(chakra(PublicTagMenuItem));
export default React.memo(PublicTagMenuItem);
import { MenuItem, chakra, useDisclosure } from '@chakra-ui/react';
import { chakra, useDisclosure } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { ItemType } from '../types';
import type { Route } from 'nextjs-routes';
import config from 'configs/app';
......@@ -11,13 +13,17 @@ import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import IconSvg from 'ui/shared/IconSvg';
import ButtonItem from '../parts/ButtonItem';
import MenuItem from '../parts/MenuItem';
interface Props {
className?: string;
hash: string;
onBeforeClick: (route: Route) => boolean;
type: ItemType;
}
const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const TokenInfoMenuItem = ({ className, hash, onBeforeClick, type }: Props) => {
const router = useRouter();
const modal = useDisclosure();
const isAuth = useHasAccount();
......@@ -61,37 +67,40 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
router.push({ pathname: '/account/verified-addresses' });
}, [ router ]);
const icon = <IconSvg name="edit" boxSize={ 6 } mr={ 2 } p={ 1 }/>;
const content = (() => {
if (!verifiedAddressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase())) {
return (
<MenuItem className={ className } onClick={ handleAddAddressClick }>
{ icon }
<span>{ tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info' }</span>
</MenuItem>
);
}
const element = (() => {
const icon = <IconSvg name="edit" boxSize={ 6 } p={ 1 }/>;
const isVerifiedAddress = verifiedAddressesQuery.data?.verifiedAddresses
.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase());
const hasApplication = applicationsQuery.data?.submissions.some(({ tokenAddress }) => tokenAddress.toLowerCase() === hash.toLowerCase());
return (
<MenuItem className={ className } onClick={ handleAddApplicationClick }>
{ icon }
<span>
{
hasApplication || tokenInfoQuery.data?.tokenAddress ?
'Update token info' :
'Add token info'
}
</span>
</MenuItem>
);
const label = (() => {
if (!isVerifiedAddress) {
return tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info';
}
return hasApplication || tokenInfoQuery.data?.tokenAddress ? 'Update token info' : 'Add token info';
})();
const onClick = isVerifiedAddress ? handleAddApplicationClick : handleAddAddressClick;
switch (type) {
case 'button': {
return <ButtonItem label={ label } icon={ icon } onClick={ onClick } className={ className }/>;
}
case 'menu_item': {
return (
<MenuItem className={ className } onClick={ onClick }>
{ icon }
<chakra.span ml={ 2 }>{ label }</chakra.span>
</MenuItem>
);
}
}
})();
return (
<>
{ content }
{ element }
<AddressVerificationModal
defaultAddress={ hash }
pageType={ PAGE_TYPE_DICT['/token/[hash]'] }
......@@ -105,4 +114,4 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
);
};
export default React.memo(chakra(TokenInfoMenuItem));
export default React.memo(TokenInfoMenuItem);
import { IconButton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
className?: string;
onClick: () => void;
label: string;
icon: IconName | React.ReactElement;
}
const ButtonItem = ({ className, label, onClick, icon }: Props) => {
return (
<Tooltip label={ label }>
<IconButton
aria-label={ label }
className={ className }
icon={ typeof icon === 'string' ? <IconSvg name={ icon } boxSize={ 6 }/> : icon }
onClick={ onClick }
size="sm"
variant="outline"
px="4px"
/>
</Tooltip>
);
};
export default ButtonItem;
import { MenuItem as MenuItemChakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
children: React.ReactNode;
onClick: () => void;
}
const MenuItem = ({ className, children, onClick }: Props) => {
return (
<MenuItemChakra className={ className } onClick={ onClick } py={ 2 } px={ 4 }>
{ children }
</MenuItemChakra>
);
};
export default MenuItem;
export type ItemType = 'button' | 'menu_item';
export interface ItemProps {
type: ItemType;
hash: string;
onBeforeClick: () => boolean;
}
......@@ -62,7 +62,7 @@ const EntityTags = ({ className, data, tagsBefore = [], tagsAfter = [], isLoadin
}
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Tag onClick={ onToggle }>+{ tags.length - 1 }</Tag>
<Tag isLoading={ isLoading }onClick={ onToggle }>+{ tags.length - 1 }</Tag>
</PopoverTrigger>
<PopoverContent w="240px">
<PopoverBody >
......
......@@ -7,17 +7,19 @@ import shortenString from 'lib/shortenString';
interface Props {
hash: string;
isTooltipDisabled?: boolean;
type?: 'long' | 'short';
as?: As;
}
const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span' }: Props) => {
if (hash.length <= 8) {
const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span', type }: Props) => {
const charNumber = type === 'long' ? 16 : 8;
if (hash.length <= charNumber) {
return <chakra.span as={ as }>{ hash }</chakra.span>;
}
return (
<Tooltip label={ hash } isDisabled={ isTooltipDisabled }>
<chakra.span as={ as }>{ shortenString(hash) }</chakra.span>
<chakra.span as={ as }>{ shortenString(hash, charNumber) }</chakra.span>
</Tooltip>
);
};
......
......@@ -67,7 +67,7 @@ const TokenTransferListItem = ({
<TxEntity
isLoading={ isLoading }
hash={ txHash }
truncation="constant"
truncation="constant_long"
fontWeight="700"
/>
{ timestamp && (
......
......@@ -38,11 +38,11 @@ const TokenTransferTable = ({
<Thead top={ top }>
<Tr>
{ showTxInfo && <Th width="44px"></Th> }
<Th width="185px">Token</Th>
<Th width="200px">Token</Th>
<Th width="160px">Token ID</Th>
{ showTxInfo && <Th width="20%">Txn hash</Th> }
<Th width="50%">From/To</Th>
<Th width="30%" isNumeric>Value</Th>
{ showTxInfo && <Th width="200px">Txn hash</Th> }
<Th width="60%">From/To</Th>
<Th width="40%" isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
......
......@@ -75,6 +75,7 @@ const TokenTransferTableItem = ({
fontWeight={ 600 }
noIcon
mt="7px"
truncation="constant_long"
/>
{ timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" mt="10px" display="inline-block">
......
......@@ -7,6 +7,7 @@ import * as cookies from 'lib/cookies';
import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner';
import HypeBanner from './HypeBanner';
import SliseBanner from './SliseBanner';
const feature = config.features.adsBanner;
......@@ -24,6 +25,8 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
return <AdbutlerBanner/>;
case 'coinzilla':
return <CoinzillaBanner/>;
case 'hype':
return <HypeBanner/>;
case 'slise':
return <SliseBanner/>;
}
......
import { Flex, chakra } from '@chakra-ui/react';
import { Banner, setWalletAddresses } from '@hypelab/sdk-react';
import Script from 'next/script';
import React from 'react';
import { useAccount } from 'wagmi';
import Web3ModalProvider from '../Web3ModalProvider';
import { hypeInit } from './hypeBannerScript';
const DESKTOP_BANNER_SLUG = 'b1559fc3e7';
const MOBILE_BANNER_SLUG = '668ed80a9e';
const HypeBannerContent = ({ className }: { className?: string }) => {
return (
<>
<Script
id="hypelab"
strategy="afterInteractive"
>{ hypeInit }</Script>
<Flex className={ className } h="90px" display={{ base: 'none', lg: 'flex' }}>
<Banner placement={ DESKTOP_BANNER_SLUG }/>
</Flex>
<Flex className={ className } h="50px" display={{ base: 'flex', lg: 'none' }}>
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
</>
);
};
const HypeBannerWithWalletAddress = ({ className }: { className?: string }) => {
const { address } = useAccount();
React.useEffect(() => {
if (address) {
setWalletAddresses([ address ]);
}
}, [ address ]);
return <HypeBannerContent className={ className }/>;
};
const HypeBanner = ({ className }: { className?: string }) => {
const fallback = React.useCallback(() => {
return <HypeBannerContent className={ className }/>;
}, [ className ]);
return (
<Web3ModalProvider fallback={ fallback }>
<HypeBannerWithWalletAddress className={ className }/>
</Web3ModalProvider>
);
};
export default chakra(HypeBanner);
import config from 'configs/app';
const PRODUCTION_PROPERTY_SLUG = '127fddd522';
const HYPE_API_URL = 'https://api.hypelab.com';
export const hypeInit = (() => {
const feature = config.features.adsBanner;
if (!feature.isEnabled || feature.provider !== 'hype') {
return;
}
return `!(function (h, y, p, e, l, a, b) {
((l = document.createElement(h)).async = !0),
(l.src = y),
(l.onload = function () {
(a = { URL: p, propertySlug: e, environment: 'production' }), HypeLab.initialize(a);
}),
(b = document.getElementsByTagName(h)[0]).parentNode.insertBefore(l, b);
})('script', 'https://api.hypelab.com/v1/scripts/hp-sdk.js?v=0', '${ HYPE_API_URL }', '${ PRODUCTION_PROPERTY_SLUG }');`;
})();
import type { ThemeTypings } from '@chakra-ui/react';
import { Flex, chakra, useBreakpointValue } from '@chakra-ui/react';
import { Flex, Grid, chakra, useBreakpointValue } from '@chakra-ui/react';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
......@@ -25,7 +25,7 @@ interface Props {
noIcon?: boolean;
}
const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading, tokenHash = '', truncation, noIcon }: Props) => {
const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading, tokenHash = '', noIcon }: Props) => {
const mode = useBreakpointValue(
{
base: (typeof modeProp === 'object' ? modeProp.base : modeProp),
......@@ -52,9 +52,10 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ current === from.hash }
noIcon={ noIcon }
tokenHash={ tokenHash }
truncation={ truncation }
maxW={ truncation === 'constant' ? undefined : 'calc(100% - 28px)' }
truncation="constant"
maxW="calc(100% - 28px)"
w="min-content"
/>
</Flex>
{ to && (
......@@ -65,8 +66,8 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ current === to.hash }
noIcon={ noIcon }
tokenHash={ tokenHash }
truncation={ truncation }
maxW={ truncation === 'constant' ? undefined : 'calc(100% - 28px)' }
truncation="constant"
maxW="calc(100% - 28px)"
w="min-content"
ml="28px"
/>
......@@ -76,10 +77,10 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
}
const isOutgoing = current === from.hash;
const iconSizeWithMargins = (5 + (isOutgoing ? 4 : 2) + 3) * 4;
const iconSize = 20;
return (
<Flex className={ className } alignItems="center">
<Grid className={ className } alignItems="center" gridTemplateColumns={ `fit-content(100%) ${ iconSize }px fit-content(100%)` }>
<Entity
address={ from }
isLoading={ isLoading }
......@@ -87,8 +88,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ isOutgoing }
noIcon={ noIcon }
tokenHash={ tokenHash }
truncation={ truncation }
maxW={ truncation === 'constant' ? undefined : `calc(50% - ${ iconSizeWithMargins / 2 }px)` }
truncation="constant"
mr={ isOutgoing ? 4 : 2 }
/>
<AddressFromToIcon
......@@ -103,12 +103,11 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading
noCopy={ current === to.hash }
noIcon={ noIcon }
tokenHash={ tokenHash }
truncation={ truncation }
maxW={ truncation === 'constant' ? undefined : `calc(50% - ${ iconSizeWithMargins / 2 }px)` }
truncation="constant"
ml={ 3 }
/>
) }
</Flex>
</Grid>
);
};
......
......@@ -7,14 +7,14 @@ import config from 'configs/app';
import * as AddressEntity from './AddressEntity';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
const AddressEntityL1 = (props: AddressEntity.EntityProps) => {
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled) {
return null;
}
const defaultHref = feature.L1BaseUrl + route({
const defaultHref = rollupFeature.L1BaseUrl + route({
pathname: '/address/[hash]',
query: {
...props.query,
......
......@@ -13,7 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
import { getIconProps, type IconSize } from './utils';
export type Truncation = 'constant' | 'dynamic' | 'tail' | 'none';
export type Truncation = 'constant' | 'constant_long' | 'dynamic' | 'tail' | 'none';
export interface EntityBaseProps {
className?: string;
......@@ -117,6 +117,14 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
const children = (() => {
switch (truncation) {
case 'constant_long':
return (
<HashStringShorten
hash={ text }
as={ asProp }
type="long"
/>
);
case 'constant':
return (
<HashStringShorten
......
......@@ -8,13 +8,13 @@ import config from 'configs/app';
import * as BlockEntity from './BlockEntity';
const feature = config.features.zkEvmRollup;
const rollupFeature = config.features.rollup;
const ZkEvmBatchEntityL2 = (props: BlockEntity.EntityProps) => {
const BatchEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled) {
return null;
}
......@@ -31,4 +31,4 @@ const ZkEvmBatchEntityL2 = (props: BlockEntity.EntityProps) => {
);
};
export default chakra(ZkEvmBatchEntityL2);
export default chakra(BatchEntityL2);
......@@ -8,13 +8,13 @@ import config from 'configs/app';
import * as BlockEntity from './BlockEntity';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
const BlockEntityL1 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled) {
return null;
}
......@@ -24,7 +24,7 @@ const BlockEntityL1 = (props: BlockEntity.EntityProps) => {
<BlockEntity.Link
{ ...linkProps }
isExternal
href={ feature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: props.hash ?? String(props.number) } }) }
href={ rollupFeature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: props.hash ?? String(props.number) } }) }
>
<BlockEntity.Content { ...partsProps }/>
</BlockEntity.Link>
......
......@@ -6,13 +6,13 @@ import config from 'configs/app';
import * as BlockEntity from './BlockEntity';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
const BlockEntityL2 = (props: BlockEntity.EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled) {
return null;
}
......
......@@ -8,13 +8,13 @@ import config from 'configs/app';
import * as TxEntity from './TxEntity';
const feature = config.features.optimisticRollup.isEnabled ? config.features.optimisticRollup : config.features.zkEvmRollup;
const rollupFeature = config.features.rollup;
const TxEntityL1 = (props: TxEntity.EntityProps) => {
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const linkProps = _omit(props, [ 'className' ]);
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled) {
return null;
}
......@@ -24,7 +24,7 @@ const TxEntityL1 = (props: TxEntity.EntityProps) => {
<TxEntity.Link
{ ...linkProps }
isExternal
href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: props.hash } }) }
href={ rollupFeature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: props.hash } }) }
>
<TxEntity.Content { ...partsProps }/>
</TxEntity.Link>
......
......@@ -37,9 +37,9 @@ const StatusTag = ({ type, text, errorText, isLoading }: Props) => {
return (
<Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex" isLoading={ isLoading }>
<IconSvg boxSize={ 2.5 } name={ icon } mr={ 2 }/>
<TagLabel>{ text }</TagLabel>
<Tag colorScheme={ colorScheme } display="flex" isLoading={ isLoading } >
<IconSvg boxSize={ 2.5 } name={ icon } mr={ 2 } flexShrink={ 0 }/>
<TagLabel display="block">{ text }</TagLabel>
</Tag>
</Tooltip>
);
......
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import type { StatusTagType } from './StatusTag';
import StatusTag from './StatusTag';
......
......@@ -40,7 +40,7 @@ const TokenTransferListItem = ({
<TxEntity
isLoading={ isLoading }
hash={ txHash }
truncation="constant"
truncation="constant_long"
fontWeight="700"
/>
{ timestamp && (
......
......@@ -26,15 +26,17 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
return (
<AddressHighlightProvider>
<Table variant="simple" size="sm">
<Table variant="simple" size="sm" minW="950px">
<Thead top={ top }>
<Tr>
<Th width={ tokenType === 'ERC-1155' ? '50%' : '75%' }>Txn hash</Th>
<Th width="164px">Method</Th>
<Th width={{ lg: '200px', xl: '420px' }}>From/To</Th>
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="25%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> }
<Th width="280px">Txn hash</Th>
<Th width="200px">Method</Th>
<Th width={{ lg: '224px', xl: '420px' }}>From/To</Th>
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') &&
<Th width={ tokenType === 'ERC-1155' ? '50%' : '100%' }>Token ID</Th>
}
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && (
<Th width="25%" isNumeric>
<Th width={ tokenType === 'ERC-1155' ? '50%' : '100%' } isNumeric>
<TruncatedValue value={ `Value ${ token?.symbol || '' }` } w="100%" verticalAlign="middle"/>
</Th>
) }
......
import { Tr, Td, Grid, Skeleton, Box } from '@chakra-ui/react';
import { Tr, Td, Flex, Skeleton, Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
......@@ -35,12 +35,13 @@ const TokenTransferTableItem = ({
return (
<Tr alignItems="top">
<Td>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content" py="7px">
<Flex alignItems="center" py="7px">
<TxEntity
hash={ txHash }
isLoading={ isLoading }
fontWeight={ 600 }
noIcon
truncation="constant_long"
/>
{ timestamp && (
<Skeleton isLoaded={ !isLoading } display="inline-block" color="gray.500" fontWeight="400" ml="10px">
......@@ -49,7 +50,7 @@ const TokenTransferTableItem = ({
</span>
</Skeleton>
) }
</Grid>
</Flex>
</Td>
<Td>
{ method ? (
......
import { Flex, chakra } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
......@@ -16,7 +16,7 @@ const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
const num = value === '1' ? '' : value;
return (
<Flex alignItems="center" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<>
{ num ? (
<>
<chakra.span color="text_secondary">for</chakra.span>
......@@ -45,7 +45,7 @@ const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
w="auto"
flexGrow={ 1 }
/>
</Flex>
</>
);
};
......
......@@ -43,6 +43,21 @@ bsInterpretationTest('with interpretation +@mobile +@dark-mode', async({ mount,
await expect(component).toHaveScreenshot();
});
bsInterpretationTest('with interpretation and view all link +@mobile', async({ mount, page }) => {
await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ data: { summaries: [ ...txInterpretation.data.summaries, ...txInterpretation.data.summaries ] } }),
}));
const component = await mount(
<TestApp>
<TxSubHeading hash={ hash } hasTag={ false }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
bsInterpretationTest('no interpretation', async({ mount, page }) => {
await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({
status: 200,
......
......@@ -29,6 +29,9 @@ const TxSubHeading = ({ hash, hasTag }: Props) => {
const hasInterpretation = hasInterpretationFeature &&
(txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length));
const hasViewAllInterpretationsLink =
!txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1;
return (
<Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%">
{ hasInterpretation && (
......@@ -37,9 +40,10 @@ const TxSubHeading = ({ hash, hasTag }: Props) => {
summary={ txInterpretationQuery.data?.data.summaries[0] }
isLoading={ txInterpretationQuery.isPlaceholderData }
fontSize="lg"
mr={ hasViewAllInterpretationsLink ? 3 : 0 }
/>
{ !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1 &&
<Link ml={ 3 } href={ `#${ TX_ACTIONS_BLOCK_ID }` }>all actions</Link> }
{ hasViewAllInterpretationsLink &&
<Link href={ `#${ TX_ACTIONS_BLOCK_ID }` }>View all</Link> }
</Flex>
) }
{ !hasInterpretation && <TxEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } mr={{ base: 0, lg: 2 }} fontFamily="heading"/> }
......
......@@ -26,7 +26,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
});
return (
<Flex flexWrap="wrap" columnGap={ 2 } rowGap={ 2 }>
<>
<chakra.span color="text_secondary">for</chakra.span>
<span>{ valueStr }</span>
<TokenEntity
......@@ -36,7 +36,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
w="auto"
/>
{ usd && <chakra.span color="text_secondary">(${ usd })</chakra.span> }
</Flex>
</>
);
}
......@@ -68,11 +68,12 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
return (
<Flex
alignItems="flex-start"
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
flexWrap="wrap"
columnGap={ 2 }
rowGap={ 3 }
flexDir="row"
w="100%"
fontWeight={ 500 }
>
<AddressFromTo
from={ data.from }
......@@ -81,9 +82,7 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
noIcon
fontWeight="500"
/>
<Flex flexDir="column" rowGap={ 5 } w="100%" overflow="hidden" fontWeight={ 500 }>
{ content }
</Flex>
{ content }
</Flex>
);
};
......
......@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { L2WithdrawalStatus } from 'types/api/l2Withdrawals';
import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
......@@ -10,7 +10,7 @@ import * as configs from 'playwright/utils/configs';
import TxDetailsWithdrawalStatus from './TxDetailsWithdrawalStatus';
const statuses: Array<L2WithdrawalStatus> = [
const statuses: Array<OptimisticL2WithdrawalStatus> = [
'Waiting for state root',
'Ready for relay',
'Relayed',
......
import { Button } from '@chakra-ui/react';
import React from 'react';
import type { L2WithdrawalStatus } from 'types/api/l2Withdrawals';
import { WITHDRAWAL_STATUSES } from 'types/api/l2Withdrawals';
import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2';
import { WITHDRAWAL_STATUSES } from 'types/api/optimisticL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
interface Props {
status: L2WithdrawalStatus | undefined;
status: OptimisticL2WithdrawalStatus | undefined;
l1TxHash: string | undefined;
}
......@@ -55,7 +55,7 @@ const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => {
return (
<VerificationSteps
steps={ steps as unknown as Array<L2WithdrawalStatus> }
steps={ steps as unknown as Array<OptimisticL2WithdrawalStatus> }
currentStep={ status }
rightSlot={ rightSlot }
my={ hasClaimButton ? '-6px' : 0 }
......
......@@ -33,8 +33,8 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
......@@ -53,6 +53,9 @@ import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
const rollupFeature = config.features.rollup;
interface Props {
data: Transaction | undefined;
isLoading: boolean;
......@@ -123,7 +126,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<CopyToClipboard text={ data.hash } isLoading={ isLoading }/>
</DetailsInfoItem>
<DetailsInfoItem
title={ config.features.zkEvmRollup.isEnabled ? 'L2 status and method' : 'Status and method' }
title={ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? 'L2 status and method' : 'Status and method' }
hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
isLoading={ isLoading }
>
......@@ -134,7 +137,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</Tag>
) }
</DetailsInfoItem>
{ config.features.optimisticRollup.isEnabled && data.op_withdrawals && data.op_withdrawals.length > 0 && (
{ rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.op_withdrawals && data.op_withdrawals.length > 0 && (
<DetailsInfoItem
title="Withdrawal status"
hint="Detailed status progress of the transaction"
......@@ -200,7 +203,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
hint="Batch index for this transaction"
isLoading={ isLoading }
>
<ZkEvmBatchEntityL2
<BatchEntityL2
isLoading={ isLoading }
number={ data.zkevm_batch_number }
/>
......@@ -419,7 +422,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
) }
</DetailsInfoItem>
) }
{ data.tx_burnt_fee && !config.UI.views.tx.hiddenFields?.burnt_fees && !config.features.optimisticRollup.isEnabled && (
{ data.tx_burnt_fee && !config.UI.views.tx.hiddenFields?.burnt_fees && !(rollupFeature.isEnabled && rollupFeature.type === 'optimistic') && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ currencyUnits.ether } burned for this transaction. Equals Block Base Fee per Gas * Gas Used` }
......@@ -434,7 +437,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
/>
</DetailsInfoItem>
) }
{ config.features.optimisticRollup.isEnabled && (
{ rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && (
<>
{ data.l1_gas_used && (
<DetailsInfoItem
......
import { Skeleton, chakra } from '@chakra-ui/react';
import { Skeleton, Tooltip, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -17,7 +17,7 @@ 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, fillStringVariables, NATIVE_COIN_SYMBOL_VAR_NAME } from './utils';
import { extractVariables, getStringChunks, fillStringVariables, checkSummary, NATIVE_COIN_SYMBOL_VAR_NAME } from './utils';
type Props = {
summary?: TxInterpretationSummary;
......@@ -116,6 +116,10 @@ const TxInterpretation = ({ summary, isLoading, className }: Props) => {
const template = summary.summary_template;
const variables = summary.summary_template_variables;
if (!checkSummary(template, variables)) {
return null;
}
const intermediateResult = fillStringVariables(template, variables);
const variablesNames = extractVariables(intermediateResult);
......@@ -123,7 +127,9 @@ const TxInterpretation = ({ summary, isLoading, className }: Props) => {
return (
<Skeleton isLoaded={ !isLoading } className={ className } fontWeight={ 500 } whiteSpace="pre-wrap" >
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 2 } verticalAlign="text-top"/>
<Tooltip label="Transaction summary">
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 2 } verticalAlign="text-top"/>
</Tooltip>
{ chunks.map((chunk, index) => {
return (
<chakra.span key={ chunk + index }>
......
import { extractVariables, getStringChunks } from './utils';
import { extractVariables, getStringChunks, checkSummary } from './utils';
const template = '{action_type} {source_amount} {native} into {destination_amount} {destination_token}';
......@@ -11,3 +11,15 @@ it('split string without capturing variables', () => {
const result = getStringChunks(template);
expect(result).toEqual([ '', ' ', ' ', ' into ', ' ', '' ]);
});
it('checks that summary is valid', () => {
const result = checkSummary('{foo} {bar}', { foo: { type: 'string', value: 'foo' }, bar: { type: 'string', value: 'bar' } });
expect(result).toBe(true);
});
it('checks that summary is invalid', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
const result = checkSummary('{foo} {bar}', { foo: { type: 'string', value: null }, bar: { type: 'string', value: 'bar' } });
expect(result).toBe(false);
});
......@@ -20,6 +20,19 @@ export function getStringChunks(template: string) {
return template.split(VAR_REGEXP);
}
export function checkSummary(template: string, variables: Record<string, TxInterpretationVariable>) {
const variablesNames = extractVariables(template);
let result = true;
for (const name of variablesNames) {
if (!variables[name] || variables[name].value === undefined || variables[name].value === null) {
result = false;
break;
}
}
return result;
}
export function fillStringVariables(template: string, variables: Record<string, TxInterpretationVariable>) {
const variablesNames = extractVariables(template);
......
......@@ -24,6 +24,7 @@ const TxStateListItem = ({ data, isLoading }: Props) => {
<AddressEntity
address={ data.address }
isLoading={ isLoading }
truncation="constant"
/>
{ tag }
</ListItemMobileGrid.Value>
......
......@@ -18,6 +18,8 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX, TX_ZKEVM_L2 } from 'stubs/tx';
const rollupFeature = config.features.rollup;
export type TxQuery = UseQueryResult<Transaction, ResourceError<{ status: number }>> & {
socketStatus: 'close' | 'error' | undefined;
setRefetchOnError: {
......@@ -46,7 +48,7 @@ export default function useTxQuery(params?: Params): TxQuery {
queryOptions: {
enabled: Boolean(hash) && params?.isEnabled !== false,
refetchOnMount: false,
placeholderData: config.features.zkEvmRollup.isEnabled ? TX_ZKEVM_L2 : TX,
placeholderData: rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? TX_ZKEVM_L2 : TX,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
......
import { Skeleton, VStack } from '@chakra-ui/react';
import React from 'react';
import type { L2TxnBatchesItem } from 'types/api/l2TxnBatches';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
......@@ -13,14 +13,14 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
type Props = { item: OptimisticL2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesListItem = ({ item, isLoading }: Props) => {
const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -72,7 +72,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => {
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
truncation="constant_long"
/>
)) }
</VStack>
......@@ -87,4 +87,4 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => {
);
};
export default TxnBatchesListItem;
export default OptimisticL2TxnBatchesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { L2TxnBatchesItem } from 'types/api/l2TxnBatches';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import TxnBatchesTableItem from './TxnBatchesTableItem';
import OptimisticL2TxnBatchesTableItem from './OptimisticL2TxnBatchesTableItem';
type Props = {
items: Array<L2TxnBatchesItem>;
items: Array<OptimisticL2TxnBatchesItem>;
top: number;
isLoading?: boolean;
}
const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Thead top={ top }>
......@@ -27,7 +27,7 @@ const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
</Thead>
<Tbody>
{ items.map((item, index) => (
<TxnBatchesTableItem
<OptimisticL2TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
......@@ -38,4 +38,4 @@ const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
);
};
export default TxnBatchesTable;
export default OptimisticL2TxnBatchesTable;
import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { L2TxnBatchesItem } from 'types/api/l2TxnBatches';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
......@@ -12,14 +12,14 @@ import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
type Props = { item: OptimisticL2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -32,6 +32,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td>
......@@ -64,7 +65,8 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
truncation="constant_long"
noIcon
/>
)) }
</VStack>
......@@ -78,4 +80,4 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
);
};
export default TxnBatchesTableItem;
export default OptimisticL2TxnBatchesTableItem;
......@@ -3,8 +3,8 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { ZKEVM_L2_TX_BATCH_STATUSES } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2TxnBatches';
import { ZKEVM_L2_TX_BATCH_STATUSES } from 'types/api/zkEvmL2';
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
......
import { Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
const feature = config.features.zkEvmRollup;
const rollupFeature = config.features.rollup;
type Props = { item: ZkEvmL2TxnBatchesItem; isLoading?: boolean };
const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') {
return null;
}
......@@ -29,7 +29,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ZkEvmBatchEntityL2
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
......@@ -69,7 +69,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
hash={ item.verify_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
truncation="constant_long"
/>
) : <Text>Pending</Text> }
</ListItemMobileGrid.Value>
......@@ -82,7 +82,7 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
hash={ item.sequence_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
truncation="constant_long"
/>
) : <Text>Pending</Text> }
</ListItemMobileGrid.Value>
......
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { default as Thead } from 'ui/shared/TheadSticky';
......@@ -15,15 +15,15 @@ type Props = {
const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Table variant="simple" size="sm" minW="1000px">
<Thead top={ top }>
<Tr>
<Th width="170px">Batch #</Th>
<Th width="150px">Status</Th>
<Th width="33%">Batch #</Th>
<Th width="33%">Status</Th>
<Th width="150px">Age</Th>
<Th width="170px">Txn count</Th>
<Th width="50%">Verify Tx Has</Th>
<Th width="50%">Sequence hash</Th>
<Th width="150px">Txn count</Th>
<Th width="230px">Verify Tx Has</Th>
<Th width="230px">Sequence hash</Th>
</Tr>
</Thead>
<Tbody>
......
import { Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2TxnBatches';
import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import ZkEvmBatchEntityL2 from 'ui/shared/entities/block/ZkEvmBatchEntityL2';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
const feature = config.features.zkEvmRollup;
const rollupFeature = config.features.rollup;
type Props = { item: ZkEvmL2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.timestamp).fromNow();
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') {
return null;
}
return (
<Tr>
<Td verticalAlign="middle">
<ZkEvmBatchEntityL2
<BatchEntityL2
isLoading={ isLoading }
number={ item.number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
......@@ -59,7 +60,8 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
hash={ item.verify_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
truncation="constant_long"
noIcon
/>
) : <Text>Pending</Text> }
</Td>
......@@ -70,7 +72,8 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
hash={ item.sequence_tx_hash }
fontSize="sm"
lineHeight={ 5 }
maxW="100%"
truncation="constant_long"
noIcon
/>
) : <Text>Pending</Text> }
</Td>
......
......@@ -49,7 +49,7 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI
<TxEntity
isLoading={ isLoading }
hash={ tx.hash }
truncation="constant"
truncation="constant_long"
fontWeight="700"
/>
{ tx.timestamp && (
......
......@@ -49,11 +49,11 @@ const TxsTable = ({
<TheadSticky top={ top }>
<Tr>
<Th width="54px"></Th>
<Th width="22%">Txn hash</Th>
<Th width="180px">Txn hash</Th>
<Th width="160px">Type</Th>
<Th width="20%">Method</Th>
{ showBlockInfo && <Th width="18%">Block</Th> }
<Th width={{ base: '224px', xl: '360px' }}>From/To</Th>
<Th width="224px">From/To</Th>
{ !config.UI.views.tx.hiddenFields?.value && (
<Th width="20%" isNumeric>
<Link onClick={ sort('value') } display="flex" justifyContent="end">
......
......@@ -55,6 +55,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
fontWeight={ 700 }
noIcon
maxW="100%"
truncation="constant_long"
/>
{ tx.timestamp && <Skeleton color="text_secondary" fontWeight="400" isLoaded={ !isLoading }><span>{ timeAgo }</span></Skeleton> }
</VStack>
......@@ -94,7 +95,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
current={ currentAddress }
isLoading={ isLoading }
mt="2px"
mode={{ lg: 'compact', xl: 'long' }}
mode="compact"
/>
</Td>
{ !config.UI.views.tx.hiddenFields?.value && (
......
......@@ -28,7 +28,7 @@ const UserOpsListItem = ({ item, isLoading, showTx, showSender }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>User op hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<UserOpEntity hash={ item.hash } isLoading={ isLoading } fontWeight="700" noIcon/>
<UserOpEntity hash={ item.hash } isLoading={ isLoading } fontWeight="700" noIcon truncation="constant_long"/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
......@@ -48,6 +48,7 @@ const UserOpsListItem = ({ item, isLoading, showTx, showSender }: Props) => {
<UserOpsAddress
address={ item.address }
isLoading={ isLoading }
truncation="constant"
/>
</ListItemMobileGrid.Value>
</>
......@@ -61,6 +62,7 @@ const UserOpsListItem = ({ item, isLoading, showTx, showSender }: Props) => {
hash={ item.transaction_hash }
isLoading={ isLoading }
noIcon
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
</>
......
......@@ -18,7 +18,7 @@ import UserOpsTableItem from './UserOpsTableItem';
const UserOpsTable = ({ items, isLoading, top, showTx, showSender }: Props) => {
return (
<Table variant="simple" size="sm">
<Table variant="simple" size="sm" minW="1000px">
<Thead top={ top }>
<Tr>
<Th w="60%">User op hash</Th>
......
......@@ -25,7 +25,7 @@ const UserOpsTableItem = ({ item, isLoading, showTx, showSender }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
<UserOpEntity hash={ item.hash } isLoading={ isLoading } noIcon fontWeight={ 700 }/>
<UserOpEntity hash={ item.hash } isLoading={ isLoading } noIcon fontWeight={ 700 } truncation="constant_long"/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block"><span>{ timeAgo }</span></Skeleton>
......
......@@ -7,7 +7,7 @@ import type { WithdrawalsItem } from 'types/api/withdrawals';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import WithdrawalsListItem from './WithdrawalsListItem';
import BeaconChainWithdrawalsListItem from './BeaconChainWithdrawalsListItem';
type Props = {
isLoading?: boolean;
......@@ -34,7 +34,7 @@ const WithdrawalsList = ({ items, view, isLoading }: Props) => {
switch (view) {
case 'address': {
return (
<WithdrawalsListItem
<BeaconChainWithdrawalsListItem
key={ key }
item={ item as AddressWithdrawalsItem }
view={ view }
......@@ -44,7 +44,7 @@ const WithdrawalsList = ({ items, view, isLoading }: Props) => {
}
case 'block': {
return (
<WithdrawalsListItem
<BeaconChainWithdrawalsListItem
key={ key }
item={ item as BlockWithdrawalsItem }
view={ view }
......@@ -54,7 +54,7 @@ const WithdrawalsList = ({ items, view, isLoading }: Props) => {
}
case 'list': {
return (
<WithdrawalsListItem
<BeaconChainWithdrawalsListItem
key={ key }
item={ item as WithdrawalsItem }
view={ view }
......
......@@ -26,7 +26,7 @@ type Props = ({
view: 'block';
}) & { isLoading?: boolean };
const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
const BeaconChainWithdrawalsListItem = ({ item, isLoading, view }: Props) => {
if (!feature.isEnabled) {
return null;
}
......@@ -64,6 +64,7 @@ const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
<AddressEntity
address={ item.receiver }
isLoading={ isLoading }
truncation="constant"
/>
</ListItemMobileGrid.Value>
</>
......@@ -87,4 +88,4 @@ const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
);
};
export default WithdrawalsListItem;
export default BeaconChainWithdrawalsListItem;
......@@ -9,7 +9,7 @@ import config from 'configs/app';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem';
import BeaconChainWithdrawalsTableItem from './BeaconChainWithdrawalsTableItem';
const feature = config.features.beaconChain;
......@@ -27,7 +27,7 @@ const feature = config.features.beaconChain;
view: 'block';
});
const WithdrawalsTable = ({ items, isLoading, top, view }: Props) => {
const BeaconChainWithdrawalsTable = ({ items, isLoading, top, view }: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(items, !isLoading);
if (!feature.isEnabled) {
......@@ -48,13 +48,13 @@ const WithdrawalsTable = ({ items, isLoading, top, view }: Props) => {
</Thead>
<Tbody>
{ view === 'list' && (items as Array<WithdrawalsItem>).slice(0, renderedItemsNum).map((item, index) => (
<WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="list" isLoading={ isLoading }/>
<BeaconChainWithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="list" isLoading={ isLoading }/>
)) }
{ view === 'address' && (items as Array<AddressWithdrawalsItem>).slice(0, renderedItemsNum).map((item, index) => (
<WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="address" isLoading={ isLoading }/>
<BeaconChainWithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="address" isLoading={ isLoading }/>
)) }
{ view === 'block' && (items as Array<BlockWithdrawalsItem>).slice(0, renderedItemsNum).map((item, index) => (
<WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="block" isLoading={ isLoading }/>
<BeaconChainWithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="block" isLoading={ isLoading }/>
)) }
<tr ref={ cutRef }/>
</Tbody>
......@@ -62,4 +62,4 @@ const WithdrawalsTable = ({ items, isLoading, top, view }: Props) => {
);
};
export default WithdrawalsTable;
export default BeaconChainWithdrawalsTable;
......@@ -21,7 +21,7 @@ type Props = ({
view: 'block';
}) & { isLoading?: boolean };
const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => {
const BeaconChainWithdrawalsTableItem = ({ item, view, isLoading }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
......@@ -37,6 +37,7 @@ const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => {
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
noIcon
/>
</Td>
) }
......@@ -63,4 +64,4 @@ const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => {
);
};
export default WithdrawalsTableItem;
export default BeaconChainWithdrawalsTableItem;
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals';
import type { OptimisticL2WithdrawalsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
......@@ -11,15 +11,15 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
type Props = { item: OptimisticL2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsListItem = ({ item, isLoading }: Props) => {
const OptimisticL2WithdrawalsListItem = ({ item, isLoading }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : null;
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : null;
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -40,6 +40,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
<AddressEntity
address={ item.from }
isLoading={ isLoading }
truncation="constant"
/>
</ListItemMobileGrid.Value>
</>
......@@ -52,6 +53,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
hash={ item.l2_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
......@@ -68,8 +70,8 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.status === 'Ready for relay' ?
<LinkExternal href={ feature.withdrawalUrl }>{ item.status }</LinkExternal> :
{ item.status === 'Ready for relay' && rollupFeature.L2WithdrawalUrl ?
<LinkExternal href={ rollupFeature.L2WithdrawalUrl }>{ item.status }</LinkExternal> :
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton> }
</ListItemMobileGrid.Value>
......@@ -82,6 +84,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
hash={ item.l1_tx_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
</>
......@@ -98,4 +101,4 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
);
};
export default WithdrawalsListItem;
export default OptimisticL2WithdrawalsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals';
import type { OptimisticL2WithdrawalsItem } from 'types/api/optimisticL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem';
import OptimisticL2WithdrawalsTableItem from './OptimisticL2WithdrawalsTableItem';
type Props = {
items: Array<L2WithdrawalsItem>;
items: Array<OptimisticL2WithdrawalsItem>;
top: number;
isLoading?: boolean;
}
const WithdrawalsTable = ({ items, top, isLoading }: Props) => {
const OptimisticL2WithdrawalsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
......@@ -29,11 +29,11 @@ const WithdrawalsTable = ({ items, top, isLoading }: Props) => {
</Thead>
<Tbody>
{ items.map((item, index) => (
<WithdrawalsTableItem key={ item.l2_tx_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
<OptimisticL2WithdrawalsTableItem key={ item.l2_tx_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default WithdrawalsTable;
export default OptimisticL2WithdrawalsTable;
import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals';
import type { OptimisticL2WithdrawalsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
......@@ -10,15 +10,15 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal';
const feature = config.features.optimisticRollup;
const rollupFeature = config.features.rollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
type Props = { item: OptimisticL2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const OptimisticL2WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
if (!feature.isEnabled) {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -40,9 +40,10 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
<TxEntity
isLoading={ isLoading }
hash={ item.l2_tx_hash }
truncation="constant"
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
......@@ -51,8 +52,8 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Skeleton>
</Td>
<Td verticalAlign="middle">
{ item.status === 'Ready for relay' ?
<LinkExternal href={ feature.withdrawalUrl }>{ item.status }</LinkExternal> :
{ item.status === 'Ready for relay' && rollupFeature.L2WithdrawalUrl ?
<LinkExternal href={ rollupFeature.L2WithdrawalUrl }>{ item.status }</LinkExternal> :
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton>
}
</Td>
......@@ -61,7 +62,8 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1_tx_hash }
truncation="constant"
truncation="constant_long"
noIcon
fontSize="sm"
lineHeight={ 5 }
/>
......@@ -76,4 +78,4 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
);
};
export default WithdrawalsTableItem;
export default OptimisticL2WithdrawalsTableItem;
......@@ -2981,6 +2981,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@hypelab/sdk-react@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@hypelab/sdk-react/-/sdk-react-1.0.0.tgz#399058576422a7a715edfd6c55b946950a26ba39"
integrity sha512-d8eJOqgocXvfMae/JbFXZdK/Q79oYYXScto1wJ7uTDHxrPZTIpgVNhuJM7UKmVJAKRSHnniRcZCjh2+pZCW5ww==
"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
......
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