Commit 843a0471 authored by tom goriunov's avatar tom goriunov Committed by GitHub

OP Superchain Explorer: MVP (#2786)

* context for storing subchain config and tx view for subchain

* address page and block page for subchain

* address aggregated page: local txs tab

* connect MultichainSelect to context

* home page placeholder

* refactor subchain select state

* envs for demo

* fix ts

* clear value for l2 review

* subchain widgets on home page

* csp policies

* sockets, duck and goose

* fix socket on subchain address page

* link builder for subchain views

* update home widget

* fix time increment

* enable tx interpretator and metadata service

* generate multichain config based on every chain app config

* Fix the multichain config to work in Docker

* multichain socket test

* rename subchain-id to subchain-slug path param

* refactoring

* update chain icons on entities

* home page: latest local txs

* latest cross-chain txs

* minor improvements

* renaming, pt. 1

* rename chain routes

* enable blockchain interaction

* add loading state to icon shield

* fix build

* fix tests

* update types package
parent a01299b6
......@@ -25,6 +25,7 @@ on:
- neon_devnet
- optimism
- optimism_sepolia
- optimism_superchain
- polygon
- rootstock
- scroll_sepolia
......
......@@ -27,6 +27,7 @@ on:
- neon_devnet
- optimism
- optimism_sepolia
- optimism_superchain
- polygon
- rari_testnet
- rootstock
......
......@@ -14,6 +14,7 @@
/out/
/public/assets/envs.js
/public/assets/configs
/public/assets/multichain
/public/icons/sprite.svg
/public/icons/sprite.*.svg
/public/icons/registry.json
......
......@@ -377,6 +377,7 @@
"optimism",
"optimism_interop_0",
"optimism_sepolia",
"optimism_superchain",
"polygon",
"rari_testnet",
"rootstock_testnet",
......
......@@ -45,6 +45,12 @@ WORKDIR /sitemap-generator
COPY ./deploy/tools/sitemap-generator/package.json ./deploy/tools/sitemap-generator/yarn.lock ./
RUN yarn --frozen-lockfile --network-timeout 100000
### MULTICHAIN CONFIG GENERATOR
# Install dependencies
WORKDIR /multichain-config-generator
COPY ./deploy/tools/multichain-config-generator/package.json ./deploy/tools/multichain-config-generator/yarn.lock ./
RUN yarn --frozen-lockfile --network-timeout 100000
# *****************************
# ****** STAGE 2: Build *******
......@@ -106,6 +112,11 @@ COPY --from=deps /favicon-generator/node_modules ./deploy/tools/favicon-generato
# Copy dependencies and source code
COPY --from=deps /sitemap-generator/node_modules ./deploy/tools/sitemap-generator/node_modules
### MULTICHAIN CONFIG GENERATOR
# Copy dependencies and source code, then build
COPY --from=deps /multichain-config-generator/node_modules ./deploy/tools/multichain-config-generator/node_modules
RUN cd ./deploy/tools/multichain-config-generator && yarn build
# *****************************
# ******* STAGE 3: Run ********
......@@ -130,8 +141,11 @@ RUN chown nextjs:nodejs .next
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
# Copy tools
COPY --from=builder /app/deploy/tools/envs-validator/index.js ./envs-validator.js
COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-reporter.js
COPY --from=builder /app/deploy/tools/multichain-config-generator/dist ./deploy/tools/multichain-config-generator/dist
# Copy scripts
## Entripoint
......
......@@ -7,6 +7,7 @@ import { getEnvValue } from './utils';
export interface ApiPropsBase {
endpoint: string;
basePath?: string;
socketEndpoint?: string;
}
export interface ApiPropsFull extends ApiPropsBase {
......@@ -100,6 +101,25 @@ const rewardsApi = (() => {
});
})();
const multichainApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST');
if (!apiHost) {
return;
}
try {
const url = new URL(apiHost);
return Object.freeze({
endpoint: apiHost,
socketEndpoint: `wss://${ url.host }`,
});
} catch (error) {
return;
}
})();
const statsApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_STATS_API_HOST');
if (!apiHost) {
......@@ -135,7 +155,7 @@ const visualizeApi = (() => {
});
})();
type Apis = {
export type Apis = {
general: ApiPropsFull;
} & Partial<Record<Exclude<ApiName, 'general'>, ApiPropsBase>>;
......@@ -145,6 +165,7 @@ const apis: Apis = Object.freeze({
bens: bensApi,
contractInfo: contractInfoApi,
metadata: metadataApi,
multichain: multichainApi,
rewards: rewardsApi,
stats: statsApi,
tac: tacApi,
......
......@@ -2,6 +2,7 @@ import type { Feature } from './types';
import chain from '../chain';
import { getEnvValue } from '../utils';
import opSuperchain from './opSuperchain';
const walletConnectProjectId = getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID');
......@@ -9,15 +10,21 @@ const title = 'Blockchain interaction (writing to contract, etc.)';
const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
if (
// all chain parameters are required for wagmi provider
// @wagmi/chains/dist/index.d.ts
const isSingleChain = Boolean(
chain.id &&
chain.name &&
chain.currency.name &&
chain.currency.symbol &&
chain.currency.decimals &&
chain.rpcUrls.length > 0 &&
chain.rpcUrls.length > 0,
);
const isOpSuperchain = opSuperchain.isEnabled;
if (
(isSingleChain || isOpSuperchain) &&
walletConnectProjectId
) {
return Object.freeze({
......
......@@ -26,6 +26,7 @@ export { default as mixpanel } from './mixpanel';
export { default as mudFramework } from './mudFramework';
export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService';
export { default as opSuperchain } from './opSuperchain';
export { default as pools } from './pools';
export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs';
......
import type { Feature } from './types';
import apis from '../apis';
import { getEnvValue } from '../utils';
const isEnabled = getEnvValue('NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED') === 'true';
const title = 'OP Superchain interop explorer';
const config: Feature<{ }> = (() => {
if (apis.multichain && isEnabled) {
return Object.freeze({
title,
isEnabled: true,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
# Set of ENVs for OP Mainnet network explorer
# https://xxx.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=optimism_superchain"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
# Instance ENVs
# TODO @tom2drum make these envs optional for multichain (adjust docs)
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=localhost
NEXT_PUBLIC_API_PORT=3001
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_NETWORK_ID=10
# TODO @tom2drum New ENVs (add to docs)
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST=https://multichain-aggregator.k8s-dev.blockscout.com
NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED=true
# TODO @tom2drum remove this
SKIP_ENVS_VALIDATION=true
NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap', 'secondary_coin_price']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)'],'text_color':['rgb(255, 255, 255)']}
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism-mainnet-light.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism-mainnet-dark.svg
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_NAME=OP Superchain
NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Superchain
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=['eth_rpc_api','rpc_api']
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=false
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_USE_NEXT_JS_PROXY=false
\ No newline at end of file
/* eslint-disable no-restricted-properties */
import type { MultichainConfig } from 'types/multichain';
import config from 'configs/app';
function isRunningInDocker() {
return process.env.HOSTNAME !== undefined;
}
let value: MultichainConfig | undefined = undefined;
async function fetchConfig() {
if (process.env.NEXT_RUNTIME !== 'edge') {
throw new Error('NEXT_RUNTIME is not edge');
}
// In edge runtime, we need to use absolute URLs
// When in Docker, use the internal hostname
const baseUrl = isRunningInDocker() ?
`http://${ process.env.HOSTNAME }:${ config.app.port || 3000 }` :
config.app.baseUrl;
const url = baseUrl + '/assets/multichain/config.json';
const response = await fetch(url);
const json = await response.json();
value = json as MultichainConfig;
return value;
}
export async function load() {
if (!value) {
return new Promise<MultichainConfig | undefined>((resolve, reject) => {
fetchConfig()
.then((value) => {
resolve(value);
}).catch((error) => {
reject(error);
});
});
}
return Promise.resolve(value);
}
export function getValue() {
return value;
}
import type { MultichainConfig } from 'types/multichain';
let value: MultichainConfig | undefined = undefined;
function readFileConfig() {
// eslint-disable-next-line no-restricted-properties
if (process.env.NEXT_RUNTIME !== 'nodejs') {
throw new Error('NEXT_RUNTIME is not nodejs');
}
try {
const path = require('path');
const { readFileSync } = require('fs');
const publicFolder = path.resolve('public');
const configPath = path.resolve(publicFolder, 'assets/multichain/config.json');
const config = readFileSync(configPath, 'utf8');
value = JSON.parse(config) as MultichainConfig;
return value;
} catch (error) {
return;
}
}
export async function load() {
if (!value) {
return new Promise<MultichainConfig | undefined>((resolve) => {
const value = readFileConfig();
resolve(value);
});
}
return Promise.resolve(value);
}
export function getValue() {
if (!value) {
return readFileConfig();
}
return value;
}
import type { MultichainConfig } from 'types/multichain';
import config from 'configs/app';
import * as multichainConfigNodejs from 'configs/multichain/config.nodejs';
import { isBrowser } from 'toolkit/utils/isBrowser';
const multichainConfig: () => MultichainConfig | undefined = () => {
if (!config.features.opSuperchain.isEnabled) {
return;
}
if (isBrowser()) {
return window.__multichainConfig;
}
return multichainConfigNodejs.getValue();
};
export default multichainConfig;
......@@ -64,6 +64,9 @@ node --no-warnings ./og_image_generator.js
# Create envs.js file with run-time environment variables for the client app
./make_envs_script.sh
# Generate multichain config
node ./deploy/tools/multichain-config-generator/dist/index.js
# Generate sitemap.xml and robots.txt files
./sitemap_generator.sh
......
node_modules
dist
*.log
.DS_Store
\ No newline at end of file
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve as resolvePath } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Worker } from 'node:worker_threads';
const currentFilePath = fileURLToPath(import.meta.url);
const currentDir = dirname(currentFilePath);
const EXPLORER_URLS = [
'https://optimism-interop-alpha-0.blockscout.com',
'https://optimism-interop-alpha-1.blockscout.com',
];
function getSlug(url: string) {
return new URL(url).hostname.replace('.blockscout.com', '').replace('.k8s-dev', '');
}
async function computeChainConfig(url: string): Promise<unknown> {
return new Promise((resolve, reject) => {
const workerPath = resolvePath(currentDir, 'worker.js');
const worker = new Worker(workerPath, {
workerData: { url },
env: {} // Start with empty environment
});
worker.on('message', (config) => {
resolve(config);
});
worker.on('error', (error) => {
console.error('Worker error:', error);
reject(error);
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${ code }`));
}
});
});
}
async function run() {
try {
if (!process.env.NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST) {
console.log('ℹ️ NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST is not set, skipping multichain config generation\n');
return;
}
console.log('🌀 Generating multichain config...');
const configs = await Promise.all(EXPLORER_URLS.map(computeChainConfig));
const config = {
chains: configs.map((config, index) => {
return {
slug: getSlug(EXPLORER_URLS[index]),
config,
};
}),
};
const outputDir = resolvePath(currentDir, '../../../../public/assets/multichain');
mkdirSync(outputDir, { recursive: true });
const outputPathJson = resolvePath(outputDir, 'config.json');
writeFileSync(outputPathJson, JSON.stringify(config, null, 2));
const outputPathJs = resolvePath(outputDir, 'config.js');
writeFileSync(outputPathJs, `window.__multichainConfig = ${ JSON.stringify(config) };`);
console.log('👍 Done!\n');
} catch (error) {
console.error('🚨 Error generating multichain config:', error);
console.log('\n');
process.exit(1);
}
}
run();
{
"name": "@blockscout/multichain-config-generator",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build --logLevel error",
"generate": "node dist/index.js"
},
"dependencies": {
},
"devDependencies": {
"typescript": "5.4.2",
"vite": "6.3.5",
"vite-plugin-dts": "4.5.4",
"@types/node": "22.12.0"
}
}
\ No newline at end of file
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"configs/*": ["../../../configs/*"],
"lib/*": ["../../../lib/*"],
"toolkit/*": ["../../../toolkit/*"],
"types/*": ["../../../types/*"]
},
"types": ["node"],
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": [
"index.ts",
"worker.ts",
"../app/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
\ No newline at end of file
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: {
index: resolve(__dirname, 'index.ts'),
worker: resolve(__dirname, 'worker.ts'),
},
formats: [ 'es' ],
fileName: (format, entryName) => `${ entryName }.js`,
},
rollupOptions: {
external: [ 'node:worker_threads', 'node:url', 'node:path', 'node:fs' ],
output: {
dir: 'dist',
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]',
},
},
},
resolve: {
alias: {
configs: resolve(__dirname, '../../../configs'),
lib: resolve(__dirname, '../../../lib'),
toolkit: resolve(__dirname, '../../../toolkit'),
types: resolve(__dirname, '../../../types'),
},
preserveSymlinks: true,
},
});
/* eslint-disable no-console */
import { parentPort, workerData } from 'node:worker_threads';
interface WorkerData {
url: string;
}
interface ChainConfig {
envs: Record<string, string>;
}
async function fetchChainConfig(url: string): Promise<ChainConfig> {
const response = await fetch(`${ url }/node-api/config`);
if (!response.ok) {
throw new Error(`Failed to fetch config from ${ url }: ${ response.statusText }`);
}
const config = await response.json();
return config as ChainConfig;
}
async function computeConfig() {
try {
const { url } = workerData as WorkerData;
console.log(' ⏳ Fetching chain config from:', url);
// 1. Fetch chain config
const chainConfig = await fetchChainConfig(url);
// 2. Set environment variables
Object.entries(chainConfig.envs).forEach(([ key, value ]) => {
// eslint-disable-next-line no-restricted-properties
process.env[key] = value;
});
// 3. Import and compute app config
const { 'default': appConfig } = await import('configs/app/index');
console.log(' ✅ Config computed for:', url);
// 4. Send config back to main thread
parentPort?.postMessage(appConfig);
} catch (error) {
console.error(' ❌ Worker error:', error);
process.exit(1);
}
}
computeConfig();
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/parser@^7.27.2":
version "7.27.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.5.tgz#ed22f871f110aa285a6fd934a0efed621d118826"
integrity sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==
dependencies:
"@babel/types" "^7.27.3"
"@babel/types@^7.27.3":
version "7.27.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec"
integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@esbuild/aix-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18"
integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==
"@esbuild/android-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f"
integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==
"@esbuild/android-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26"
integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==
"@esbuild/android-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff"
integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==
"@esbuild/darwin-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34"
integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==
"@esbuild/darwin-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418"
integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==
"@esbuild/freebsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c"
integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==
"@esbuild/freebsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f"
integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==
"@esbuild/linux-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8"
integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==
"@esbuild/linux-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911"
integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==
"@esbuild/linux-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783"
integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==
"@esbuild/linux-loong64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506"
integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==
"@esbuild/linux-mips64el@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96"
integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==
"@esbuild/linux-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9"
integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==
"@esbuild/linux-riscv64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e"
integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==
"@esbuild/linux-s390x@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d"
integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==
"@esbuild/linux-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4"
integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==
"@esbuild/netbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d"
integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==
"@esbuild/netbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79"
integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==
"@esbuild/openbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd"
integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==
"@esbuild/openbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0"
integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==
"@esbuild/sunos-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5"
integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==
"@esbuild/win32-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e"
integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==
"@esbuild/win32-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d"
integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==
"@esbuild/win32-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1"
integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@microsoft/api-extractor-model@7.30.6":
version "7.30.6"
resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.30.6.tgz#cd9c434521dda3b226cc0f6aefb9c20afaf99c92"
integrity sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==
dependencies:
"@microsoft/tsdoc" "~0.15.1"
"@microsoft/tsdoc-config" "~0.17.1"
"@rushstack/node-core-library" "5.13.1"
"@microsoft/api-extractor@^7.50.1":
version "7.52.8"
resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz#7cc944f44ca1b1ad9d7272ab5d98e81987c1f8ca"
integrity sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg==
dependencies:
"@microsoft/api-extractor-model" "7.30.6"
"@microsoft/tsdoc" "~0.15.1"
"@microsoft/tsdoc-config" "~0.17.1"
"@rushstack/node-core-library" "5.13.1"
"@rushstack/rig-package" "0.5.3"
"@rushstack/terminal" "0.15.3"
"@rushstack/ts-command-line" "5.0.1"
lodash "~4.17.15"
minimatch "~3.0.3"
resolve "~1.22.1"
semver "~7.5.4"
source-map "~0.6.1"
typescript "5.8.2"
"@microsoft/tsdoc-config@~0.17.1":
version "0.17.1"
resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz#e0f0b50628f4ad7fe121ca616beacfe6a25b9335"
integrity sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==
dependencies:
"@microsoft/tsdoc" "0.15.1"
ajv "~8.12.0"
jju "~1.4.0"
resolve "~1.22.2"
"@microsoft/tsdoc@0.15.1", "@microsoft/tsdoc@~0.15.1":
version "0.15.1"
resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz#d4f6937353bc4568292654efb0a0e0532adbcba2"
integrity sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==
"@rollup/pluginutils@^5.1.4":
version "5.1.4"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a"
integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^4.0.2"
"@rollup/rollup-android-arm-eabi@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz#f39f09f60d4a562de727c960d7b202a2cf797424"
integrity sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==
"@rollup/rollup-android-arm64@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz#d19af7e23760717f1d879d4ca3d2cd247742dff2"
integrity sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==
"@rollup/rollup-darwin-arm64@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz#1c3a2fbf205d80641728e05f4a56c909e95218b7"
integrity sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==
"@rollup/rollup-darwin-x64@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz#aa66d2ba1a25e609500e13bef06dc0e71cc0c0d4"
integrity sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==
"@rollup/rollup-freebsd-arm64@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz#df10a7b6316a0ef1028c6ca71a081124c537e30d"
integrity sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==
"@rollup/rollup-freebsd-x64@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz#a3fdce8a05e95b068cbcb46e4df5185e407d0c35"
integrity sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==
"@rollup/rollup-linux-arm-gnueabihf@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz#49f766c55383bd0498014a9d76924348c2f3890c"
integrity sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==
"@rollup/rollup-linux-arm-musleabihf@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz#1d4d7d32fc557e17d52e1857817381ea365e2959"
integrity sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==
"@rollup/rollup-linux-arm64-gnu@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz#f4fc317268441e9589edad3be8f62b6c03009bc1"
integrity sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==
"@rollup/rollup-linux-arm64-musl@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz#63a1f1b0671cb17822dabae827fef0e443aebeb7"
integrity sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==
"@rollup/rollup-linux-loongarch64-gnu@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz#c659b01cc6c0730b547571fc3973e1e955369f98"
integrity sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==
"@rollup/rollup-linux-powerpc64le-gnu@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz#612e746f9ad7e58480f964d65e0d6c3f4aae69a8"
integrity sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==
"@rollup/rollup-linux-riscv64-gnu@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz#4610dbd1dcfbbae32fbc10c20ae7387acb31110c"
integrity sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==
"@rollup/rollup-linux-riscv64-musl@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz#054911fab40dc83fafc21e470193c058108f19d8"
integrity sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==
"@rollup/rollup-linux-s390x-gnu@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz#98896eca8012547c7f04bd07eaa6896825f9e1a5"
integrity sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==
"@rollup/rollup-linux-x64-gnu@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz#01cf56844a1e636ee80dfb364e72c2b7142ad896"
integrity sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==
"@rollup/rollup-linux-x64-musl@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz#e67c7531df6dff0b4c241101d4096617fbca87c3"
integrity sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==
"@rollup/rollup-win32-arm64-msvc@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz#7eeada98444e580674de6989284e4baacd48ea65"
integrity sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==
"@rollup/rollup-win32-ia32-msvc@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz#516c4b54f80587b4a390aaf4940b40870271d35d"
integrity sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==
"@rollup/rollup-win32-x64-msvc@4.41.1":
version "4.41.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz#848f99b0d9936d92221bb6070baeff4db6947a30"
integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==
"@rushstack/node-core-library@5.13.1":
version "5.13.1"
resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-5.13.1.tgz#e56b915ecb08b5a92711acac6b233417353a32dc"
integrity sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==
dependencies:
ajv "~8.13.0"
ajv-draft-04 "~1.0.0"
ajv-formats "~3.0.1"
fs-extra "~11.3.0"
import-lazy "~4.0.0"
jju "~1.4.0"
resolve "~1.22.1"
semver "~7.5.4"
"@rushstack/rig-package@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@rushstack/rig-package/-/rig-package-0.5.3.tgz#ea4d8a3458540b1295500149c04e645f23134e5d"
integrity sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==
dependencies:
resolve "~1.22.1"
strip-json-comments "~3.1.1"
"@rushstack/terminal@0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@rushstack/terminal/-/terminal-0.15.3.tgz#365e0ae5ac73bb4883b096ae36c5011f52911861"
integrity sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==
dependencies:
"@rushstack/node-core-library" "5.13.1"
supports-color "~8.1.1"
"@rushstack/ts-command-line@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@rushstack/ts-command-line/-/ts-command-line-5.0.1.tgz#e147394b5ce87ef79db95b5b4f155461d6f2c50e"
integrity sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==
dependencies:
"@rushstack/terminal" "0.15.3"
"@types/argparse" "1.0.38"
argparse "~1.0.9"
string-argv "~0.3.1"
"@types/argparse@1.0.38":
version "1.0.38"
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9"
integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==
"@types/estree@1.0.7", "@types/estree@^1.0.0":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/node@22.12.0":
version "22.12.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.12.0.tgz#bf8af3b2af0837b5a62a368756ff2b705ae0048c"
integrity sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==
dependencies:
undici-types "~6.20.0"
"@volar/language-core@2.4.14", "@volar/language-core@~2.4.11":
version "2.4.14"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.14.tgz#dac7573014d4f3bafb186cb16888ffea5698be71"
integrity sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==
dependencies:
"@volar/source-map" "2.4.14"
"@volar/source-map@2.4.14":
version "2.4.14"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.14.tgz#cdcecd533c2e767449b2414cc22327d2bda7ef95"
integrity sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==
"@volar/typescript@^2.4.11":
version "2.4.14"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.14.tgz#b99a1025dd6a8b751e96627ebcb0739ceed0e5f1"
integrity sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==
dependencies:
"@volar/language-core" "2.4.14"
path-browserify "^1.0.1"
vscode-uri "^3.0.8"
"@vue/compiler-core@3.5.16":
version "3.5.16"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.16.tgz#2f95f4f17c16c09c57bbf64399075b921506630b"
integrity sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==
dependencies:
"@babel/parser" "^7.27.2"
"@vue/shared" "3.5.16"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@^3.5.0":
version "3.5.16"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz#151d8390252975c0b1a773029220fdfcfaa2d743"
integrity sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==
dependencies:
"@vue/compiler-core" "3.5.16"
"@vue/shared" "3.5.16"
"@vue/compiler-vue2@^2.7.16":
version "2.7.16"
resolved "https://registry.yarnpkg.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz#2ba837cbd3f1b33c2bc865fbe1a3b53fb611e249"
integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
"@vue/language-core@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.2.0.tgz#e48c54584f889f78b120ce10a050dfb316c7fcdf"
integrity sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==
dependencies:
"@volar/language-core" "~2.4.11"
"@vue/compiler-dom" "^3.5.0"
"@vue/compiler-vue2" "^2.7.16"
"@vue/shared" "^3.5.0"
alien-signals "^0.4.9"
minimatch "^9.0.3"
muggle-string "^0.4.1"
path-browserify "^1.0.1"
"@vue/shared@3.5.16", "@vue/shared@^3.5.0":
version "3.5.16"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.16.tgz#d5ea7671182742192938a4b4cbf86ef12bef7418"
integrity sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==
acorn@^8.14.0:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
ajv-draft-04@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8"
integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==
ajv-formats@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578"
integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==
dependencies:
ajv "^8.0.0"
ajv@^8.0.0:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
dependencies:
fast-deep-equal "^3.1.3"
fast-uri "^3.0.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
ajv@~8.12.0:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ajv@~8.13.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91"
integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==
dependencies:
fast-deep-equal "^3.1.3"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.4.1"
alien-signals@^0.4.9:
version "0.4.14"
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-0.4.14.tgz#9ff8f72a272300a51692f54bd9bbbada78fbf539"
integrity sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==
argparse@~1.0.9:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
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"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
compare-versions@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.1.tgz#7af3cc1099ba37d244b3145a9af5201b629148a9"
integrity sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==
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==
confbox@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
confbox@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110"
integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
debug@^4.4.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
dependencies:
ms "^2.1.3"
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
esbuild@^0.25.0:
version "0.25.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430"
integrity sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.5"
"@esbuild/android-arm" "0.25.5"
"@esbuild/android-arm64" "0.25.5"
"@esbuild/android-x64" "0.25.5"
"@esbuild/darwin-arm64" "0.25.5"
"@esbuild/darwin-x64" "0.25.5"
"@esbuild/freebsd-arm64" "0.25.5"
"@esbuild/freebsd-x64" "0.25.5"
"@esbuild/linux-arm" "0.25.5"
"@esbuild/linux-arm64" "0.25.5"
"@esbuild/linux-ia32" "0.25.5"
"@esbuild/linux-loong64" "0.25.5"
"@esbuild/linux-mips64el" "0.25.5"
"@esbuild/linux-ppc64" "0.25.5"
"@esbuild/linux-riscv64" "0.25.5"
"@esbuild/linux-s390x" "0.25.5"
"@esbuild/linux-x64" "0.25.5"
"@esbuild/netbsd-arm64" "0.25.5"
"@esbuild/netbsd-x64" "0.25.5"
"@esbuild/openbsd-arm64" "0.25.5"
"@esbuild/openbsd-x64" "0.25.5"
"@esbuild/sunos-x64" "0.25.5"
"@esbuild/win32-arm64" "0.25.5"
"@esbuild/win32-ia32" "0.25.5"
"@esbuild/win32-x64" "0.25.5"
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
exsolve@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf"
integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-uri@^3.0.1:
version "3.0.6"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748"
integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==
fdir@^6.4.4:
version "6.4.5"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.5.tgz#328e280f3a23699362f95f2e82acf978a0c0cb49"
integrity sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==
fs-extra@~11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
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==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
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==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
import-lazy@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
is-core-module@^2.16.0:
version "2.16.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
dependencies:
hasown "^2.0.2"
jju@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
kolorist@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==
local-pkg@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.1.tgz#f5fe74a97a3bd3c165788ee08ca9fbe998dc58dd"
integrity sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==
dependencies:
mlly "^1.7.4"
pkg-types "^2.0.1"
quansync "^0.2.8"
lodash@~4.17.15:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
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"
magic-string@^0.30.17:
version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
minimatch@^9.0.3:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
minimatch@~3.0.3:
version "3.0.8"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==
dependencies:
brace-expansion "^1.1.7"
mlly@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f"
integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==
dependencies:
acorn "^8.14.0"
pathe "^2.0.1"
pkg-types "^1.3.0"
ufo "^1.5.4"
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
muggle-string@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
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==
pathe@^2.0.1, pathe@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
pkg-types@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df"
integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==
dependencies:
confbox "^0.1.8"
mlly "^1.7.4"
pathe "^2.0.1"
pkg-types@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.1.0.tgz#70c9e1b9c74b63fdde749876ee0aa007ea9edead"
integrity sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==
dependencies:
confbox "^0.2.1"
exsolve "^1.0.1"
pathe "^2.0.3"
postcss@^8.5.3:
version "8.5.4"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.4.tgz#d61014ac00e11d5f58458ed7247d899bd65f99c0"
integrity sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
quansync@^0.2.8:
version "0.2.10"
resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.10.tgz#32053cf166fa36511aae95fc49796116f2dc20e1"
integrity sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
resolve@~1.22.1, resolve@~1.22.2:
version "1.22.10"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
dependencies:
is-core-module "^2.16.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
rollup@^4.34.9:
version "4.41.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.1.tgz#46ddc1b33cf1b0baa99320d3b0b4973dc2253b6a"
integrity sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==
dependencies:
"@types/estree" "1.0.7"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.41.1"
"@rollup/rollup-android-arm64" "4.41.1"
"@rollup/rollup-darwin-arm64" "4.41.1"
"@rollup/rollup-darwin-x64" "4.41.1"
"@rollup/rollup-freebsd-arm64" "4.41.1"
"@rollup/rollup-freebsd-x64" "4.41.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.41.1"
"@rollup/rollup-linux-arm-musleabihf" "4.41.1"
"@rollup/rollup-linux-arm64-gnu" "4.41.1"
"@rollup/rollup-linux-arm64-musl" "4.41.1"
"@rollup/rollup-linux-loongarch64-gnu" "4.41.1"
"@rollup/rollup-linux-powerpc64le-gnu" "4.41.1"
"@rollup/rollup-linux-riscv64-gnu" "4.41.1"
"@rollup/rollup-linux-riscv64-musl" "4.41.1"
"@rollup/rollup-linux-s390x-gnu" "4.41.1"
"@rollup/rollup-linux-x64-gnu" "4.41.1"
"@rollup/rollup-linux-x64-musl" "4.41.1"
"@rollup/rollup-win32-arm64-msvc" "4.41.1"
"@rollup/rollup-win32-ia32-msvc" "4.41.1"
"@rollup/rollup-win32-x64-msvc" "4.41.1"
fsevents "~2.3.2"
semver@~7.5.4:
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"
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
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==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
string-argv@~0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6"
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
strip-json-comments@~3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
supports-color@~8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
dependencies:
has-flag "^4.0.0"
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==
tinyglobby@^0.2.13:
version "0.2.14"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==
dependencies:
fdir "^6.4.4"
picomatch "^4.0.2"
typescript@5.4.2:
version "5.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
typescript@5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
ufo@^1.5.4:
version "1.6.1"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"
integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==
undici-types@~6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
uri-js@^4.2.2, uri-js@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
vite-plugin-dts@4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-4.5.4.tgz#51b60aaaa760d9cf5c2bb3676c69d81910d6b08c"
integrity sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==
dependencies:
"@microsoft/api-extractor" "^7.50.1"
"@rollup/pluginutils" "^5.1.4"
"@volar/typescript" "^2.4.11"
"@vue/language-core" "2.2.0"
compare-versions "^6.1.1"
debug "^4.4.0"
kolorist "^1.8.0"
local-pkg "^1.0.0"
magic-string "^0.30.17"
vite@6.3.5:
version "6.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.5.tgz#fec73879013c9c0128c8d284504c6d19410d12a3"
integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.4"
picomatch "^4.0.2"
postcss "^8.5.3"
rollup "^4.34.9"
tinyglobby "^0.2.13"
optionalDependencies:
fsevents "~2.3.3"
vscode-uri@^3.0.8:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
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==
......@@ -48,32 +48,8 @@ frontend:
cpu: 250m
env:
NEXT_PUBLIC_APP_ENV: review
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json
NEXT_PUBLIC_API_HOST: base.blockscout.com
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST: https://stats-l2-base-mainnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
NEXT_PUBLIC_NETWORK_RPC_URL: https://mainnet.base.org
NEXT_PUBLIC_WEB3_WALLETS: "['coinbase']"
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: true
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs']"
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_NAME_SERVICE_API_HOST: https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata.services.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE: optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL: https://eth.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
NEXT_PUBLIC_NAVIGATION_LAYOUT: horizontal
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/blocks','/name-domains']"
SKIP_ENVS_VALIDATION: true
envFromSecret:
NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY
import type { MultichainConfig } from 'types/multichain';
import type { WalletProvider } from 'types/web3';
type CPreferences = {
......@@ -19,6 +20,7 @@ declare global {
};
abkw: string;
__envs: Record<string, string>;
__multichainConfig: MultichainConfig;
}
namespace NodeJS {
......
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NEXT_OPEN_TELEMETRY_ENABLED === 'true') {
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (process.env.NEXT_OPEN_TELEMETRY_ENABLED === 'true') {
await import('./instrumentation.node');
}
await import('./startup.node');
}
}
......@@ -2,6 +2,7 @@ import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from '
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useMultichainContext } from 'lib/contexts/multichain';
import parseMetaPayload from './parseMetaPayload';
......@@ -9,14 +10,18 @@ export default function useAddressMetadataInfoQuery(addresses: Array<string>, is
const resource = 'metadata:info';
const multichainContext = useMultichainContext();
const feature = multichainContext?.chain?.config.features.addressMetadata || config.features.addressMetadata;
const chainId = multichainContext?.chain?.config.chain.id || config.chain.id;
return useApiQuery<typeof resource, unknown, AddressMetadataInfoFormatted>(resource, {
queryParams: {
addresses,
chainId: config.chain.id,
chainId,
tagsLimit: '20',
},
queryOptions: {
enabled: isEnabled && addresses.length > 0 && config.features.addressMetadata.isEnabled,
enabled: isEnabled && addresses.length > 0 && feature.isEnabled && Boolean(chainId),
select: (data) => {
const addresses = Object.entries(data.addresses)
.map(([ address, { tags, reputation } ]) => {
......
import { compile } from 'path-to-regexp';
import type { ChainConfig } from 'types/multichain';
import config from 'configs/app';
import getResourceParams from './getResourceParams';
......@@ -11,8 +13,9 @@ export default function buildUrl<R extends ResourceName>(
pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
noProxy?: boolean,
chain?: ChainConfig,
): string {
const { api, resource } = getResourceParams(resourceFullName);
const { api, resource } = getResourceParams(resourceFullName, chain);
const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : api.endpoint;
const basePath = api.basePath ?? '';
const path = !noProxy && isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
......
import type { ApiName, ApiResource } from './types';
import type { ChainConfig } from 'types/multichain';
import config from 'configs/app';
import type { ResourceName } from './resources';
import { RESOURCES } from './resources';
export default function getResourceParams(resourceFullName: ResourceName) {
export default function getResourceParams(resourceFullName: ResourceName, chain?: ChainConfig) {
const [ apiName, resourceName ] = resourceFullName.split(':') as [ ApiName, string ];
const apiConfig = config.apis[apiName];
const apiConfig = (() => {
if (chain) {
return chain.config.apis[apiName];
}
return config.apis[apiName];
})();
if (!apiConfig) {
throw new Error(`API config for ${ apiName } not found`);
......
import appConfig from 'configs/app';
export default function getSocketUrl(config: typeof appConfig = appConfig) {
return `${ config.apis.general.socketEndpoint }${ config.apis.general.basePath ?? '' }/socket/v2`;
}
......@@ -10,6 +10,8 @@ import { GENERAL_API_RESOURCES } from './services/general';
import type { GeneralApiResourceName, GeneralApiResourcePayload, GeneralApiPaginationFilters, GeneralApiPaginationSorting } from './services/general';
import type { MetadataApiResourceName, MetadataApiResourcePayload } from './services/metadata';
import { METADATA_API_RESOURCES } from './services/metadata';
import type { MultichainApiResourceName, MultichainApiResourcePayload } from './services/multichain';
import { MULTICHAIN_API_RESOURCES } from './services/multichain';
import type { RewardsApiResourceName, RewardsApiResourcePayload } from './services/rewards';
import { REWARDS_API_RESOURCES } from './services/rewards';
import type { StatsApiResourceName, StatsApiResourcePayload } from './services/stats';
......@@ -30,6 +32,7 @@ export const RESOURCES = {
contractInfo: CONTRACT_INFO_API_RESOURCES,
general: GENERAL_API_RESOURCES,
metadata: METADATA_API_RESOURCES,
multichain: MULTICHAIN_API_RESOURCES,
rewards: REWARDS_API_RESOURCES,
stats: STATS_API_RESOURCES,
tac: TAC_OPERATION_LIFECYCLE_API_RESOURCES,
......@@ -51,6 +54,7 @@ R extends BensApiResourceName ? BensApiResourcePayload<R> :
R extends ContractInfoApiResourceName ? ContractInfoApiResourcePayload<R> :
R extends GeneralApiResourceName ? GeneralApiResourcePayload<R> :
R extends MetadataApiResourceName ? MetadataApiResourcePayload<R> :
R extends MultichainApiResourceName ? MultichainApiResourcePayload<R> :
R extends RewardsApiResourceName ? RewardsApiResourcePayload<R> :
R extends StatsApiResourceName ? StatsApiResourcePayload<R> :
R extends TacOperationLifecycleApiResourceName ? TacOperationLifecycleApiResourcePayload<R> :
......
import type { ApiResource } from '../types';
import type * as multichain from '@blockscout/multichain-aggregator-types';
export const MULTICHAIN_API_RESOURCES = {
interop_messages: {
path: '/api/v1/interop/messages',
paginated: true,
},
} satisfies Record<string, ApiResource>;
export type MultichainApiResourceName = `multichain:${ keyof typeof MULTICHAIN_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type MultichainApiResourcePayload<R extends MultichainApiResourceName> =
R extends 'multichain:interop_messages' ? multichain.ListInteropMessagesResponse :
never;
/* eslint-enable @stylistic/indent */
export type ApiName = 'general' | 'admin' | 'bens' | 'contractInfo' | 'metadata' | 'rewards' | 'stats' | 'visualize' | 'tac';
export type ApiName = 'general' | 'admin' | 'bens' | 'contractInfo' | 'metadata' | 'multichain' | 'rewards' | 'stats' | 'tac' | 'visualize';
export interface ApiResource {
path: string;
......
......@@ -3,6 +3,7 @@ import { omit, pickBy } from 'es-toolkit';
import React from 'react';
import type { CsrfData } from 'types/client/account';
import type { ChainConfig } from 'types/multichain';
import config from 'configs/app';
import isBodyAllowed from 'lib/api/isBodyAllowed';
......@@ -21,21 +22,23 @@ export interface Params<R extends ResourceName> {
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
logError?: boolean;
chain?: ChainConfig;
}
export default function useApiFetch() {
const fetch = useFetch();
const queryClient = useQueryClient();
const { token: csrfToken } = queryClient.getQueryData<CsrfData>(getResourceKey('general:csrf')) || {};
return React.useCallback(<R extends ResourceName, SuccessType = unknown, ErrorType = unknown>(
resourceName: R,
{ pathParams, queryParams, fetchParams, logError }: Params<R> = {},
{ pathParams, queryParams, fetchParams, logError, chain }: Params<R> = {},
) => {
const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
const { api, apiName, resource } = getResourceParams(resourceName);
const url = buildUrl(resourceName, pathParams, queryParams);
const { api, apiName, resource } = getResourceParams(resourceName, chain);
const url = buildUrl(resourceName, pathParams, queryParams, undefined, chain);
const withBody = isBodyAllowed(fetchParams?.method);
const headers = pickBy({
'x-endpoint': api.endpoint && apiName !== 'general' && isNeedProxy() ? api.endpoint : undefined,
......
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import multichainConfig from 'configs/multichain';
import { useMultichainContext } from 'lib/contexts/multichain';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
import type { ResourceError, ResourceName, ResourcePathParams, ResourcePayload } from './resources';
......@@ -12,29 +14,37 @@ export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'headers'>;
queryOptions?: Partial<Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryFn'>>;
logError?: boolean;
chainSlug?: string;
}
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
export interface GetResourceKeyParams<R extends ResourceName, E = unknown, D = ResourcePayload<R>>
extends Pick<Params<R, E, D>, 'pathParams' | 'queryParams'> {
chainSlug?: string;
}
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams, chainSlug }: GetResourceKeyParams<R> = {}) {
if (pathParams || queryParams) {
return [ resource, { ...pathParams, ...queryParams } ];
return [ resource, chainSlug, { ...pathParams, ...queryParams } ].filter(Boolean);
}
return [ resource ];
return [ resource, chainSlug ].filter(Boolean);
}
export default function useApiQuery<R extends ResourceName, E = unknown, D = ResourcePayload<R>>(
resource: R,
{ queryOptions, pathParams, queryParams, fetchParams, logError }: Params<R, E, D> = {},
{ queryOptions, pathParams, queryParams, fetchParams, logError, chainSlug }: Params<R, E, D> = {},
) {
const apiFetch = useApiFetch();
const { chain } = useMultichainContext() ||
{ chain: chainSlug ? multichainConfig()?.chains.find((chain) => chain.slug === chainSlug) : undefined };
return useQuery<ResourcePayload<R>, ResourceError<E>, D>({
queryKey: queryOptions?.queryKey || getResourceKey(resource, { pathParams, queryParams }),
queryKey: queryOptions?.queryKey || getResourceKey(resource, { pathParams, queryParams, chainSlug: chain?.slug }),
queryFn: async({ signal }) => {
// all errors and error typing is handled by react-query
// so error response will never go to the data
// that's why we are safe here to do type conversion "as Promise<ResourcePayload<R>>"
return apiFetch(resource, { pathParams, queryParams, logError, fetchParams: { ...fetchParams, signal } }) as Promise<ResourcePayload<R>>;
return apiFetch(resource, { pathParams, queryParams, chain, logError, fetchParams: { ...fetchParams, signal } }) as Promise<ResourcePayload<R>>;
},
...queryOptions,
});
......
import { useRouter } from 'next/router';
import React from 'react';
import type { ChainConfig } from 'types/multichain';
import multichainConfig from 'configs/multichain';
import getQueryParamString from 'lib/router/getQueryParamString';
interface MultichainProviderProps {
children: React.ReactNode;
chainSlug?: string;
}
export interface TMultichainContext {
chain: ChainConfig;
}
export const MultichainContext = React.createContext<TMultichainContext | null>(null);
export function MultichainProvider({ children, chainSlug: chainSlugProp }: MultichainProviderProps) {
const router = useRouter();
const chainSlugQueryParam = router.pathname.includes('chain-slug') ? getQueryParamString(router.query['chain-slug']) : undefined;
const [ chainSlug, setChainSlug ] = React.useState<string | undefined>(chainSlugProp ?? chainSlugQueryParam);
React.useEffect(() => {
if (chainSlugProp) {
setChainSlug(chainSlugProp);
}
}, [ chainSlugProp ]);
const chain = React.useMemo(() => {
const config = multichainConfig();
if (!config) {
return;
}
if (!chainSlug) {
return;
}
return config.chains.find((chain) => chain.slug === chainSlug);
}, [ chainSlug ]);
const value = React.useMemo(() => {
if (!chain) {
return null;
}
return {
chain,
};
}, [ chain ]);
return (
<MultichainContext.Provider value={ value }>
{ children }
</MultichainContext.Provider>
);
}
export function useMultichainContext(disabled: boolean = !multichainConfig) {
const context = React.useContext(MultichainContext);
if (context === undefined || disabled) {
return null;
}
return context;
}
......@@ -76,6 +76,8 @@ export default function useTimeAgoIncrement(ts: string | number | null, isEnable
timeouts.push(endTimeoutId);
};
setValue(dayjs(ts).fromNow());
isEnabled && startIncrement();
!isEnabled && setValue(dayjs(ts).fromNow());
......
......@@ -63,6 +63,10 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/pools': 'Root page',
'/pools/[hash]': 'Regular page',
'/interop-messages': 'Root page',
'/chain/[chain-slug]/accounts/label/[slug]': 'Root page',
'/chain/[chain-slug]/address/[hash]': 'Regular page',
'/chain/[chain-slug]/block/[height_or_hash]': 'Regular page',
'/chain/[chain-slug]/tx/[hash]': 'Regular page',
'/operations': 'Root page',
'/operation/[id]': 'Regular page',
......
......@@ -66,6 +66,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/pools': DEFAULT_TEMPLATE,
'/pools/[hash]': DEFAULT_TEMPLATE,
'/interop-messages': DEFAULT_TEMPLATE,
'/chain/[chain-slug]/accounts/label/[slug]': DEFAULT_TEMPLATE,
'/chain/[chain-slug]/address/[hash]': DEFAULT_TEMPLATE,
'/chain/[chain-slug]/block/[height_or_hash]': DEFAULT_TEMPLATE,
'/chain/[chain-slug]/tx/[hash]': DEFAULT_TEMPLATE,
'/operations': DEFAULT_TEMPLATE,
'/operation/[id]': DEFAULT_TEMPLATE,
......
......@@ -63,6 +63,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/pools': '%network_name% DEX pools',
'/pools/[hash]': '%network_name% pool details',
'/interop-messages': '%network_name% interop messages',
'/chain/[chain-slug]/accounts/label/[slug]': '%network_name% addresses search by label',
'/chain/[chain-slug]/address/[hash]': '%network_name% address details for %hash%',
'/chain/[chain-slug]/block/[height_or_hash]': '%network_name% block %height_or_hash% details',
'/chain/[chain-slug]/tx/[hash]': '%network_name% transaction %hash% details',
'/operations': '%network_name% operations',
'/operation/[id]': '%network_name% operation %id%',
......
......@@ -61,6 +61,10 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/pools': 'DEX pools',
'/pools/[hash]': 'Pool details',
'/interop-messages': 'Interop messages',
'/chain/[chain-slug]/accounts/label/[slug]': 'Chain addresses search by label',
'/chain/[chain-slug]/address/[hash]': 'Chain address details',
'/chain/[chain-slug]/block/[height_or_hash]': 'Chain block details',
'/chain/[chain-slug]/tx/[hash]': 'Chain transaction details',
'/operations': 'Operations',
'/operation/[id]': 'Operation details',
......
// https://hexdocs.pm/phoenix/js/
import type { SocketConnectOption } from 'phoenix';
import type { Channel, SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react';
export const SocketContext = React.createContext<Socket | null>(null);
type ChannelRegistry = Record<string, { channel: Channel; subscribers: number }>;
export const SocketContext = React.createContext<{
socket: Socket | null;
channelRegistry: React.MutableRefObject<ChannelRegistry>;
} | null>(null);
interface SocketProviderProps {
children: React.ReactNode;
......@@ -13,6 +18,7 @@ interface SocketProviderProps {
export function SocketProvider({ children, options, url }: SocketProviderProps) {
const [ socket, setSocket ] = useState<Socket | null>(null);
const channelRegistry = React.useRef<ChannelRegistry>({});
useEffect(() => {
if (!url) {
......@@ -29,8 +35,13 @@ export function SocketProvider({ children, options, url }: SocketProviderProps)
};
}, [ options, url ]);
const value = React.useMemo(() => ({
socket,
channelRegistry,
}), [ socket, channelRegistry ]);
return (
<SocketContext.Provider value={ socket }>
<SocketContext.Provider value={ value }>
{ children }
</SocketContext.Provider>
);
......
import type { Channel } from 'phoenix';
import type * as multichain from '@blockscout/multichain-aggregator-types';
import type { AddressCoinBalanceHistoryItem, AddressTokensBalancesSocketMessage } from 'types/api/address';
import type { NewArbitrumBatchSocketResponse } from 'types/api/arbitrumL2';
import type { NewBlockSocketResponse } from 'types/api/block';
......@@ -11,11 +12,13 @@ import type { Transaction } from 'types/api/transaction';
import type { NewZkEvmBatchSocketResponse } from 'types/api/zkEvmL2';
export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.NewBlockMultichain |
SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx |
SocketMessage.NewInteropMessage |
SocketMessage.NewPendingTx |
SocketMessage.NewOptimisticDeposits |
SocketMessage.NewArbitrumDeposits |
......@@ -49,11 +52,13 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type NewBlockMultichain = SocketMessageParamsGeneric<'new_blocks', Array<{ block_number: number; chain_id: number }>>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', { finished: boolean; ratio: string }>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'index_status', { finished: boolean; ratio: string }>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewInteropMessage = SocketMessageParamsGeneric<'new_messages', Array<multichain.InteropMessage>>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type NewOptimisticDeposits = SocketMessageParamsGeneric<'new_optimism_deposits', { deposits: number }>;
export type NewArbitrumDeposits = SocketMessageParamsGeneric<'new_messages_to_rollup_amount', { new_messages_to_rollup_amount: number }>;
......
......@@ -3,8 +3,6 @@ import { useEffect, useRef, useState } from 'react';
import { useSocket } from './context';
const CHANNEL_REGISTRY: Record<string, { channel: Channel; subscribers: number }> = {};
interface Params {
topic: string | undefined;
params?: object;
......@@ -15,7 +13,7 @@ interface Params {
}
export default function useSocketChannel({ topic, params, isDisabled, onJoin, onSocketClose, onSocketError }: Params) {
const socket = useSocket();
const { socket, channelRegistry } = useSocket() || {};
const [ channel, setChannel ] = useState<Channel>();
const onCloseRef = useRef<string>(undefined);
const onErrorRef = useRef<string>(undefined);
......@@ -47,18 +45,18 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
}, [ channel, isDisabled ]);
useEffect(() => {
if (socket === null || isDisabled || !topic) {
if (!socket || isDisabled || !topic || !channelRegistry) {
return;
}
let ch: Channel;
if (CHANNEL_REGISTRY[topic]) {
ch = CHANNEL_REGISTRY[topic].channel;
CHANNEL_REGISTRY[topic].subscribers++;
if (channelRegistry.current[topic]) {
ch = channelRegistry.current[topic].channel;
channelRegistry.current[topic].subscribers++;
onJoinRef.current?.(ch, '');
} else {
ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = { channel: ch, subscribers: 1 };
channelRegistry.current[topic] = { channel: ch, subscribers: 1 };
ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
......@@ -68,18 +66,20 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
setChannel(ch);
const currentRegistry = channelRegistry.current;
return () => {
if (CHANNEL_REGISTRY[topic]) {
CHANNEL_REGISTRY[topic].subscribers > 0 && CHANNEL_REGISTRY[topic].subscribers--;
if (CHANNEL_REGISTRY[topic].subscribers === 0) {
if (currentRegistry[topic]) {
currentRegistry[topic].subscribers > 0 && currentRegistry[topic].subscribers--;
if (currentRegistry[topic].subscribers === 0) {
ch.leave();
delete CHANNEL_REGISTRY[topic];
delete currentRegistry[topic];
}
}
setChannel(undefined);
};
}, [ socket, topic, params, isDisabled, onSocketError ]);
}, [ socket, topic, params, isDisabled, onSocketError, channelRegistry ]);
return channel;
}
import { type Chain } from 'viem';
import config from 'configs/app';
import appConfig from 'configs/app';
import multichainConfig from 'configs/multichain';
export const currentChain: Chain = {
const getChainInfo = (config: typeof appConfig = appConfig) => {
return {
id: Number(config.chain.id),
name: config.chain.name ?? '',
nativeCurrency: {
......@@ -22,10 +24,13 @@ export const currentChain: Chain = {
},
},
testnet: config.chain.isTestnet,
};
};
export const currentChain: Chain | undefined = !appConfig.features.opSuperchain.isEnabled ? getChainInfo() : undefined;
export const parentChain: Chain | undefined = (() => {
const rollupFeature = config.features.rollup;
const rollupFeature = appConfig.features.rollup;
const parentChain = rollupFeature.isEnabled && rollupFeature.parentChain;
......@@ -55,3 +60,13 @@ export const parentChain: Chain | undefined = (() => {
testnet: parentChain.isTestnet,
};
})();
export const clusterChains: Array<Chain> | undefined = (() => {
const config = multichainConfig();
if (!config) {
return;
}
return config.chains.map(({ config }) => getChainInfo(config)).filter(Boolean);
})();
......@@ -3,7 +3,8 @@ import { createPublicClient, http } from 'viem';
import { currentChain } from './chains';
export const publicClient = (() => {
if (currentChain.rpcUrls.default.http.filter(Boolean).length === 0) {
// TODO @tom2drum public clients for multichain (currently used only in degradation views)
if (currentChain?.rpcUrls.default.http.filter(Boolean).length === 0) {
return;
}
......
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import type { AppKitNetwork } from '@reown/appkit/networks';
import type { Chain } from 'viem';
import type { Chain, Transport } from 'viem';
import { fallback, http } from 'viem';
import { createConfig } from 'wagmi';
import config from 'configs/app';
import { currentChain, parentChain } from 'lib/web3/chains';
import appConfig from 'configs/app';
import multichainConfig from 'configs/multichain';
import { currentChain, parentChain, clusterChains } from 'lib/web3/chains';
const feature = config.features.blockchainInteraction;
const feature = appConfig.features.blockchainInteraction;
const chains = [ currentChain, parentChain ].filter(Boolean);
const chains = [ currentChain, parentChain, ...(clusterChains ?? []) ].filter(Boolean);
const getChainTransportFromConfig = (config: typeof appConfig, readOnly?: boolean): Record<string, Transport> => {
if (!config.chain.id) {
return {};
}
return {
[config.chain.id]: fallback(
config.chain.rpcUrls
.concat(readOnly ? `${ config.apis.general.endpoint }/api/eth-rpc` : '')
.filter(Boolean)
.map((url) => http(url, { batch: { wait: 100 } })),
),
};
};
const reduceClusterChainsToTransportConfig = (readOnly: boolean): Record<string, Transport> => {
const config = multichainConfig();
if (!config) {
return {};
}
return config.chains
.map(({ config }) => getChainTransportFromConfig(config, readOnly))
.reduce((result, item) => {
Object.entries(item).map(([ id, transport ]) => {
result[id] = transport;
});
return result;
}, {} as Record<string, Transport>);
};
const wagmi = (() => {
......@@ -17,12 +50,9 @@ const wagmi = (() => {
const wagmiConfig = createConfig({
chains: chains as [Chain, ...Array<Chain>],
transports: {
[currentChain.id]: fallback(
config.chain.rpcUrls
.map((url) => http(url))
.concat(http(`${ config.apis.general.endpoint }/api/eth-rpc`)),
),
...getChainTransportFromConfig(appConfig, true),
...(parentChain ? { [parentChain.id]: http(parentChain.rpcUrls.default.http[0]) } : {}),
...reduceClusterChainsToTransportConfig(true),
},
ssr: true,
batch: { multicall: { wait: 100 } },
......@@ -35,8 +65,9 @@ const wagmi = (() => {
networks: chains as Array<AppKitNetwork>,
multiInjectedProviderDiscovery: true,
transports: {
[currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))),
...getChainTransportFromConfig(appConfig, false),
...(parentChain ? { [parentChain.id]: http() } : {}),
...reduceClusterChainsToTransportConfig(false),
},
projectId: feature.walletConnect.projectId,
ssr: true,
......
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import generateCspPolicy from 'nextjs/csp/generateCspPolicy';
import * as csp from 'nextjs/csp/index';
import * as middlewares from 'nextjs/middlewares/index';
const cspPolicy = generateCspPolicy();
export function middleware(req: NextRequest) {
export async function middleware(req: NextRequest) {
const isPageRequest = req.headers.get('accept')?.includes('text/html');
const start = Date.now();
......@@ -27,7 +25,9 @@ export function middleware(req: NextRequest) {
const end = Date.now();
res.headers.append('Content-Security-Policy', cspPolicy);
const cspHeader = await csp.get();
res.headers.append('Content-Security-Policy', cspHeader);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
res.headers.append('Docker-ID', process.env.HOSTNAME || '');
......
......@@ -15,6 +15,7 @@ function generateCspPolicy() {
descriptors.marketplace(),
descriptors.mixpanel(),
descriptors.monaco(),
descriptors.multichain(),
descriptors.rollbar(),
descriptors.safe(),
descriptors.usernameApi(),
......
import appConfig from 'configs/app';
import * as multichainConfig from 'configs/multichain/config.edge';
import generateCspPolicy from './generateCspPolicy';
let cspPolicy: string | undefined = undefined;
export async function get() {
if (!cspPolicy) {
appConfig.features.opSuperchain.isEnabled && await multichainConfig.load();
cspPolicy = generateCspPolicy();
}
return cspPolicy;
}
......@@ -41,7 +41,7 @@ export function app(): CspDev.DirectiveDescriptor {
// APIs
...Object.values(config.apis).filter(Boolean).map((api) => api.endpoint),
config.apis.general.socketEndpoint,
...Object.values(config.apis).filter(Boolean).map((api) => api.socketEndpoint),
// chain RPC server
...config.chain.rpcUrls,
......
......@@ -10,6 +10,7 @@ export { helia } from './helia';
export { marketplace } from './marketplace';
export { mixpanel } from './mixpanel';
export { monaco } from './monaco';
export { multichain } from './multichain';
export { rollbar } from './rollbar';
export { safe } from './safe';
export { usernameApi } from './usernameApi';
......
import type CspDev from 'csp-dev';
import * as multichainConfig from 'configs/multichain/config.edge';
export function multichain(): CspDev.DirectiveDescriptor {
const value = multichainConfig.getValue();
if (!value) {
return {};
}
const apiEndpoints = value.chains.map((chain) => {
return [
...Object.values(chain.config.apis).filter(Boolean).map((api) => api.endpoint),
...Object.values(chain.config.apis).filter(Boolean).map((api) => api.socketEndpoint),
].filter(Boolean);
}).flat();
const rpcEndpoints = value.chains.map(({ config }) => config.chain.rpcUrls).flat();
return {
'connect-src': [
...apiEndpoints,
...rpcEndpoints,
],
};
}
......@@ -6,12 +6,14 @@ import type { RollupType } from 'types/client/rollup';
import type { Route } from 'nextjs-routes';
import config from 'configs/app';
const rollupFeature = config.features.rollup;
const adBannerFeature = config.features.adsBanner;
import multichainConfig from 'configs/multichain';
import isNeedProxy from 'lib/api/isNeedProxy';
import * as cookies from 'lib/cookies';
import type * as metadata from 'lib/metadata';
const rollupFeature = config.features.rollup;
const adBannerFeature = config.features.adsBanner;
export interface Props<Pathname extends Route['pathname'] = never> {
query: Route['query'];
cookies: string;
......@@ -411,6 +413,29 @@ export const interopMessages: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const opSuperchain: GetServerSideProps<Props> = async(context) => {
if (!config.features.opSuperchain.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const opSuperchainAccountsLabelSearch: GetServerSideProps<Props> = async(context) => {
const chainSlug = context.params?.['chain-slug'];
const chain = multichainConfig()?.chains.find((chain) => chain.slug === chainSlug);
if (!chain?.config.features.addressMetadata.isEnabled || !context.query.tagType) {
return {
notFound: true,
};
}
return opSuperchain(context);
};
export const pools: GetServerSideProps<Props> = async(context) => {
if (!config.features.pools.isEnabled) {
return {
......
......@@ -38,6 +38,10 @@ declare module "nextjs-routes" {
| DynamicRoute<"/block/countdown/[height]", { "height": string }>
| StaticRoute<"/block/countdown">
| StaticRoute<"/blocks">
| DynamicRoute<"/chain/[chain-slug]/accounts/label/[slug]", { "chain-slug": string; "slug": string }>
| DynamicRoute<"/chain/[chain-slug]/address/[hash]", { "chain-slug": string; "hash": string }>
| DynamicRoute<"/chain/[chain-slug]/block/[height_or_hash]", { "chain-slug": string; "height_or_hash": string }>
| DynamicRoute<"/chain/[chain-slug]/tx/[hash]", { "chain-slug": string; "hash": string }>
| StaticRoute<"/chakra">
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export">
......
import type { Route } from 'nextjs-routes';
import { route as nextjsRoute } from 'nextjs-routes';
import type { TMultichainContext } from 'lib/contexts/multichain';
export const route = (route: Route, multichainContext?: TMultichainContext | null) => {
return nextjsRoute(routeParams(route, multichainContext));
};
export const routeParams = (route: Route, multichainContext?: TMultichainContext | null): Route => {
if (multichainContext) {
const pathname = '/chain/[chain-slug]' + route.pathname;
return { ...route, pathname, query: { ...route.query, 'chain-slug': multichainContext.chain.slug } } as Route;
}
return route;
};
......@@ -13,7 +13,7 @@
"dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js",
"build": "next build",
"build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && yarn svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./",
"build:docker": "./tools/scripts/build.docker.sh",
"start": "next start",
"start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout-frontend:local",
"start:docker:preset": "./tools/scripts/docker.preset.sh",
......@@ -43,6 +43,7 @@
},
"dependencies": {
"@blockscout/bens-types": "1.4.1",
"@blockscout/multichain-aggregator-types": "1.6.0-alpha.0",
"@blockscout/points-types": "1.3.0-alpha.2",
"@blockscout/stats-types": "^2.9.0",
"@blockscout/tac-operation-lifecycle-types": "0.0.1-alpha.6",
......
......@@ -8,6 +8,7 @@ import React from 'react';
import type { NextPageWithLayout } from 'nextjs/types';
import config from 'configs/app';
import getSocketUrl from 'lib/api/getSocketUrl';
import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
......@@ -72,6 +73,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
);
})();
const socketUrl = !config.features.opSuperchain.isEnabled ? getSocketUrl() : undefined;
return (
<ChakraProvider>
<RollbarProvider config={ rollbarConfig }>
......@@ -84,7 +87,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<QueryClientProvider client={ queryClient }>
<GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ config.apis.general.socketEndpoint }${ config.apis.general.basePath ?? '' }/socket/v2` }>
<SocketProvider url={ socketUrl }>
<RewardsContextProvider>
<MarketplaceContextProvider>
<SettingsContextProvider>
......
......@@ -44,6 +44,12 @@ class MyDocument extends Document {
{ /* eslint-disable-next-line @next/next/no-sync-scripts */ }
<script src="/assets/envs.js"/>
{ config.features.opSuperchain.isEnabled && (
<>
{ /* eslint-disable-next-line @next/next/no-sync-scripts */ }
<script src="/assets/multichain/config.js"/>
</>
) }
{ /* FAVICON */ }
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png"/>
......
......@@ -10,6 +10,7 @@ import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressOpSuperchain from 'ui/optimismSuperchain/address/AddressOpSuperchain';
import Address from 'ui/pages/Address';
const pathname: Route['pathname'] = '/address/[hash]';
......@@ -17,7 +18,7 @@ const pathname: Route['pathname'] = '/address/[hash]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return (
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
<Address/>
{ config.features.opSuperchain.isEnabled ? <AddressOpSuperchain/> : <Address/> }
</PageNextJs>
);
};
......@@ -27,7 +28,7 @@ export default Page;
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) {
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse && !config.features.opSuperchain.isEnabled) {
const botInfo = detectBotRequest(ctx.req);
if (botInfo?.type === 'social_preview') {
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import { MultichainProvider } from 'lib/contexts/multichain';
const AccountsLabelSearch = dynamic(() => import('ui/pages/AccountsLabelSearch'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/chain/[chain-slug]/accounts/label/[slug]">
<MultichainProvider>
<AccountsLabelSearch/>
</MultichainProvider>
</PageNextJs>
);
};
export default Page;
export { opSuperchainAccountsLabelSearch as getServerSideProps } from 'nextjs/getServerSideProps';
import type { NextPage } from 'next';
import React from 'react';
import type { Route } from 'nextjs-routes';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import multichainConfig from 'configs/multichain';
import getSocketUrl from 'lib/api/getSocketUrl';
import { MultichainProvider } from 'lib/contexts/multichain';
import { SocketProvider } from 'lib/socket/context';
import Address from 'ui/pages/Address';
const pathname: Route['pathname'] = '/chain/[chain-slug]/address/[hash]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
const chainSlug = props.query?.['chain-slug'];
const chainData = multichainConfig()?.chains.find(chain => chain.slug === chainSlug);
return (
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
<SocketProvider url={ getSocketUrl(chainData?.config) }>
<MultichainProvider>
<Address/>
</MultichainProvider>
</SocketProvider>
</PageNextJs>
);
};
export default Page;
export { opSuperchain as getServerSideProps } from 'nextjs/getServerSideProps';
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import { MultichainProvider } from 'lib/contexts/multichain';
const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/chain/[chain-slug]/block/[height_or_hash]" query={ props.query }>
<MultichainProvider>
<Block/>
</MultichainProvider>
</PageNextJs>
);
};
export default Page;
export { opSuperchain as getServerSideProps } from 'nextjs/getServerSideProps';
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import { MultichainProvider } from 'lib/contexts/multichain';
const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/chain/[chain-slug]/tx/[hash]" query={ props.query }>
<MultichainProvider>
<Transaction/>
</MultichainProvider>
</PageNextJs>
);
};
export default Page;
export { opSuperchain as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -4,13 +4,15 @@ import type { NextPageWithLayout } from 'nextjs/types';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
import HomeOpSuperchain from 'ui/optimismSuperchain/home/HomeOpSuperchain';
import Home from 'ui/pages/Home';
import LayoutHome from 'ui/shared/layout/LayoutHome';
const Page: NextPageWithLayout = () => {
return (
<PageNextJs pathname="/">
<Home/>
{ config.features.opSuperchain.isEnabled ? <HomeOpSuperchain/> : <Home/> }
</PageNextJs>
);
};
......
......@@ -47,7 +47,7 @@ const defaultMarketplaceContext = {
setIsAutoConnectDisabled: () => {},
};
const wagmiConfig = createConfig({
const wagmiConfig = currentChain ? createConfig({
chains: [ currentChain ],
connectors: [
mock({
......@@ -59,7 +59,7 @@ const wagmiConfig = createConfig({
transports: {
[currentChain.id]: http(),
},
});
}) : undefined;
const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketplaceContext = defaultMarketplaceContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
......@@ -79,7 +79,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp
<MarketplaceContext.Provider value={ marketplaceContext }>
<SettingsContextProvider>
<GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }>
<WagmiProvider config={ wagmiConfig! }>
<RewardsContextProvider>
{ children }
</RewardsContextProvider>
......
import config from 'configs/app';
import * as multichainConfig from 'configs/multichain/config.nodejs';
(async() => {
config.features.opSuperchain.isEnabled && await multichainConfig.load();
})();
import * as multichain from '@blockscout/multichain-aggregator-types';
import { ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx';
export const INTEROP_MESSAGE: multichain.InteropMessage = {
sender: {
hash: ADDRESS_HASH,
},
target: {
hash: ADDRESS_HASH,
},
nonce: 4261,
init_chain_id: '420120000',
init_transaction_hash: TX_HASH,
timestamp: '2025-06-03T10:43:58.000Z',
relay_chain_id: '420120001',
relay_transaction_hash: TX_HASH,
payload: '0x4f0edcc90000000000000000000000004',
message_type: 'coin_transfer',
method: 'sendERC20',
status: multichain.InteropMessage_Status.PENDING,
};
#!/bin/bash
# remove previous assets
rm -rf ./public/assets/configs
rm -rf ./public/assets/multichain
rm -rf ./public/assets/envs.js
docker build --progress=plain --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./
\ No newline at end of file
......@@ -14,11 +14,23 @@ if [ ! -f "$config_file" ]; then
exit 1
fi
# remove previous assets
rm -rf ./public/assets/configs
rm -rf ./public/assets/multichain
rm -rf ./public/assets/envs.js
# download assets for the running instance
dotenv \
-e $config_file \
-- bash -c './deploy/scripts/download_assets.sh ./public/assets/configs'
# generate multichain config (adjust condition accordingly)
if [[ "$preset_name" == "optimism_superchain" ]]; then
dotenv \
-e $config_file \
-- bash -c 'cd deploy/tools/multichain-config-generator && yarn install --silent && yarn build && yarn generate'
fi
source ./deploy/scripts/build_sprite.sh
echo ""
......
#!/bin/bash
# remove previous assets
rm -rf ./public/assets/configs
rm -rf ./public/assets/multichain
rm -rf ./public/assets/envs.js
# download assets for the running instance
dotenv \
-e .env.development.local \
......
......@@ -18,5 +18,5 @@
"types": ["node", "jest"],
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.node.ts", "**/*.tsx", "**/*.pw.tsx", "decs.d.ts", "global.d.ts"],
"exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator", "./deploy/tools/favicon-generator", "./toolkit/package"],
"exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator", "./deploy/tools/favicon-generator", "./deploy/tools/multichain-config-generator", "./toolkit/package"],
}
import type config from 'configs/app';
export interface ChainConfig {
slug: string;
config: typeof config;
}
export interface MultichainConfig {
chains: Array<ChainConfig>;
}
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import type { TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting';
import { SORT_OPTIONS } from 'ui/txs/useTxsSort';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
import useAddressTxsQuery from './useAddressTxsQuery';
type Props = {
shouldRender?: boolean;
......@@ -32,37 +20,14 @@ type Props = {
const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
const [ sort, setSort ] = React.useState<TransactionsSortingValue>(getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default');
const isMobile = useIsMobile();
const currentAddress = getQueryParamString(router.query.hash);
const initialFilterValue = getFilterValue(router.query.filter);
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(initialFilterValue);
const addressTxsQuery = useQueryWithPages({
resourceName: 'general:address_txs',
pathParams: { hash: currentAddress },
filters: { filter: filterValue },
sorting: getSortParamsFromValue<TransactionsSortingValue, TransactionsSortingField, TransactionsSorting['order']>(sort),
options: {
const { query, filterValue, initialFilterValue, onFilterChange, sort, setSort } = useAddressTxsQuery({
addressHash: currentAddress,
enabled: isQueryEnabled,
placeholderData: generateListStub<'general:address_txs'>(TX, 50, { next_page_params: {
block_number: 9005713,
index: 5,
items_count: 50,
} }),
},
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val);
setFilterValue(newVal);
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
if (!isMounted || !shouldRender) {
return null;
}
......@@ -70,9 +35,9 @@ const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const filter = (
<AddressTxsFilter
initialValue={ initialFilterValue }
onFilterChange={ handleFilterChange }
onFilterChange={ onFilterChange }
hasActiveFilter={ Boolean(filterValue) }
isLoading={ addressTxsQuery.pagination.isLoading }
isLoading={ query.pagination.isLoading }
/>
);
......@@ -81,7 +46,7 @@ const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
address={ currentAddress }
params={{ type: 'transactions', filterType: 'address', filterValue }}
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
isLoading={ query.pagination.isLoading }
/>
);
......@@ -91,13 +56,13 @@ const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
<ActionBar>
{ filter }
{ currentAddress && csvExportLink }
<Pagination { ...addressTxsQuery.pagination } ml={ 8 }/>
<Pagination { ...query.pagination } ml={ 8 }/>
</ActionBar>
) }
<TxsWithAPISorting
filter={ filter }
filterValue={ filterValue }
query={ addressTxsQuery }
query={ query }
currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined }
enableTimeIncrement
socketType="address_txs"
......
......@@ -5,6 +5,7 @@ import { usePublicClient } from 'wagmi';
import type { FormSubmitResult, MethodCallStrategy, SmartContractMethod } from './types';
import config from 'configs/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import useAccount from 'lib/web3/useAccount';
import { getNativeCoinValue } from './utils';
......@@ -17,7 +18,9 @@ interface Params {
}
export default function useCallMethodPublicClient(): (params: Params) => Promise<FormSubmitResult> {
const publicClient = usePublicClient({ chainId: Number(config.chain.id) });
const multichainContext = useMultichainContext();
const chainId = Number((multichainContext?.chain.config ?? config).chain.id);
const publicClient = usePublicClient({ chainId });
const { address: account } = useAccount();
return React.useCallback(async({ args, item, addressHash, strategy }) => {
......
......@@ -5,6 +5,7 @@ import { useAccount, useWalletClient, useSwitchChain } from 'wagmi';
import type { FormSubmitResult, SmartContractMethod } from './types';
import config from 'configs/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import { getNativeCoinValue } from './utils';
......@@ -16,7 +17,10 @@ interface Params {
}
export default function useCallMethodWalletClient(): (params: Params) => Promise<FormSubmitResult> {
const { data: walletClient } = useWalletClient();
const multichainContext = useMultichainContext();
const chainConfig = (multichainContext?.chain.config ?? config).chain;
const { data: walletClient } = useWalletClient({ chainId: Number(chainConfig.id) });
const { isConnected, chainId, address: account } = useAccount();
const { switchChainAsync } = useSwitchChain();
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
......@@ -30,8 +34,8 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
throw new Error('Wallet Client is not defined');
}
if (chainId && String(chainId) !== config.chain.id) {
await switchChainAsync({ chainId: Number(config.chain.id) });
if (chainId && String(chainId) !== chainConfig.id) {
await switchChainAsync({ chainId: Number(chainConfig.id) });
}
const address = getAddress(addressHash);
......@@ -81,5 +85,5 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
}
return { source: 'wallet_client', data: { hash } };
}, [ chainId, isConnected, switchChainAsync, walletClient, account, trackTransaction, trackTransactionConfirm ]);
}, [ chainId, chainConfig, isConnected, switchChainAsync, walletClient, account, trackTransaction, trackTransactionConfirm ]);
}
......@@ -4,9 +4,10 @@ import React from 'react';
import type { AddressCounters } from 'types/api/address';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
import type { ResourceError } from 'lib/api/resources';
import { useMultichainContext } from 'lib/contexts/multichain';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
......@@ -25,6 +26,7 @@ const PROP_TO_TAB = {
};
const AddressCounterItem = ({ prop, query, address, isAddressQueryLoading, isDegradedData }: Props) => {
const multichainContext = useMultichainContext();
const handleClick = React.useCallback(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
......@@ -56,7 +58,7 @@ const AddressCounterItem = ({ prop, query, address, isAddressQueryLoading, isDeg
return (
<Link
href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) }
href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }, multichainContext) }
scroll={ false }
onClick={ handleClick }
>
......
......@@ -6,9 +6,10 @@ import React from 'react';
import type { Address } from 'types/api/address';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
import { getResourceKey } from 'lib/api/useApiQuery';
import { useMultichainContext } from 'lib/contexts/multichain';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -26,14 +27,19 @@ const TokenSelect = () => {
const router = useRouter();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const multichainContext = useMultichainContext();
const addressHash = getQueryParamString(router.query.hash);
const addressResourceKey = getResourceKey('general:address', { pathParams: { hash: addressHash } });
const addressResourceKey = getResourceKey('general:address', { pathParams: { hash: addressHash }, chainSlug: multichainContext?.chain?.slug });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isPending } = useFetchTokens({ hash: addressQueryData?.hash });
const tokensResourceKey = getResourceKey('general:address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensResourceKey = getResourceKey('general:address_tokens', {
pathParams: { hash: addressQueryData?.hash },
queryParams: { type: 'ERC-20' },
chainSlug: multichainContext?.chain?.slug,
});
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
const handleIconButtonClick = React.useCallback(() => {
......@@ -63,7 +69,7 @@ const TokenSelect = () => {
}
<Tooltip content="Show all tokens">
<Link
href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }) }
href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }, multichainContext) }
asChild
scroll={ false }
>
......
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import type { TransactionsSorting, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import { SORT_OPTIONS } from 'ui/txs/useTxsSort';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
interface Props {
addressHash: string;
enabled: boolean;
}
export default function useAddressTxsQuery({ addressHash, enabled }: Props) {
const router = useRouter();
const [ sort, setSort ] = React.useState<TransactionsSortingValue>(getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default');
const initialFilterValue = getFilterValue(router.query.filter);
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(initialFilterValue);
const query = useQueryWithPages({
resourceName: 'general:address_txs',
pathParams: { hash: addressHash },
filters: { filter: filterValue },
sorting: getSortParamsFromValue<TransactionsSortingValue, TransactionsSortingField, TransactionsSorting['order']>(sort),
options: {
enabled: enabled,
placeholderData: generateListStub<'general:address_txs'>(TX, 50, { next_page_params: {
block_number: 9005713,
index: 5,
items_count: 50,
} }),
},
});
const onFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val);
setFilterValue(newVal);
query.onFilterChange({ filter: newVal });
}, [ query ]);
return React.useMemo(() => ({
query,
filterValue,
initialFilterValue,
onFilterChange,
sort,
setSort,
}), [ query, filterValue, initialFilterValue, onFilterChange, sort ]);
}
......@@ -6,10 +6,11 @@ import React from 'react';
import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes';
import { route, routeParams } from 'nextjs/routes';
import config from 'configs/app';
import getBlockReward from 'lib/block/getBlockReward';
import { useMultichainContext } from 'lib/contexts/multichain';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import * as arbitrum from 'lib/rollups/arbitrum';
......@@ -53,6 +54,7 @@ const rollupFeature = config.features.rollup;
const BlockDetails = ({ query }: Props) => {
const router = useRouter();
const heightOrHash = getQueryParamString(router.query.height_or_hash);
const multichainContext = useMultichainContext();
const { data, isPlaceholderData } = query;
......@@ -64,8 +66,8 @@ const BlockDetails = ({ query }: Props) => {
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.height + increment);
router.push({ pathname: '/block/[height_or_hash]', query: { height_or_hash: nextId } }, undefined);
}, [ data, router ]);
router.push(routeParams({ pathname: '/block/[height_or_hash]', query: { height_or_hash: nextId } }, multichainContext), undefined);
}, [ data, multichainContext, router ]);
if (!data) {
return null;
......@@ -113,7 +115,7 @@ const BlockDetails = ({ query }: Props) => {
const txsNum = (() => {
const blockTxsNum = (
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'txs' } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'txs' } }, multichainContext) }>
{ data.transactions_count } txn{ data.transactions_count === 1 ? '' : 's' }
</Link>
);
......@@ -121,7 +123,7 @@ const BlockDetails = ({ query }: Props) => {
const blockBlobTxsNum = (config.features.dataAvailability.isEnabled && data.blob_transaction_count) ? (
<>
<span> including </span>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'blob_txs' } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'blob_txs' } }, multichainContext) }>
{ data.blob_transaction_count } blob txn{ data.blob_transaction_count === 1 ? '' : 's' }
</Link>
</>
......@@ -266,7 +268,7 @@ const BlockDetails = ({ query }: Props) => {
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<Skeleton loading={ isPlaceholderData }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'withdrawals' } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'withdrawals' } }, multichainContext) }>
{ data.withdrawals_count } withdrawal{ data.withdrawals_count === 1 ? '' : 's' }
</Link>
</Skeleton>
......@@ -666,7 +668,7 @@ const BlockDetails = ({ query }: Props) => {
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue flexWrap="nowrap">
<Link
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(data.height - 1) } }) }
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(data.height - 1) } }, multichainContext) }
overflow="hidden"
whiteSpace="nowrap"
>
......
......@@ -162,6 +162,8 @@ export default function useBlockTxsQuery({ heightOrHash, blockQuery, tab }: Para
pagination: emptyPagination,
onFilterChange: () => {},
onSortingChange: () => {},
chainValue: undefined,
onChainValueChange: () => {},
};
const query = isRpcQuery ? rpcQueryWithPages : apiQuery;
......
......@@ -129,6 +129,8 @@ export default function useBlockWithdrawalsQuery({ heightOrHash, blockQuery, tab
pagination: emptyPagination,
onFilterChange: () => {},
onSortingChange: () => {},
chainValue: undefined,
onChainValueChange: () => {},
};
const query = isRpcQuery ? rpcQueryWithPages : apiQuery;
......
import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress';
import getQueryParamString from 'lib/router/getQueryParamString';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import TextAd from 'ui/shared/ad/TextAd';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle';
import AddressOpSuperchainTxs, { ADDRESS_OP_SUPERCHAIN_TXS_TAB_IDS } from './AddressOpSuperchainTxs';
const AddressOpSuperchain = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const isLoading = false;
const addressQuery = {
data: {
hash: undefined,
},
};
const checkSummedHash = React.useMemo(() => addressQuery.data?.hash ?? getCheckedSummedAddress(hash), [ hash, addressQuery.data?.hash ]);
const tabs: Array<TabItemRegular> = React.useMemo(() => {
return [
{
id: 'index',
title: 'Details',
component: <div>Coming soon 🔜</div>,
},
{
id: 'txs',
title: 'Transactions',
component: <AddressOpSuperchainTxs/>,
subTabs: ADDRESS_OP_SUPERCHAIN_TXS_TAB_IDS,
},
];
}, []);
const titleSecondRow = (
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<AddressEntity
address={{
...addressQuery.data,
hash: checkSummedHash,
name: '',
ens_domain_name: '',
implementations: null,
}}
isLoading={ isLoading }
variant="subheading"
noLink
/>
<AddressQrCode hash={ checkSummedHash } isLoading={ isLoading }/>
</Flex>
);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title="Address details"
isLoading={ isLoading }
secondRow={ titleSecondRow }
/>
<RoutedTabs tabs={ tabs } isLoading={ isLoading }/>
</>
);
};
export default React.memo(AddressOpSuperchain);
import { HStack } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import multichainConfig from 'configs/multichain';
import getSocketUrl from 'lib/api/getSocketUrl';
import { MultichainProvider } from 'lib/contexts/multichain';
import getQueryParamString from 'lib/router/getQueryParamString';
import { SocketProvider } from 'lib/socket/context';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
// import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import AddressTxsFilter from 'ui/address/AddressTxsFilter';
import useAddressTxsQuery from 'ui/address/useAddressTxsQuery';
import ChainSelect from 'ui/shared/multichain/ChainSelect';
import Pagination from 'ui/shared/pagination/Pagination';
import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting';
export const ADDRESS_OP_SUPERCHAIN_TXS_TAB_IDS = [ 'cross_chain_txs', 'local_txs' ];
const TAB_LIST_PROPS = {
marginBottom: 0,
pt: 6,
pb: 3,
marginTop: -6,
};
const ACTION_BAR_HEIGHT_DESKTOP = 68;
const AddressOpSuperchainTxs = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const tab = getQueryParamString(router.query.tab);
const txsQueryLocal = useAddressTxsQuery({
addressHash: hash,
enabled: tab === 'local_txs',
});
const txsLocalFilter = tab === 'local_txs' ? (
<AddressTxsFilter
initialValue={ txsQueryLocal.filterValue }
onFilterChange={ txsQueryLocal.onFilterChange }
hasActiveFilter={ Boolean(txsQueryLocal.filterValue) }
isLoading={ txsQueryLocal.query.pagination.isLoading }
/>
) : null;
const rightSlot = tab === 'local_txs' ? (
<>
<HStack gap={ 2 }>
{ txsLocalFilter }
<ChainSelect
loading={ txsQueryLocal.query.pagination.isLoading }
value={ txsQueryLocal.query.chainValue }
onValueChange={ txsQueryLocal.query.onChainValueChange }
/>
</HStack>
<HStack gap={ 6 }>
{ /* <AddressCsvExportLink
address={ hash }
params={{ type: 'transactions', filterType: 'address', filterValue: txsQueryLocal.filterValue }}
ml="auto"
isLoading={ txsQueryLocal.query.pagination.isLoading }
/> */ }
<Pagination { ...txsQueryLocal.query.pagination } ml={ 8 }/>
</HStack>
</>
) : null;
const chainData = multichainConfig()?.chains.find(chain => chain.slug === txsQueryLocal.query.chainValue?.[0]);
const tabs: Array<TabItemRegular> = [
{
id: 'cross_chain_txs',
title: 'Cross-chain',
component: <div>Coming soon 🔜</div>,
},
{
id: 'local_txs',
title: 'Local',
component: (
<SocketProvider url={ getSocketUrl(chainData?.config) }>
<MultichainProvider chainSlug={ txsQueryLocal.query.chainValue?.[0] }>
<TxsWithAPISorting
filter={ txsLocalFilter }
filterValue={ txsQueryLocal.filterValue }
query={ txsQueryLocal.query }
currentAddress={ hash }
enableTimeIncrement
socketType="address_txs"
top={ ACTION_BAR_HEIGHT_DESKTOP }
sorting={ txsQueryLocal.sort }
setSort={ txsQueryLocal.setSort }
/>
</MultichainProvider>
</SocketProvider>
),
},
];
return (
<RoutedTabs
variant="secondary"
size="sm"
tabs={ tabs }
rightSlot={ rightSlot }
rightSlotProps={{ display: 'flex', justifyContent: 'space-between', ml: 8, widthAllocation: 'available' }}
listProps={ TAB_LIST_PROPS }
stickyEnabled
/>
);
};
export default React.memo(AddressOpSuperchainTxs);
import React from 'react';
import * as multichain from '@blockscout/multichain-aggregator-types';
import type { BadgeProps } from 'toolkit/chakra/badge';
import StatusTag from 'ui/shared/statusTag/StatusTag';
interface Props extends BadgeProps {
status: multichain.InteropMessage_Status;
}
const CrossChainTxStatusTag = ({ status: statusProp, ...rest }: Props) => {
const { status, text } = (() => {
switch (statusProp) {
case multichain.InteropMessage_Status.SUCCESS:
return { status: 'ok' as const, text: 'Relayed' };
case multichain.InteropMessage_Status.FAILED:
return { status: 'error' as const, text: 'Failed' };
case multichain.InteropMessage_Status.PENDING:
return { status: 'pending' as const, text: 'Sent' };
default:
return { status: undefined, text: undefined };
}
})();
if (!status || !text) {
return null;
}
return <StatusTag type={ status } text={ text } { ...rest }/>;
};
export default React.memo(CrossChainTxStatusTag);
import React from 'react';
import type * as multichain from '@blockscout/multichain-aggregator-types';
import type { TxsSocketType } from 'ui/txs/socket/types';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle';
import TxsSocketNotice from 'ui/txs/socket/TxsSocketNotice';
import CrossChainTxsTableItem from './CrossChainTxsTableItem';
interface Props {
items: Array<multichain.InteropMessage>;
isLoading: boolean;
socketType?: TxsSocketType;
}
const CrossChainTxsTable = ({ items, isLoading, socketType }: Props) => {
return (
<AddressHighlightProvider>
<TableRoot minW="1150px">
<TableHeaderSticky top={ 68 }>
<TableRow>
<TableColumnHeader width="52px"/>
<TableColumnHeader w="180px">
Message
<TimeFormatToggle/>
</TableColumnHeader>
<TableColumnHeader w="130px">Type</TableColumnHeader>
<TableColumnHeader w="130px">Method</TableColumnHeader>
<TableColumnHeader w="25%">Source tx</TableColumnHeader>
<TableColumnHeader w="25%">Destination tx</TableColumnHeader>
<TableColumnHeader w="25%">Sender</TableColumnHeader>
<TableColumnHeader w="32px"/>
<TableColumnHeader w="25%">Target</TableColumnHeader>
<TableColumnHeader w="130px">Value</TableColumnHeader>
</TableRow>
</TableHeaderSticky>
<TableBody>
{ socketType && <TxsSocketNotice type={ socketType } place="table" isLoading={ isLoading }/> }
{ items.map((item, index) => (
<CrossChainTxsTableItem
key={ String(item.nonce) + (isLoading ? index : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</TableBody>
</TableRoot>
</AddressHighlightProvider>
);
};
export default React.memo(CrossChainTxsTable);
import { Spinner, VStack } from '@chakra-ui/react';
import React from 'react';
import type * as multichain from '@blockscout/multichain-aggregator-types';
import type { TransactionType } from 'types/api/transaction';
import multichainConfig from 'configs/multichain';
import getCurrencyValue from 'lib/getCurrencyValue';
import { Badge } from 'toolkit/chakra/badge';
import { Link } from 'toolkit/chakra/link';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import CrossChainTxStatusTag from 'ui/optimismSuperchain/components/CrossChainTxStatusTag';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import AddressFromToIcon from 'ui/shared/address/AddressFromToIcon';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
import TxType from 'ui/txs/TxType';
interface Props {
item: multichain.InteropMessage;
isLoading: boolean;
animation?: string;
}
const CrossChainTxsTableItem = ({ item, isLoading, animation }: Props) => {
const sourceChain = React.useMemo(() => {
const config = multichainConfig();
return config?.chains.find((chain) => chain.config.chain.id === item.init_chain_id);
}, [ item ]);
const targetChain = React.useMemo(() => {
const config = multichainConfig();
return config?.chains.find((chain) => chain.config.chain.id === item.relay_chain_id);
}, [ item ]);
const value = getCurrencyValue({
value: item.transfer?.total?.value ?? '0',
decimals: '18',
});
return (
<TableRow animation={ animation }>
<TableCell pl={ 4 }>
<AdditionalInfoButton loading={ isLoading }/>
</TableCell>
<TableCell>
<VStack alignItems="start">
<Link fontWeight="700" loading={ isLoading }>{ item.nonce }</Link>
<TimeWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text.secondary"
/>
</VStack>
</TableCell>
<TableCell>
<VStack alignItems="start">
<TxType types={ [ item.message_type as TransactionType ] } isLoading={ isLoading }/>
<CrossChainTxStatusTag status={ item.status } loading={ isLoading }/>
</VStack>
</TableCell>
<TableCell>
<Badge colorPalette="gray" loading={ isLoading } truncated>{ item.method }</Badge>
</TableCell>
<TableCell>
{ item.init_transaction_hash ? (
<TxEntity
hash={ item.init_transaction_hash }
isLoading={ isLoading }
truncation="constant"
chain={ sourceChain }
/>
) :
<Spinner size="md"/>
}
</TableCell>
<TableCell>
{ item.relay_transaction_hash ? (
<TxEntity
hash={ item.relay_transaction_hash }
isLoading={ isLoading }
truncation="constant"
chain={ targetChain }
/>
) :
<Spinner size="md"/>
}
</TableCell>
<TableCell>
{ item.sender ? (
<AddressEntity
address={{ hash: item.sender.hash }}
isLoading={ isLoading }
chain={ sourceChain }
/>
) : '-' }
</TableCell>
<TableCell>
<AddressFromToIcon isLoading={ isLoading } type="unspecified"/>
</TableCell>
<TableCell>
{ item.target ? (
<AddressEntity
address={{ hash: item.target.hash }}
isLoading={ isLoading }
chain={ targetChain }
/>
) : '-' }
</TableCell>
<TableCell>
{ value.valueStr }
</TableCell>
</TableRow>
);
};
export default React.memo(CrossChainTxsTableItem);
import { Box, HStack } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import { route } from 'nextjs-routes';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { Link } from 'toolkit/chakra/link';
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
interface Props {
slug: string;
}
const ChainLatestBlockInfo = ({ slug }: Props) => {
const queryClient = useQueryClient();
const blocksQuery = useApiQuery('general:homepage_blocks', {
chainSlug: slug,
queryOptions: {
placeholderData: [ BLOCK ],
},
});
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
const queryKey = getResourceKey('general:homepage_blocks', { chainSlug: slug });
queryClient.setQueryData(queryKey, () => {
return [ payload.block ];
});
}, [ queryClient, slug ]);
const channel = useSocketChannel({
topic: 'blocks:new_block',
isDisabled: blocksQuery.isPlaceholderData || blocksQuery.isError,
});
useSocketMessage({
channel,
event: 'new_block',
handler: handleNewBlockMessage,
});
if (!blocksQuery.data?.[0]) {
return null;
}
return (
<HStack gap={ 2 }>
<Box color="text.secondary">Latest block</Box>
<Link
loading={ blocksQuery.isPlaceholderData }
href={ route({
pathname: '/chain/[chain-slug]/block/[height_or_hash]',
query: {
'chain-slug': slug,
height_or_hash: blocksQuery.data[0].height.toString(),
},
}) }
>
{ blocksQuery.data[0].height }
</Link>
<TimeWithTooltip
timestamp={ blocksQuery.data[0].timestamp }
enableIncrement={ !blocksQuery.isPlaceholderData }
isLoading={ blocksQuery.isPlaceholderData }
color="text.secondary"
flexShrink={ 0 }
timeFormat="relative"
/>
</HStack>
);
};
export default React.memo(ChainLatestBlockInfo);
import { Box, HStack, VStack } from '@chakra-ui/react';
import React from 'react';
import type { ChainConfig } from 'types/multichain';
import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import { Heading } from 'toolkit/chakra/heading';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg';
import ChainLatestBlockInfo from './ChainLatestBlockInfo';
interface Props {
data: ChainConfig;
}
const ChainWidget = ({ data }: Props) => {
const statsQuery = useApiQuery('general:stats', {
chainSlug: data.slug,
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
return (
<Box
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
borderRadius="xl"
border="1px solid"
borderColor={{ _light: 'gray.200', _dark: 'gray.900' }}
p={ 4 }
flexBasis="50%"
textStyle="sm"
>
<HStack justifyContent="space-between">
<Image src={ data.config.UI.navigation.icon.default } alt={ data.config.chain.name } boxSize="30px" borderRadius="full"/>
<Link
href={ data.config.app.baseUrl }
target="_blank"
p={ 1 }
color="gray.500"
_hover={{
color: 'link.primary.hover',
}}
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
borderRadius="base"
>
<IconSvg name="globe" boxSize={ 6 }/>
</Link>
</HStack>
<Heading mt={ 3 } level="3">{ data.config.chain.name }</Heading>
<VStack gap={ 2 } mt={ 3 } alignItems="flex-start">
<HStack gap={ 2 }>
<Box color="text.secondary">Chain ID</Box>
<Box>{ data.config.chain.id }</Box>
<CopyToClipboard text={ String(data.config.chain.id) } ml={ 0 }/>
</HStack>
<ChainLatestBlockInfo slug={ data.slug }/>
{ statsQuery.data && statsQuery.data.gas_prices && data.config.features.gasTracker.isEnabled && (
<HStack gap={ 2 }>
<Box color="text.secondary">Gas price</Box>
<Skeleton loading={ statsQuery.isPlaceholderData }>
<GasPrice data={ statsQuery.data.gas_prices.average }/>
</Skeleton>
</HStack>
) }
</VStack>
</Box>
);
};
export default React.memo(ChainWidget);
import { Box, HStack } from '@chakra-ui/react';
import React from 'react';
import multichainConfig from 'configs/multichain';
import getSocketUrl from 'lib/api/getSocketUrl';
import { MultichainProvider } from 'lib/contexts/multichain';
import { SocketProvider } from 'lib/socket/context';
import HeroBanner from 'ui/home/HeroBanner';
import ChainWidget from './ChainWidget';
import LatestTxs from './LatestTxs';
const HomeOpSuperchain = () => {
return (
<Box as="main">
<HeroBanner/>
<HStack mt={ 3 } gap={ 6 }>
{ multichainConfig()?.chains.map(chain => {
return (
<MultichainProvider key={ chain.slug } chainSlug={ chain.slug }>
<SocketProvider url={ getSocketUrl(chain.config) }>
<ChainWidget data={ chain }/>
</SocketProvider>
</MultichainProvider>
);
}) }
</HStack>
<LatestTxs/>
</Box>
);
};
export default React.memo(HomeOpSuperchain);
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import multichainConfig from 'configs/multichain';
import getQueryParamString from 'lib/router/getQueryParamString';
import { Heading } from 'toolkit/chakra/heading';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import ChainSelect from 'ui/shared/multichain/ChainSelect';
import LatestTxsCrossChain from './LatestTxsCrossChain';
import LatestTxsLocal from './LatestTxsLocal';
const LatestTxs = () => {
const router = useRouter();
const tab = getQueryParamString(router.query.tab);
const [ chainValue, setChainValue ] = React.useState<Array<string> | undefined>(
[ getQueryParamString(router.query['chain-slug']) ?? multichainConfig()?.chains[0]?.slug ].filter(Boolean),
);
const handleChainValueChange = React.useCallback(({ value }: { value: Array<string> }) => {
setChainValue(value);
router.push({
query: {
...router.query,
'chain-slug': value[0],
},
}, undefined, { shallow: true });
}, [ router ]);
const tabs = [
{
id: 'cross_chain_txs',
title: 'Cross-chain',
component: <LatestTxsCrossChain/>,
},
{
id: 'local_txs',
title: 'Local',
component: chainValue ? <LatestTxsLocal key={ chainValue[0] } chainSlug={ chainValue[0] }/> : null,
},
];
const rightSlot = tab === 'local_txs' ? (
<ChainSelect
loading={ false }
value={ chainValue }
onValueChange={ handleChainValueChange }
w="fit-content"
/>
) : null;
return (
<Box as="section" mt={ 8 }>
<Heading level="3" mb={ 6 }>Latest transactions</Heading>
<RoutedTabs
tabs={ tabs }
rightSlot={ rightSlot }
/>
</Box>
);
};
export default React.memo(LatestTxs);
import { Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { SocketProvider } from 'lib/socket/context';
import { INTEROP_MESSAGE } from 'stubs/optimismSuperchain';
import { generateListStub } from 'stubs/utils';
import CrossChainTxsTable from 'ui/optimismSuperchain/crossChainTxs/CrossChainTxsTable';
import DataListDisplay from 'ui/shared/DataListDisplay';
const socketUrl = config.apis.multichain?.socketEndpoint ? `${ config.apis.multichain.socketEndpoint }/socket` : undefined;
const LatestTxsCrossChain = () => {
const { data, isError, isPlaceholderData } = useApiQuery('multichain:interop_messages', {
queryOptions: {
placeholderData: generateListStub<'multichain:interop_messages'>(INTEROP_MESSAGE, 5, { next_page_params: undefined }),
select: (data) => ({ ...data, items: data.items.slice(0, 5) }),
},
});
const content = data?.items ? (
<>
<Box hideFrom="lg">
Coming soon 🔜
</Box>
<Box hideBelow="lg">
<CrossChainTxsTable
isLoading={ isPlaceholderData }
items={ data.items }
socketType="txs_home_cross_chain"
/>
</Box>
</>
) : null;
return (
<SocketProvider url={ socketUrl }>
<DataListDisplay
itemsNum={ data?.items?.length }
isError={ isError }
emptyText="There are no cross-chain transactions."
>
{ content }
</DataListDisplay>
</SocketProvider>
);
};
export default React.memo(LatestTxsCrossChain);
import { noop } from 'es-toolkit';
import React from 'react';
import type { PaginationParams } from 'ui/shared/pagination/types';
import multichainConfig from 'configs/multichain';
import getSocketUrl from 'lib/api/getSocketUrl';
import useApiQuery from 'lib/api/useApiQuery';
import { MultichainProvider } from 'lib/contexts/multichain';
import { SocketProvider } from 'lib/socket/context';
import { TX } from 'stubs/tx';
import TxsContent from 'ui/txs/TxsContent';
const PAGINATION_PARAMS: PaginationParams = {
page: 1,
isVisible: false,
isLoading: false,
hasPages: false,
hasNextPage: false,
canGoBackwards: false,
onNextPageClick: () => {},
onPrevPageClick: () => {},
resetPage: () => {},
};
interface Props {
chainSlug: string;
}
const LatestTxsLocal = ({ chainSlug }: Props) => {
const query = useApiQuery('general:homepage_txs', {
chainSlug,
queryOptions: {
placeholderData: Array(5).fill(TX),
select: (data) => data.slice(0, 5),
},
});
const chainData = multichainConfig()?.chains.find(chain => chain.slug === chainSlug);
return (
<MultichainProvider chainSlug={ chainSlug }>
<SocketProvider url={ getSocketUrl(chainData?.config) }>
<TxsContent
items={ query.data || [] }
isPlaceholderData={ query.isPlaceholderData }
isError={ query.isError }
pagination={ PAGINATION_PARAMS }
setSorting={ noop }
sort="default"
socketType="txs_home"
/>
</SocketProvider>
</MultichainProvider>
);
};
export default React.memo(LatestTxsLocal);
......@@ -11,6 +11,7 @@ import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery
import useAddressMetadataInitUpdate from 'lib/address/useAddressMetadataInitUpdate';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
......@@ -69,6 +70,7 @@ const xScoreFeature = config.features.xStarScore;
const AddressPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const { chain } = useMultichainContext() || {};
const hash = getQueryParamString(router.query.hash);
......@@ -437,11 +439,13 @@ const AddressPageContent = () => {
</Flex>
);
const chainText = chain ? ` on ${ chain.config.chain.name }` : '';
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `${ addressQuery.data?.is_contract && addressQuery.data?.proxy_type !== 'eip7702' ? 'Contract' : 'Address' } details` }
title={ `${ addressQuery.data?.is_contract && addressQuery.data?.proxy_type !== 'eip7702' ? 'Contract' : 'Address' } details${ chainText }` }
backLink={ backLink }
contentAfter={ titleContentAfter }
secondRow={ titleSecondRow }
......
......@@ -8,6 +8,7 @@ import type { PaginationParams } from 'ui/shared/pagination/types';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -46,6 +47,7 @@ const BlockPageContent = () => {
const appProps = useAppContext();
const heightOrHash = getQueryParamString(router.query.height_or_hash);
const tab = getQueryParamString(router.query.tab);
const { chain } = useMultichainContext() || {};
const blockQuery = useBlockQuery({ heightOrHash });
const blockTxsQuery = useBlockTxsQuery({ heightOrHash, blockQuery, tab });
......@@ -145,15 +147,17 @@ const BlockPageContent = () => {
}
const title = (() => {
const chainText = chain ? ` on ${ chain.config.chain.name }` : '';
switch (blockQuery.data?.type) {
case 'reorg':
return `Reorged block #${ blockQuery.data?.height }`;
return `Reorged block #${ blockQuery.data?.height }${ chainText }`;
case 'uncle':
return `Uncle block #${ blockQuery.data?.height }`;
return `Uncle block #${ blockQuery.data?.height }${ chainText }`;
default:
return `Block #${ blockQuery.data?.height }`;
return `Block #${ blockQuery.data?.height }${ chainText }`;
}
})();
......
......@@ -7,6 +7,7 @@ import type { EntityTag as TEntityTag } from 'ui/shared/EntityTags/types';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import useEtherscanRedirects from 'lib/router/useEtherscanRedirects';
......@@ -38,6 +39,7 @@ const tacFeature = config.features.tac;
const TransactionPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const { chain } = useMultichainContext() || {};
const hash = getQueryParamString(router.query.hash);
......@@ -133,7 +135,7 @@ const TransactionPageContent = () => {
<>
<TextAd mb={ 6 }/>
<PageTitle
title="Transaction details"
title={ chain ? `Transaction details on ${ chain.config.chain.name }` : 'Transaction details' }
backLink={ backLink }
contentAfter={ tags }
secondRow={ titleSecondRow }
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { EntityTag as TEntityTag } from './types';
import { useMultichainContext } from 'lib/contexts/multichain';
import * as mixpanel from 'lib/mixpanel/index';
import { Link, LinkExternalIcon } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
......@@ -19,8 +20,9 @@ interface Props extends HTMLChakraProps<'span'> {
}
const EntityTag = ({ data, isLoading, noLink, ...rest }: Props) => {
const multichainContext = useMultichainContext();
const linkParams = !noLink ? getTagLinkParams(data) : undefined;
const linkParams = !noLink ? getTagLinkParams(data, multichainContext) : undefined;
const hasLink = Boolean(linkParams);
const iconColor = data.meta?.textColor ?? 'gray.400';
......
import type { EntityTag } from './types';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined {
import type { TMultichainContext } from 'lib/contexts/multichain';
export function getTagLinkParams(data: EntityTag, multichainContext?: TMultichainContext | null): { type: 'external' | 'internal'; href: string } | undefined {
if (data.meta?.warpcastHandle) {
return {
type: 'external',
......@@ -20,7 +22,7 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna
if (data.tagType === 'generic' || data.tagType === 'protocol') {
return {
type: 'internal',
href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }),
href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }, multichainContext),
};
}
}
......@@ -4,7 +4,7 @@ import React from 'react';
import { WagmiProvider } from 'wagmi';
import config from 'configs/app';
import { currentChain, parentChain } from 'lib/web3/chains';
import { currentChain, parentChain, clusterChains } from 'lib/web3/chains';
import wagmiConfig from 'lib/web3/wagmiConfig';
import { useColorMode } from 'toolkit/chakra/color-mode';
import colors from 'toolkit/theme/foundations/colors';
......@@ -19,9 +19,11 @@ const init = () => {
return;
}
const networks = [ currentChain, parentChain, ...(clusterChains ?? []) ].filter(Boolean) as [AppKitNetwork, ...Array<AppKitNetwork>];
createAppKit({
adapters: [ wagmiConfig.adapter ],
networks: [ currentChain, parentChain ].filter(Boolean) as [AppKitNetwork, ...Array<AppKitNetwork>],
networks,
metadata: {
name: `${ config.chain.name } explorer`,
description: `${ config.chain.name } explorer`,
......
......@@ -3,10 +3,11 @@ import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
import { toBech32Address } from 'lib/address/bech32';
import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import { useMultichainContext } from 'lib/contexts/multichain';
import { useSettingsContext } from 'lib/contexts/settings';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
......@@ -24,7 +25,10 @@ const getDisplayedAddress = (address: AddressProp, altHash?: string) => {
};
const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/address/[hash]', query: { ...props.query, hash: props.address.hash } });
const defaultHref = route(
{ pathname: '/address/[hash]', query: { ...props.query, hash: props.address.hash } },
props.chain ? { chain: props.chain } : undefined,
);
return (
<EntityBase.Link
......@@ -43,7 +47,10 @@ const Icon = (props: IconProps) => {
return null;
}
const marginRight = props.marginRight ?? (props.shield ? '18px' : '8px');
const shield = props.shield ?? (props.chain ? { src: props.chain.config.UI.navigation.icon.default } : undefined);
const hintPostfix: string = props.hintPostfix ?? (props.chain ? ` on ${ props.chain.config.chain.name } (Chain ID: ${ props.chain.config.chain.id })` : '');
const marginRight = props.marginRight ?? (shield ? '18px' : '8px');
const styles = {
...getIconProps(props.variant),
marginRight,
......@@ -60,6 +67,7 @@ const Icon = (props: IconProps) => {
return (
<EntityBase.Icon
{ ...props }
shield={ shield }
name="brands/safe"
/>
);
......@@ -68,11 +76,12 @@ const Icon = (props: IconProps) => {
const isProxy = Boolean(props.address.implementations?.length);
const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + (props.hintPostfix ?? '');
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract') + hintPostfix;
return (
<EntityBase.Icon
{ ...props }
shield={ shield }
name={ isProxy ? 'contracts/proxy' : contractIconName }
color={ isVerified ? 'green.500' : undefined }
borderRadius={ 0 }
......@@ -83,7 +92,11 @@ const Icon = (props: IconProps) => {
const label = (() => {
if (isDelegatedAddress) {
return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + (props.hintPostfix ?? '');
return (props.address.is_verified ? 'EOA + verified code' : 'EOA + code') + hintPostfix;
}
if (props.chain) {
return 'Address' + hintPostfix;
}
return props.hint;
......@@ -94,14 +107,14 @@ const Icon = (props: IconProps) => {
content={ label }
disabled={ !label }
interactive={ props.tooltipInteractive }
positioning={ props.shield ? { offset: { mainAxis: 8 } } : undefined }
positioning={ shield ? { offset: { mainAxis: 8 } } : undefined }
>
<Flex marginRight={ styles.marginRight } position="relative">
<AddressIdenticon
size={ props.variant === 'heading' ? 30 : 20 }
hash={ getDisplayedAddress(props.address) }
/>
{ props.shield && <EntityBase.IconShield { ...props.shield }/> }
{ shield && <EntityBase.IconShield { ...shield }/> }
{ isDelegatedAddress && <AddressIconDelegated isVerified={ Boolean(props.address.is_verified) }/> }
</Flex>
</Tooltip>
......@@ -183,7 +196,10 @@ const AddressEntity = (props: EntityProps) => {
const partsProps = distributeEntityProps(props);
const highlightContext = useAddressHighlightContext(props.noHighlight);
const settingsContext = useSettingsContext();
const multichainContext = useMultichainContext();
const altHash = !props.noAltHash && settingsContext?.addressFormat === 'bech32' ? toBech32Address(props.address.hash) : undefined;
const chain = props.chain ?? multichainContext?.chain;
// inside highlight context all tooltips should be interactive
// because non-interactive ones will not pass 'onMouseLeave' event to the parent component
......@@ -201,8 +217,8 @@ const AddressEntity = (props: EntityProps) => {
position="relative"
zIndex={ 0 }
>
<Icon { ...partsProps.icon } tooltipInteractive={ Boolean(highlightContext) }/>
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
<Icon { ...partsProps.icon } tooltipInteractive={ Boolean(highlightContext) } chain={ chain }/>
{ props.noLink ? content : <Link { ...partsProps.link } chain={ chain }>{ content }</Link> }
<Copy { ...partsProps.copy } altHash={ altHash } tooltipInteractive={ Boolean(highlightContext) }/>
</Container>
);
......
......@@ -11,7 +11,7 @@ import IconSvg from 'ui/shared/IconSvg';
import { distributeEntityProps } from '../base/utils';
import * as AddressEntity from './AddressEntity';
interface Props extends AddressEntity.EntityProps {
interface Props extends Omit<AddressEntity.EntityProps, 'chain'> {
chain: ChainInfo | null;
}
......@@ -40,10 +40,10 @@ const IconStub = () => {
);
};
const AddressEntryInterop = (props: Props) => {
const AddressEntryInterop = ({ chain, ...props }: Props) => {
const partsProps = distributeEntityProps(props);
const href = props.chain?.instance_url ? props.chain.instance_url.replace(/\/$/, '') + route({
const href = chain?.instance_url ? chain.instance_url.replace(/\/$/, '') + route({
pathname: '/address/[hash]',
query: {
...props.query,
......@@ -55,13 +55,13 @@ const AddressEntryInterop = (props: Props) => {
<Box position="relative">
<AddressEntity.Icon { ...partsProps.icon }/>
{ !props.isLoading && (
props.chain?.chain_logo ? (
chain?.chain_logo ? (
<Image
position="absolute"
bottom="-3px"
right="4px"
src={ props.chain.chain_logo }
alt={ props.chain.chain_name || 'external chain logo' }
src={ chain.chain_logo }
alt={ chain.chain_name || 'external chain logo' }
width="14px"
height="14px"
borderRadius="base"
......@@ -75,12 +75,12 @@ const AddressEntryInterop = (props: Props) => {
return (
<AddressEntity.Container className={ props.className }>
{ props.chain && (
<Tooltip content={ `Address on ${ props.chain.chain_name ? props.chain.chain_name : 'external chain' } (chain id ${ props.chain.chain_id })` }>
{ chain && (
<Tooltip content={ `Address on ${ chain.chain_name ? chain.chain_name : 'external chain' } (chain id ${ chain.chain_id })` }>
{ addressIcon }
</Tooltip>
) }
{ !props.chain && addressIcon }
{ !chain && addressIcon }
{ href ? (
<AddressEntity.Link { ...partsProps.link } href={ href } isExternal>
<AddressEntity.Content { ...partsProps.content }/>
......
......@@ -2,6 +2,8 @@ import { Box, chakra, Flex } from '@chakra-ui/react';
import type { IconProps } from '@chakra-ui/react';
import React from 'react';
import type { ChainConfig } from 'types/multichain';
import type { ImageProps } from 'toolkit/chakra/image';
import { Image } from 'toolkit/chakra/image';
import type { LinkProps } from 'toolkit/chakra/link';
......@@ -38,6 +40,7 @@ export interface EntityBaseProps {
truncationMaxSymbols?: number;
variant?: 'content' | 'heading' | 'subheading';
linkVariant?: LinkProps['variant'];
chain?: ChainConfig;
}
export interface ContainerBaseProps extends Pick<EntityBaseProps, 'className'> {
......@@ -59,7 +62,7 @@ const Container = chakra(({ className, children, ...props }: ContainerBaseProps)
);
});
export interface LinkBaseProps extends Pick<EntityBaseProps, 'className' | 'onClick' | 'isLoading' | 'isExternal' | 'href' | 'noLink' | 'query'> {
export interface LinkBaseProps extends Pick<EntityBaseProps, 'className' | 'onClick' | 'isLoading' | 'isExternal' | 'href' | 'noLink' | 'query' | 'chain'> {
children: React.ReactNode;
variant?: LinkProps['variant'];
}
......@@ -97,7 +100,7 @@ interface EntityIconProps extends Pick<IconProps, 'color' | 'borderRadius' | 'ma
tooltipInteractive?: boolean;
}
export interface IconBaseProps extends Pick<EntityBaseProps, 'isLoading' | 'noIcon' | 'variant'>, EntityIconProps {}
export interface IconBaseProps extends Pick<EntityBaseProps, 'isLoading' | 'noIcon' | 'variant' | 'chain'>, EntityIconProps {}
const Icon = ({ isLoading, noIcon, variant, name, color, borderRadius, marginRight, boxSize, shield, hint, tooltipInteractive }: IconBaseProps) => {
if (noIcon || !name) {
......@@ -136,12 +139,12 @@ const Icon = ({ isLoading, noIcon, variant, name, color, borderRadius, marginRig
return (
<Box position="relative">
{ iconElementWithHint }
<IconShield { ...shield }/>
<IconShield isLoading={ isLoading } { ...shield }/>
</Box>
);
};
type IconShieldProps = (ImageProps | IconSvgProps);
type IconShieldProps = (ImageProps | IconSvgProps) & { isLoading?: boolean };
const IconShield = (props: IconShieldProps) => {
......@@ -157,15 +160,16 @@ const IconShield = (props: IconShieldProps) => {
// Because the highlighted styles are described as CSS classes, we must do the same for the shield border color.
// borderColor: 'global.body.bg',
// backgroundColor: 'global.body.bg',
className: 'entity__shield',
};
if ('src' in props) {
return <Image className="entity__shield" { ...styles } { ...props }/>;
return props.isLoading ? <Skeleton loading { ...styles }/> : <Image { ...styles } { ...props }/>;
}
const svgProps = props as IconSvgProps;
return <IconSvg className="entity__shield" { ...styles } { ...svgProps }/>;
return <IconSvg { ...styles } { ...svgProps }/>;
};
export interface ContentBaseProps extends Pick<
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
import { useMultichainContext } from 'lib/contexts/multichain';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
......@@ -11,7 +12,10 @@ type LinkProps = EntityBase.LinkBaseProps & Partial<Pick<EntityProps, 'hash' | '
const Link = chakra((props: LinkProps) => {
const heightOrHash = props.hash ?? String(props.number);
const defaultHref = route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash } });
const defaultHref = route(
{ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash } },
props.chain ? { chain: props.chain } : undefined,
);
return (
<EntityBase.Link
......@@ -52,6 +56,7 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const BlockEntity = (props: EntityProps) => {
const multichainContext = useMultichainContext();
const partsProps = distributeEntityProps(props);
const content = <Content { ...partsProps.content }/>;
......@@ -59,7 +64,7 @@ const BlockEntity = (props: EntityProps) => {
return (
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
{ props.noLink ? content : <Link { ...partsProps.link } chain={ multichainContext?.chain }>{ content }</Link> }
</Container>
);
};
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
import { useMultichainContext } from 'lib/contexts/multichain';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
......@@ -10,7 +11,10 @@ import { distributeEntityProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'hash'>;
const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/tx/[hash]', query: { hash: props.hash } });
const defaultHref = route(
{ pathname: '/tx/[hash]', query: { hash: props.hash } },
props.chain ? { chain: props.chain } : undefined,
);
return (
<EntityBase.Link
......@@ -27,6 +31,10 @@ const Icon = (props: EntityBase.IconBaseProps) => {
<EntityBase.Icon
{ ...props }
name={ props.name ?? 'transactions_slim' }
shield={ props.shield ?? (props.chain ? {
src: props.chain.config.UI.navigation.icon.default,
} : undefined) }
hint={ props.chain ? `Transaction on ${ props.chain.config.chain.name } (Chain ID: ${ props.chain.config.chain.id })` : undefined }
/>
);
};
......@@ -63,13 +71,16 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
}
const TxEntity = (props: EntityProps) => {
const multichainContext = useMultichainContext();
const partsProps = distributeEntityProps(props);
const chain = props.chain ?? multichainContext?.chain;
const content = <Content { ...partsProps.content }/>;
return (
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
{ props.noLink ? content : <Link { ...partsProps.link } chain={ chain }>{ content }</Link> }
<Copy { ...partsProps.copy }/>
</Container>
);
......
......@@ -17,7 +17,7 @@ import * as TxEntity from './TxEntity';
type Props = {
chain: ChainInfo | null;
hash?: string | null;
} & Omit<TxEntity.EntityProps, 'hash'>;
} & Omit<TxEntity.EntityProps, 'hash' | 'chain'>;
const IconStub = ({ isLoading }: { isLoading?: boolean }) => {
return (
......
......@@ -5,11 +5,10 @@ import type { GasPriceInfo } from 'types/api/stats';
import type { GasUnit } from 'types/client/gasTracker';
import config from 'configs/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import formatGasValue from './formatGasValue';
const feature = config.features.gasTracker;
const UNITS_TO_API_FIELD_MAP: Record<GasUnit, 'price' | 'fiat_price'> = {
gwei: 'price',
usd: 'fiat_price',
......@@ -23,6 +22,9 @@ interface Props {
}
const GasPrice = ({ data, prefix, className, unitMode = 'primary' }: Props) => {
const multichainContext = useMultichainContext();
const feature = multichainContext?.chain?.config.features.gasTracker || config.features.gasTracker;
if (!data || !feature.isEnabled) {
return null;
}
......
import { createListCollection } from '@chakra-ui/react';
import React from 'react';
import multichainConfig from 'configs/multichain';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import { Image } from 'toolkit/chakra/image';
import { Select } from 'toolkit/chakra/select';
import type { SelectOption, SelectProps } from 'toolkit/chakra/select';
const collection = createListCollection<SelectOption>({
items: multichainConfig()?.chains.map((chain) => ({
value: chain.slug,
label: chain.config.chain.name || chain.slug,
icon: <Image src={ chain.config.UI.navigation.icon.default } alt={ chain.config.chain.name } boxSize={ 5 } borderRadius="full"/>,
})) || [],
});
interface Props extends Omit<SelectProps, 'collection' | 'placeholder'> {
loading?: boolean;
}
const ChainSelect = ({ loading, ...props }: Props) => {
const isInitialLoading = useIsInitialLoading(loading);
return (
<Select
collection={ collection }
defaultValue={ [ collection.items[0].value ] }
placeholder="Select chain"
loading={ isInitialLoading }
{ ...props }
/>
);
};
export default React.memo(ChainSelect);
......@@ -9,6 +9,7 @@ import type { PaginationParams } from './types';
import type { Route } from 'nextjs-routes';
import multichainConfig from 'configs/multichain';
import getResourceParams from 'lib/api/getResourceParams';
import type { PaginatedResourceName, PaginationFilters, PaginationSorting, ResourceError, ResourcePayload } from 'lib/api/resources';
import { SORTING_FIELDS } from 'lib/api/resources';
......@@ -51,12 +52,23 @@ function getNextPageParams<R extends PaginatedResourceName>(data: ResourcePayloa
return data.next_page_params;
}
function getChainValue(queryParam: string | Array<string> | undefined) {
const config = multichainConfig();
if (!config) {
return undefined;
}
return [ getQueryParamString(queryParam) || config.chains[0].slug ];
}
export type QueryWithPagesResult<Resource extends PaginatedResourceName> =
UseQueryResult<ResourcePayload<Resource>, ResourceError<unknown>> &
{
onFilterChange: <R extends PaginatedResourceName = Resource>(filters: PaginationFilters<R>) => void;
onSortingChange: (sorting?: PaginationSorting<Resource>) => void;
pagination: PaginationParams;
chainValue: Array<string> | undefined;
onChainValueChange: ({ value }: { value: Array<string> }) => void;
};
export default function useQueryWithPages<Resource extends PaginatedResourceName>({
......@@ -75,6 +87,9 @@ export default function useQueryWithPages<Resource extends PaginatedResourceName
[page]: getPaginationParamsFromQuery(router.query.next_page_params),
});
const [ hasPages, setHasPages ] = React.useState(page > 1);
const [ chainValue, setChainValue ] = React.useState<Array<string> | undefined>(
getChainValue(router.query['chain-slug']),
);
const isMounted = React.useRef(false);
const queryParams = { ...pageParams[page], ...filters, ...sorting };
......@@ -90,6 +105,7 @@ export default function useQueryWithPages<Resource extends PaginatedResourceName
staleTime: page === 1 ? 0 : Infinity,
...options,
},
chainSlug: chainValue?.[0],
});
const { data } = queryResult;
const nextPageParams = getNextPageParams(data);
......@@ -134,15 +150,19 @@ export default function useQueryWithPages<Resource extends PaginatedResourceName
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}, [ router, page, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => {
const resetPage = useCallback(({ chainValue }: { chainValue?: Array<string> } = {}) => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
const nextRouterQuery = omit(router.query, [ 'next_page_params', 'page' ]);
if (chainValue) {
nextRouterQuery['chain-slug'] = chainValue[0];
}
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
setPage(1);
setPageParams(INITIAL_PAGE_PARAMS);
chainValue && setChainValue(chainValue);
window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from
// so have to remove it but with some delay :)
......@@ -204,6 +224,20 @@ export default function useQueryWithPages<Resource extends PaginatedResourceName
});
}, [ router, scrollToTop ]);
const onChainValueChange = useCallback(({ value }: { value: Array<string> }) => {
if (page !== 1) {
resetPage({ chainValue: value });
} else {
const nextPageQuery = {
...router.query,
'chain-slug': value[0],
};
setChainValue(value);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}
}, [ page, resetPage, router ]);
const hasNextPage = nextPageParams ? Object.keys(nextPageParams).length > 0 : false;
const pagination = {
......@@ -245,5 +279,5 @@ export default function useQueryWithPages<Resource extends PaginatedResourceName
setHasPages(pageFromQuery > 1);
}, [ router.query ]);
return { ...queryResult, pagination, onFilterChange, onSortingChange };
return { ...queryResult, pagination, onFilterChange, onSortingChange, chainValue, onChainValueChange };
}
......@@ -31,6 +31,7 @@ const Footer = () => {
const { data: backendVersionData } = useApiQuery('general:config_backend_version', {
queryOptions: {
staleTime: Infinity,
enabled: !config.features.opSuperchain.isEnabled,
},
});
const apiVersionUrl = getApiVersionUrl(backendVersionData?.backend_version);
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { IndexingStatus } from 'types/api/indexingStatus';
import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -14,7 +15,11 @@ import IconSvg from 'ui/shared/IconSvg';
const IntTxsIndexingStatus = () => {
const { data, isError, isPending } = useApiQuery('general:homepage_indexing_status');
const { data, isError, isPending } = useApiQuery('general:homepage_indexing_status', {
queryOptions: {
enabled: !config.UI.indexingAlert.intTxs.isHidden,
},
});
const bgColor = { base: 'blackAlpha.100', _dark: 'whiteAlpha.100' };
......
......@@ -23,7 +23,7 @@ const TopBar = () => {
justifyContent="space-between"
alignItems="center"
>
<TopBarStats/>
{ !config.features.opSuperchain.isEnabled ? <TopBarStats/> : <div/> }
<Flex alignItems="center">
{ config.features.deFiDropdown.isEnabled && (
<>
......
......@@ -5,6 +5,7 @@ import type { AddressParam } from 'types/api/addressParams';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useMultichainContext } from 'lib/contexts/multichain';
import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate';
import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import { Link } from 'toolkit/chakra/link';
......@@ -25,9 +26,10 @@ type Props = {
txQuery: TxQuery;
};
const feature = config.features.txInterpretation;
const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
const multichainContext = useMultichainContext();
const feature = multichainContext?.chain?.config.features.txInterpretation || config.features.txInterpretation;
const hasInterpretationFeature = feature.isEnabled;
const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves';
......
......@@ -4,9 +4,10 @@ import React from 'react';
import type { Transaction } from 'types/api/transaction';
import { route } from 'nextjs-routes';
import { route } from 'nextjs/routes';
import config from 'configs/app';
import { useMultichainContext } from 'lib/contexts/multichain';
import getValueWithUnit from 'lib/getValueWithUnit';
import { currencyUnits } from 'lib/units';
import { Link } from 'toolkit/chakra/link';
......@@ -16,6 +17,8 @@ import TxFee from 'ui/shared/tx/TxFee';
import Utilization from 'ui/shared/Utilization/Utilization';
const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
const multichainContext = useMultichainContext();
const sectionProps = {
borderBottom: '1px solid',
borderColor: 'border.divider',
......@@ -116,7 +119,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
</Box>
</Box>
) }
<Link href={ route({ pathname: '/tx/[hash]', query: { hash: tx.hash } }) }>More details</Link>
<Link href={ route({ pathname: '/tx/[hash]', query: { hash: tx.hash } }, multichainContext) }>More details</Link>
</>
);
};
......
......@@ -63,7 +63,10 @@ const TxType = ({ types, isLoading }: Props) => {
default:
label = 'Transaction';
colorPalette = 'purple';
}
if (!label) {
return null;
}
return (
......
......@@ -4,12 +4,12 @@ import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
import type { PaginationParams } from 'ui/shared/pagination/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import getNextSortValue from 'ui/shared/sort/getNextSortValue';
import useDescribeTxs from './noves/useDescribeTxs';
......@@ -25,10 +25,7 @@ const SORT_SEQUENCE: Record<TransactionsSortingField, Array<TransactionsSortingV
type Props = {
query: QueryWithPagesResult<'general:txs_validated' | 'general:txs_pending'> |
QueryWithPagesResult<'general:txs_watchlist'> |
QueryWithPagesResult<'general:block_txs'> |
QueryWithPagesResult<'general:zkevm_l2_txn_batch_txs'>;
pagination: PaginationParams;
showBlockInfo?: boolean;
socketType?: TxsSocketType;
currentAddress?: string;
......@@ -44,7 +41,7 @@ type Props = {
};
const TxsContent = ({
query,
pagination,
filter,
filterValue,
showBlockInfo = true,
......@@ -65,7 +62,7 @@ const TxsContent = ({
setSorting(value);
}, [ sort, setSorting ]);
const itemsWithTranslation = useDescribeTxs(items, currentAddress, query.isPlaceholderData);
const itemsWithTranslation = useDescribeTxs(items, currentAddress, isPlaceholderData);
const content = itemsWithTranslation ? (
<>
......@@ -86,7 +83,7 @@ const TxsContent = ({
onSortToggle={ onSortToggle }
showBlockInfo={ showBlockInfo }
socketType={ socketType }
top={ top || (query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0) }
top={ top || (pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0) }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
isLoading={ isPlaceholderData }
......@@ -100,14 +97,14 @@ const TxsContent = ({
mt={ -6 }
sorting={ sort }
setSorting={ setSorting }
paginationProps={ query.pagination }
showPagination={ query.pagination.isVisible }
paginationProps={ pagination }
showPagination={ pagination.isVisible }
filterComponent={ filter }
linkSlot={ currentAddress ? (
<AddressCsvExportLink
address={ currentAddress }
params={{ type: 'transactions', filterType: 'address', filterValue }}
isLoading={ query.pagination.isLoading }
isLoading={ pagination.isLoading }
/>
) : null
}
......
......@@ -55,7 +55,7 @@ const TxsWithAPISorting = ({
isError={ query.isError }
setSorting={ handleSortChange }
sort={ sorting }
query={ query }
pagination={ query.pagination }
/>
);
};
......
......@@ -48,7 +48,7 @@ const TxsWithFrontendSorting = ({
isError={ isError }
setSorting={ setSortByValue }
sort={ sorting }
query={ query }
pagination={ query.pagination }
/>
);
};
......
......@@ -22,6 +22,7 @@ const TxsSocketNotice = ({ type, place, isLoading }: Props) => {
switch (type) {
case 'txs_home':
case 'txs_home_cross_chain':
case 'txs_validated':
case 'txs_pending': {
return <TxsSocketNoticeTypeAll type={ type } place={ place } isLoading={ isLoading }/>;
......
......@@ -2,6 +2,9 @@ import React from 'react';
import type { TxsSocketNoticePlace, TxsSocketType } from './types';
import { route } from 'nextjs/routes';
import { useMultichainContext } from 'lib/contexts/multichain';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useNewTxsSocketTypeAll from './useTxsSocketTypeAll';
......@@ -13,18 +16,30 @@ interface Props {
}
const TxsSocketNoticeTypeAll = ({ type, place, isLoading }: Props) => {
const multichainContext = useMultichainContext();
const { num, alertText } = useNewTxsSocketTypeAll({ type, isLoading });
if (num === undefined) {
return null;
}
const url = (() => {
if (type === 'txs_home_cross_chain') {
return route({ pathname: '/txs' });
}
if (type === 'txs_home' && multichainContext) {
return route({ pathname: '/txs' }, multichainContext);
}
})();
if (place === 'table') {
return (
<SocketNewItemsNotice.Desktop
alert={ alertText }
num={ num }
isLoading={ isLoading }
url={ url }
/>
);
}
......@@ -35,6 +50,7 @@ const TxsSocketNoticeTypeAll = ({ type, place, isLoading }: Props) => {
num={ num }
alert={ alertText }
isLoading={ isLoading }
url={ url }
/>
);
}
......
export type TxsSocketType = 'txs_validated' | 'txs_pending' | 'txs_home' | 'address_txs';
export type TxsSocketType = 'txs_validated' | 'txs_pending' | 'txs_home' | 'address_txs' | 'txs_home_cross_chain';
export type TxsSocketNoticePlace = 'list' | 'table';
......@@ -8,6 +8,7 @@ import type { Transaction, TransactionsSortingValue } from 'types/api/transactio
import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import { useMultichainContext } from 'lib/contexts/multichain';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -47,12 +48,18 @@ export default function useTxsSocketTypeAddress({ isLoading }: Params) {
const filterValue = getQueryParamString(router.query.filter);
const page = getQueryParamString(router.query.page);
const sort = getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default';
const { chain } = useMultichainContext() || {};
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => {
setAlertText('');
const queryKey = getResourceKey('general:address_txs', {
pathParams: { hash: currentAddress },
queryParams: filterValue ? { filter: filterValue } : undefined,
chainSlug: chain?.slug,
});
queryClient.setQueryData(
getResourceKey('general:address_txs', { pathParams: { hash: currentAddress }, queryParams: filterValue ? { filter: filterValue } : undefined }),
queryKey,
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
......@@ -90,7 +97,7 @@ export default function useTxsSocketTypeAddress({ isLoading }: Params) {
].sort(sortTxsFromSocket(sort)),
};
});
}, [ currentAddress, filterValue, queryClient, sort ]);
}, [ currentAddress, filterValue, queryClient, sort, chain?.slug ]);
const handleSocketClose = React.useCallback(() => {
setAlertText('Connection is lost. Please refresh the page to load new transactions.');
......
......@@ -14,6 +14,9 @@ function getSocketParams(type: TxsSocketType, page: string) {
case 'txs_home': {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
case 'txs_home_cross_chain': {
return { topic: 'interop_messages:new_messages' as const, event: 'new_messages' as const };
}
case 'txs_validated': {
return !page || page === '1' ? { topic: 'transactions:new_transaction' as const, event: 'transaction' as const } : {};
}
......
......@@ -1568,6 +1568,11 @@
resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66"
integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ==
"@blockscout/multichain-aggregator-types@1.6.0-alpha.0":
version "1.6.0-alpha.0"
resolved "https://registry.yarnpkg.com/@blockscout/multichain-aggregator-types/-/multichain-aggregator-types-1.6.0-alpha.0.tgz#13e26516af2568daecaee9c1e43066baa4050838"
integrity sha512-7M8WvkOrRwPzE+ZItEVl0Fq27OerVnaNy+CY//WNfQF7x4bZNY1fTlvi6gTAQUiAv7mbkVAJj22XwfKENascMw==
"@blockscout/points-types@1.3.0-alpha.2":
version "1.3.0-alpha.2"
resolved "https://registry.yarnpkg.com/@blockscout/points-types/-/points-types-1.3.0-alpha.2.tgz#0308dcb4eef0dadf96f43b144835470e9f78f64f"
......
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