Merge upstream blockscout/frontend (29 commits)

Sync with upstream blockscout frontend, keeping Lux Network branding
in footer and documentation links. Added llms.txt link from upstream.
This commit is contained in:
Zach Kelling
2026-02-22 00:59:24 -08:00
552 changed files with 9649 additions and 1846 deletions
+53
View File
@@ -0,0 +1,53 @@
# Bugbot PR Review Rules
In addition to the regular bug-finding process, check the following aspects in the code changes introduced by the Pull Request.
## 1. Design System & Theming
- Verify that all color values use **semantic color tokens** from the project's design system (`text.secondary`, `border.divider`, `icon.secondary`, `button.solid.bg`, `global.body.bg`, etc.). Flag any hardcoded color values such as `rgb(...)`, `#hex`. The full list of available tokens can be found in `toolkit/theme/foundations/semanticTokens.ts` and `toolkit/theme/foundations/colors.ts`. In rare cases the author may use hardcoded color values, but they must leave a comment explaining why.
- Flag any custom `box-shadow` values. Advise the author to use the design system's shadow tokens instead.
- Check for custom margins or paddings that override defaults of **internal parts** of compound components (e.g., `DialogHeader` inside a `Dialog`). This rule does not apply to the root component itself. If a part component already has default spacing from the design system, it should not be overridden with custom values. This applies to all components from the toolkit (see `toolkit/chakra`).
- Look for duplicated style props — check whether a property is already set by an inherited style prop and flag any redundant re-declarations.
- Refer to the Chakra documentation at [llms.txt](https://chakra-ui.com/llms.txt) to understand what components Chakra UI provides and how the styling system works.
## 2. TypeScript & Code Quality
- Check for usage of the `any` type. If found, advise the author to use `unknown` instead and narrow it properly in the code.
- Look for `eslint-disable` comments. If any exist without an accompanying explanation, ask the author to add a comment describing why the rule was disabled. Example: `// eslint-disable-next-line @typescript-eslint/no-explicit-any -- API response shape is dynamic and validated at runtime`.
- Identify magic numbers in the code. Advise the author to extract them into named constants using `UPPER_UNDERSCORE_CASE` and place the constants above the component definition. For example, `const MAX_VISIBLE_ITEMS = 4;` is preferable to an inline `4`.
- Evaluate naming of hooks, features, components, functions, and variables. Names should be specific and self-documenting. Flag vague names like `useWidgets` and suggest more descriptive alternatives such as `useAddress3rdPartyWidgets`.
- Where an `as MyType[]` assertion is used, suggest using `satisfies Array<MyType>` instead, if applicable.
- Check for inline empty arrays or objects used as default values on every render (e.g., `?? []`). Advise the author to define a static constant outside the component (e.g., `const EMPTY_ARRAY: Array<T> = [];`) and reference it instead.
- Check whether `.filter()`, `.map()`, or `.reduce()` calls produce new array references that are passed as props or used as hook dependencies. If so, advise the author to wrap them in `useMemo`.
- Flag unnecessary string interpolation in file paths. Explicit file names (e.g., `streak_30.png`) are preferable to template literals (e.g., `` `streak_${days}.png` ``), as they are easier to locate with search tools.
## 3. Environment Variables
When a PR adds, changes, renames, or removes an environment variable, verify that **all** of the following steps have been completed:
- The variable is documented in `docs/ENVS.md` with its name, expected type, compulsoriness, default value, and an example value.
- The variable is added to the app config in the appropriate section of `configs/app/` (`features/`, `ui.ts`, `api.ts`, etc.).
- A validation schema is added or updated in `deploy/tools/envs-validator/schema.ts`. For complex configs, a **separate sub-schema** should be created (similar to `tacScheme` or `beaconChainSchema`).
- The variable is added to the test presets in `deploy/tools/envs-validator/test/.env.base`. If the variable supports alternative configurations, examples should also be added to `.env.alt`.
- If the variable contains an external URL that is **not** an asset URL (i.e., not an image or JSON file), the appropriate CSP policy in `nextjs/csp/policies/` is updated. Policies should only be added when the relevant config option is actually enabled.
- If the variable links to an external resource (image or JSON config), the `ASSETS_ENVS` array in `deploy/scripts/download_assets.sh` is extended with the variable name.
- If the variable stores a JSON config URL, an example file is added to `deploy/tools/envs-validator/test/assets/configs/` and the `envsWithJsonConfig` array in `deploy/tools/envs-validator/index.ts` is extended.
- The PR description clearly states that a new ENV variable was added and includes example values.
## 4. Component Architecture & Reuse
- When links to **application pages** are constructed, verify that `nextjs-routes` or `nextjs/routes` utilities are used instead of string concatenation or template literals. The full list of application routes is available in `nextjs/nextjs-routes.d.ts`.
## 5. Testing with Playwright
- Flag any tests that rely on CSS class selectors (`.className`). These will break when the implementation changes. Advise the author to use roles, test IDs, text content, or other semantic selectors instead.
- Flag unnecessary use of the `testFn` pattern (i.e., `const testFn: TestFixture<...> = async (...)`). This pattern makes refactoring harder and can bypass Playwright lint rules. It should only be used when genuinely sharing test logic across multiple test suites.
- Check for unexplained magic values in test data. If a test uses a specific number or string, its meaning should be clear. Advise the author to use named constants (e.g., `const BLOCK_HEIGHT = 3947;` instead of a bare `3947`).
- Check for hardcoded URLs or values that already exist in mock files. Advise the author to import and reference them from the existing mocks to keep tests DRY and consistent.
## 6. Utilities & Best Practices
- Check whether any utility logic could be replaced by an `es-toolkit` function (e.g., `clamp`, `get`). Advise the author to prefer existing utilities over manual implementations.
- Flag unsafe type coercions such as `as unknown as MyType`. If coercion is necessary, the author should add runtime validation or a comment explaining why the cast is safe.
- Check that date and time formatting uses the shared project utilities (`Time` or `TimeWithTooltip` components). The project maintains a single consistent date format.
- Flag any commented-out code blocks. Advise the author to either implement the feature or remove the dead code entirely. TODOs are acceptable only when there is a clear follow-up plan.
+1 -1
View File
@@ -21,7 +21,7 @@ _Note_ in the command output, format all URLs as clickable Markdown links: `[Lin
- If found, fetch the issue details using `gh issue view [issue_number]`
- Include "Resolves #[issue_number]" at the very beginning of the description (in the "Description and Related Issue(s)" section)
- Summarize the changes clearly and concisely, using no more than two paragraphs. If necessary, use bullet points to highlight the main changes in the codebase. Be precise, this description should not be very long.
- List any changes in the enviroment variables (look at the `./docs/ENVS.md` file) in a separate section, describe purpose of each variable change
- List any changes in the environment variables (look at the `./docs/ENVS.md` file) in a separate section, describe purpose of each variable change
- Bad example: "Added `NEXT_PUBLIC_VIEWS_TX_GROUPED_FEES` environment variable to the documentation"
- Good example: "Added `NEXT_PUBLIC_VIEWS_TX_GROUPED_FEES` to group transaction fees into one section on the transaction page"
- Good example: "Extended possible values for `NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS` with set_max_gas_limit to display the maximum gas price set by the transaction sender"
+2 -2
View File
@@ -183,7 +183,7 @@ type FetchingState<TData> =
Do not introduce new enums into the codebase. Retain existing enums.
If you require enum-like behaviour, use an `as const` object:
If you require enum-like behavior, use an `as const` object:
```ts
const backendToFrontendEnum = {
@@ -267,7 +267,7 @@ interface C extends A, B {
Use JSDoc comments to annotate functions and types.
Be concise in JSDoc comments, and only provide JSDoc comments if the function's behaviour is not self-evident.
Be concise in JSDoc comments, and only provide JSDoc comments if the function's behavior is not self-evident.
Use the JSDoc inline `@link` tag to link to other functions and types within the same file.
+2 -1
View File
@@ -10,4 +10,5 @@ README.md
*.tsbuildinfo
.eslintcache
/test-results/
/playwright-report/
/playwright-report/
/blob-report/
+116 -21
View File
@@ -13,7 +13,7 @@ on:
- 'public/**'
- 'stub/**'
- 'tools/**'
# concurrency:
# group: ${{ github.workflow }}__${{ github.job }}__${{ github.ref }}
# cancel-in-progress: true
@@ -44,7 +44,7 @@ jobs:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
@@ -59,6 +59,12 @@ jobs:
- name: Compile TypeScript
run: yarn lint:tsc
- name: Check licenses
run: yarn lint:license:check
- name: Check spelling
run: yarn lint:cspell --no-progress
toolkit_build_check:
name: Toolkit build check
needs: [ code_quality ]
@@ -80,7 +86,7 @@ jobs:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install project dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
@@ -111,12 +117,12 @@ jobs:
echo "Build failed: dist directory not found"
exit 1
fi
if [ ! -f "dist/index.js" ]; then
echo "Build failed: dist/index.js not found"
exit 1
fi
if [ ! -f "dist/index.d.ts" ]; then
echo "Build failed: dist/index.d.ts not found"
exit 1
@@ -143,7 +149,7 @@ jobs:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
@@ -186,7 +192,7 @@ jobs:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
@@ -222,7 +228,7 @@ jobs:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
@@ -231,14 +237,14 @@ jobs:
if: steps.cache-node-modules.outputs.cache-hit == 'true'
run: yarn chakra:typegen
- name: Install script dependencies
- name: Install script dependencies
run: cd ./deploy/tools/affected-tests && yarn --frozen-lockfile
- name: Run script
run: yarn test:pw:detect-affected
- name: Upload result file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: playwright-affected-tests
path: ./playwright/affected-tests.txt
@@ -249,9 +255,9 @@ jobs:
needs: [ code_quality, envs_validation, pw_affected_tests ]
if: |
always() &&
needs.code_quality.result == 'success' &&
needs.envs_validation.result == 'success' &&
(needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
needs.code_quality.result == 'success' &&
needs.envs_validation.result == 'success' &&
(needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-noble
@@ -283,7 +289,7 @@ jobs:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
@@ -294,7 +300,7 @@ jobs:
- name: Download affected tests list
if: ${{ needs.pw_affected_tests.result == 'success' }}
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
continue-on-error: true
with:
name: playwright-affected-tests
@@ -306,10 +312,99 @@ jobs:
HOME: /root
PW_PROJECT: ${{ matrix.project }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v5
with:
name: playwright-report-${{ matrix.project }}
name: blob-report-${{ matrix.project }}
path: blob-report
retention-days: 1
pw_report:
name: Generate Playwright report
# Merge reports after pw_tests job, even if some shards have failed
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
needs: [ pw_tests ]
steps:
- name: Download blob reports from GitHub Actions Artifacts
id: download-reports
uses: actions/download-artifact@v5
continue-on-error: true
with:
path: ${{ runner.temp }}/all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Check for downloaded reports
id: check-reports
run: |
if [ -d "$RUNNER_TEMP/all-blob-reports" ] && [ -n "$(ls -A "$RUNNER_TEMP/all-blob-reports" 2>/dev/null)" ]; then
echo "has_reports=true" >> $GITHUB_OUTPUT
else
echo "has_reports=false" >> $GITHUB_OUTPUT
fi
- name: Checkout repo
if: steps.check-reports.outputs.has_reports == 'true'
uses: actions/checkout@v4
- name: Setup node
if: steps.check-reports.outputs.has_reports == 'true'
uses: actions/setup-node@v4
with:
node-version: 22.14.0
cache: 'yarn'
- name: Cache node_modules
if: steps.check-reports.outputs.has_reports == 'true'
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.check-reports.outputs.has_reports == 'true' && steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
- name: Merge into HTML Report
if: steps.check-reports.outputs.has_reports == 'true'
run: npx playwright merge-reports --reporter html "$RUNNER_TEMP/all-blob-reports"
- name: Upload HTML report artifact
# Upload HTML report to GitHub Actions Artifacts only for pull requests from forks
if: steps.check-reports.outputs.has_reports == 'true' && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
uses: actions/upload-artifact@v5
with:
name: playwright-html-report
path: playwright-report
retention-days: 10
retention-days: 7
- name: Configure AWS credentials
if: steps.check-reports.outputs.has_reports == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Upload HTML report to S3
# Upload HTML report to S3 only for pull requests from the main repository or for non-pull requests events
if: steps.check-reports.outputs.has_reports == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
run: |
export "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`"
aws s3 cp playwright-report/ s3://${{ vars.AWS_BUCKET_NAME }}/report-${SHORT_SHA} --recursive
echo "PLAYWRIGHT_URL=https://${{ vars.AWS_BUCKET_NAME }}.s3.${{ vars.AWS_REGION }}.amazonaws.com/report-${SHORT_SHA}/index.html" >> "$GITHUB_ENV"
- name: Add link to job summary
if: steps.check-reports.outputs.has_reports == 'true'
run: |
echo "### Playwright report" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ env.PLAYWRIGHT_URL }}" ]; then
echo "[View report (S3)](${{ env.PLAYWRIGHT_URL }})" >> $GITHUB_STEP_SUMMARY
else
echo "Download the **playwright-html-report** artifact below to view the report (open \`index.html\` in a browser)." >> $GITHUB_STEP_SUMMARY
fi
echo "[Read about ducks](https://animalkingdom.org/animals/ducks/)" >> $GITHUB_STEP_SUMMARY
+14 -13
View File
@@ -1,19 +1,20 @@
name: Deploy from main branch
on:
push:
branches:
- main
paths-ignore:
- '.github/ISSUE_TEMPLATE/**'
- '.husky/**'
- '.vscode/**'
- 'docs/**'
- 'vitest/**'
- 'mocks/**'
- 'playwright/**'
- 'stubs/**'
- 'tools/**'
# NOTE: Currently disabled to avoid unnecessary deployments since we only publish private images and do not use unstable builds in our internal pipelines.
# push:
# branches:
# - main
# paths-ignore:
# - '.github/ISSUE_TEMPLATE/**'
# - '.husky/**'
# - '.vscode/**'
# - 'docs/**'
# - 'vitest/**'
# - 'mocks/**'
# - 'playwright/**'
# - 'stubs/**'
# - 'tools/**'
workflow_dispatch:
concurrency:
+1
View File
@@ -24,6 +24,7 @@ on:
- immutable
- mega_eth
- neon_devnet
- numine
- optimism
- optimism_sepolia
- optimism_superchain
+1
View File
@@ -26,6 +26,7 @@ on:
- mega_eth
- mekong
- neon_devnet
- numine
- optimism
- optimism_sepolia
- optimism_superchain
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
with:
project_name: ${{ vars.PROJECT_NAME }}
field_name: Status
field_value: Ready For Realease
field_value: Ready For Release
issues: "[${{ github.event.issue.number }}]"
secrets: inherit
permissions:
+2
View File
@@ -55,6 +55,7 @@ yarn-error.log*
**.decrypted~**
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.browser/
/playwright/envs.js
@@ -65,6 +66,7 @@ yarn-error.log*
# build outputs
/tools/preset-sync/index.js
/toolkit/package/dist
license.json
AGENTS.md
CLAUDE.md
+3
View File
@@ -5,6 +5,9 @@
echo 🧿 Running file linter...
npx lint-staged
echo 🧿 Running spelling checker...
git diff --diff-filter=ACMRT --cached --name-only | npx cspell --config cspell.jsonc --file-list stdin --no-must-find-files
# format svg
echo 🧿 Running svg formatter...
for file in `git diff --diff-filter=ACMRT --cached --name-only | grep ".svg\$"`
+6
View File
@@ -0,0 +1,6 @@
{
"exclude": [
// It currently has two licenses (MIT and Apache-2.0), which are not correctly recognized by license-report-check.
"@helia/verified-fetch"
]
}
+2 -1
View File
@@ -30,7 +30,7 @@
"command": "yarn dev:preset:sync --name=${input:dev_config_preset}",
"problemMatcher": [],
"label": "dev preset sync",
"detail": "syncronize dev preset",
"detail": "synchronize dev preset",
"presentation": {
"reveal": "always",
"panel": "shared",
@@ -355,6 +355,7 @@
"mega_eth",
"mekong",
"neon_devnet",
"numine",
"optimism",
"optimism_interop_0",
"optimism_sepolia",
+1 -1
View File
@@ -98,7 +98,7 @@ RUN set -a && \
# ENV NEXT_TELEMETRY_DISABLED 1
# Build app for production
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NODE_OPTIONS="--max-old-space-size=8192"
RUN yarn build
+12
View File
@@ -83,6 +83,17 @@ const contractInfoApi = (() => {
});
})();
const interchainIndexerApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const metadataApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST');
if (!apiHost) {
@@ -221,6 +232,7 @@ const apis: Apis = Object.freeze({
bens: bensApi,
clusters: clustersApi,
contractInfo: contractInfoApi,
interchainIndexer: interchainIndexerApi,
metadata: metadataApi,
multichainAggregator: multichainAggregatorApi,
multichainStats: multichainStatsApi,
+30 -8
View File
@@ -1,4 +1,5 @@
import type { Feature } from './types';
import type { AuthProvider } from 'types/client/account';
import app from '../app';
import services from '../services';
@@ -6,18 +7,39 @@ import { getEnvValue } from '../utils';
const title = 'My account';
const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
const config: Feature<{
isEnabled: true;
authProvider: AuthProvider;
dynamic?: {
environmentId: string;
};
}> = (() => {
if (
!app.isPrivateMode &&
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
services.reCaptchaV2.siteKey
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true'
) {
return Object.freeze({
title,
isEnabled: true,
recaptchaSiteKey: services.reCaptchaV2.siteKey,
});
const authProvider = getEnvValue('NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER');
const dynamicEnvironmentId = getEnvValue('NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID');
if (authProvider === 'dynamic' && dynamicEnvironmentId) {
return Object.freeze({
title,
isEnabled: true,
authProvider: 'dynamic',
dynamic: {
environmentId: dynamicEnvironmentId,
},
});
}
if (services.reCaptchaV2.siteKey) {
return Object.freeze({
title,
isEnabled: true,
authProvider: 'auth0',
});
}
}
return Object.freeze({
+31 -11
View File
@@ -3,13 +3,22 @@ import type { Feature } from './types';
import app from '../app';
import chain from '../chain';
import { getEnvValue, parseEnvJson } from '../utils';
import accountFeature from './account';
import opSuperchain from './opSuperchain';
const walletConnectProjectId = getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID');
const title = 'Blockchain interaction (writing to contract, etc.)';
const config: Feature<{ walletConnect: { projectId: string; featuredWalletIds: Array<string> } }> = (() => {
type FeaturePayload = {
connectorType: 'reown';
reown: { projectId: string; featuredWalletIds: Array<string> };
} | {
connectorType: 'dynamic';
dynamic: { environmentId: string };
};
const config: Feature<FeaturePayload> = (() => {
// all chain parameters are required for wagmi provider
// @wagmi/chains/dist/index.d.ts
@@ -26,17 +35,28 @@ const config: Feature<{ walletConnect: { projectId: string; featuredWalletIds: A
if (
!app.isPrivateMode &&
(isSingleChain || isOpSuperchain) &&
walletConnectProjectId
(isSingleChain || isOpSuperchain)
) {
return Object.freeze({
title,
isEnabled: true,
walletConnect: {
projectId: walletConnectProjectId,
featuredWalletIds: parseEnvJson<Array<string>>(getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS')) ?? [],
},
});
if (accountFeature.isEnabled && accountFeature.authProvider === 'dynamic' && accountFeature.dynamic?.environmentId) {
return Object.freeze({
title,
isEnabled: true,
connectorType: 'dynamic',
dynamic: {
environmentId: accountFeature.dynamic.environmentId,
},
});
} else if (walletConnectProjectId) {
return Object.freeze({
title,
isEnabled: true,
connectorType: 'reown',
reown: {
projectId: walletConnectProjectId,
featuredWalletIds: parseEnvJson<Array<string>>(getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS')) ?? [],
},
});
}
}
return Object.freeze({
+22
View File
@@ -0,0 +1,22 @@
import type { Feature } from './types';
import apis from '../apis';
import { getEnvValue } from '../utils';
const title = 'Cross-chain transactions';
const config: Feature<{}> = (() => {
if (getEnvValue('NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED') === 'true' && apis.interchainIndexer) {
return Object.freeze({
title,
isEnabled: true,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
+1
View File
@@ -10,6 +10,7 @@ export { default as beaconChain } from './beaconChain';
export { default as bridgedTokens } from './bridgedTokens';
export { default as blockchainInteraction } from './blockchainInteraction';
export { default as celo } from './celo';
export { default as crossChainTxs } from './crossChainTxs';
export { default as csvExport } from './csvExport';
export { default as dataAvailability } from './dataAvailability';
export { default as deFiDropdown } from './deFiDropdown';
+2 -1
View File
@@ -8,7 +8,8 @@ import blockchainInteraction from './blockchainInteraction';
const title = 'Rewards service integration';
const config: Feature<{}> = (() => {
if (!app.isPrivateMode && apis.rewards && account.isEnabled && blockchainInteraction.isEnabled) {
// @0xdeval: as of now, we won't support rewards programs with dynamic auth provider
if (!app.isPrivateMode && apis.rewards && account.isEnabled && account.authProvider === 'auth0' && blockchainInteraction.isEnabled) {
return Object.freeze({
title,
isEnabled: true,
+3
View File
@@ -85,6 +85,9 @@ const UI = Object.freeze({
maintenanceAlert: {
message: getEnvValue('NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE'),
},
apiKeysAlert: {
message: getEnvValue('NEXT_PUBLIC_API_KEYS_ALERT_MESSAGE'),
},
explorers: {
items: parseEnvJson<Array<NetworkExplorer>>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [],
},
+1
View File
@@ -76,3 +76,4 @@ NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
NEXT_PUBLIC_API_KEYS_ALERT_MESSAGE='<strong>Chain-specific API keys are being deprecated.</strong> Please migrate to <a href="https://docs.blockscout.com/for-developers/api-keys/pro-api" target="_blank" rel="noopener noreferrer">Blockscout's PRO API</a> for new multichain access. Existing API keys will become invalid on 1st of Jan 2027'
+2 -1
View File
@@ -10,6 +10,7 @@ NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/homepage-highlights/test.json
# NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic
# Instance ENVs
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
@@ -64,7 +65,7 @@ NEXT_PUBLIC_NETWORK_ID=11155111
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_NAME=Sepolia
NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia.drpc.org
NEXT_PUBLIC_NETWORK_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
+40
View File
@@ -0,0 +1,40 @@
# Set of ENVs for Numine network explorer
# https://numine.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=numine"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED=true
NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST=https://interchain-indexer.k8s-dev.blockscout.com
# Instance ENVs
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol','drops','blockscoutbadges','gitpoap','efp','etherscore','webacy','humanpassport','trustblock','smartmuv','humanode']
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=numine.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES={"record_sessions_percent": 0.5,"record_heatmap_data": true}
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=NUMINE
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=NUMINE
NEXT_PUBLIC_NETWORK_ID=8021
NEXT_PUBLIC_NETWORK_NAME=Numine
NEXT_PUBLIC_NETWORK_RPC_URL=https://subnets.avax.network/numi/mainnet/rpc
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/capyPuzzleBadge
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
NEXT_PUBLIC_STATS_API_HOST=https://numine.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
+2 -1
View File
@@ -53,8 +53,9 @@ NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=http://localhost:3100
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=http://localhost:3110
NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST=http://localhost:3111
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST=http://localhost:3012
NEXT_PUBLIC_MULTICHAIN_CLUSTER=test
NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST=http://localhost:3013
NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST=http://localhost:3014
NEXT_PUBLIC_MULTICHAIN_CLUSTER=test
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
@@ -22,10 +22,14 @@ async function fetchConfig() {
const url = baseUrl + '/assets/essential-dapps/chains.json';
const response = await fetch(url);
const json = await response.json();
value = json as MultichainConfig;
return value;
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const json = await response.json();
value = json as MultichainConfig;
return value;
}
}
}
export async function load() {
+8 -4
View File
@@ -22,10 +22,14 @@ async function fetchConfig() {
const url = baseUrl + '/assets/multichain/config.json';
const response = await fetch(url);
const json = await response.json();
value = json as MultichainConfig;
return value;
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const json = await response.json();
value = json as MultichainConfig;
return value;
}
}
}
export async function load() {
+286
View File
@@ -0,0 +1,286 @@
{
// Version of the setting file. Always 0.2
"version": "0.2",
// language - current active spelling language
"language": "en",
"useGitignore": true,
"ignorePaths": [
"**/*.svg",
"**/*.sfd",
".git",
"mocks/zetaChain/zetaChainCCTX.ts",
"mocks/metadata/address.ts",
"playwright/mocks/file_mock_with_very_long_name.json",
"playwright/fixtures/rewards.ts",
"public/static/capybara/index.js",
"ui/showcases/utils.ts",
"ui/tx/TxExternalTxs.pw.tsx"
],
"enableGlobDot": true,
"ignoreRandomStrings": true,
"allowCompoundWords": true,
"ignoreRegExpList": [
// Ignore filecoin f410f-like native addresses
"f410f[a-z2-7]{39}",
// Specify publisher key
"spk_\\w+",
// Posthog project key
"phc_\\w+",
// GitHub account names
"@(\\w|-)+"
],
// words - list of words to be always considered correct
"words": [
"aatx",
"abfnrtv",
"abitype",
"abkw",
"ACMRT",
"adbutler",
"addrs",
"adsbyslise",
"aeiou",
"Ahrefs",
"airtable",
"Alexa",
"alfajores",
"andb",
"anyblock",
"anytrust",
"apng",
"apos",
"appkit",
"Applebot",
"Arianee",
"ARTIS",
"astar",
"asymp",
"autoscout",
"Baiduspider",
"barooumba",
"basehead",
"bech",
"bedag",
"bingbot",
"bitquery",
"blackfort",
"blockie",
"blockies",
"blockscout",
"buildx",
"callvalue",
"campnetwork",
"CCIP",
"cctx",
"cctxs",
"celenium",
"celestia",
"CERC",
"cfasync",
"chainid",
"chainscout",
"chakra",
"clstr",
"coinzilla",
"coinzillatag",
"Computor",
"contentscript",
"contractname",
"convictional",
"Couldn",
"crios",
"dappscout",
"dbaeumer",
"deepdao",
"defi",
"devnet",
"didn",
"doesn",
"dotenv",
"DRPC",
"DTEND",
"duckduckgo",
"eamodio",
"Eigenda",
"Emelyanov",
"Enkrypt",
"explorable",
"facebookexternalhit",
"favicons",
"filecoin",
"flashblock",
"flashblocks",
"foos",
"fxios",
"geas",
"giga",
"gitpoap",
"Googlebot",
"goriunov",
"gotmpl",
"grecaptcha",
"growthbook",
"gstatic",
"hairsp",
"healthz",
"helia",
"hyfi",
"ICTT",
"identicons",
"IERC",
"Ihnatsyeu",
"Inde",
"inpage",
"internaltx",
"ipfs",
"iszero",
"jazzicon",
"jfif",
"jumpdest",
"jumpi",
"keccak",
"Kiryl",
"labelable",
"laquo",
"libc",
"libp",
"Liquality",
"llms",
"lokijs",
"LUKSO",
"mainnets",
"megaeth",
"merkle",
"metasuites",
"mgas",
"mload",
"mmss",
"msize",
"mulmod",
"multiarch",
"multicall",
"multichain",
"multisend",
"nbdash",
"NCAABB",
"ndash",
"negb",
"nextjs",
"noopener",
"noreferrer",
"noves",
"numine",
"nums",
"okex",
"okhttp",
"opblock",
"opengraph",
"paych",
"peekers",
"pino",
"pjpeg",
"posthog",
"PWDEBUG",
"pwstory",
"pyftsubset",
"qrcode",
"rabby",
"raleway",
"randao",
"raquo",
"rari",
"Rarible",
"RBTC",
"rdns",
"rdparty",
"regcred",
"remasc",
"reown",
"resizer",
"rgba",
"RIPEMD",
"rlespinasse",
"rogerbot",
"rollbar",
"ROLLUP",
"rollups",
"rubic",
"Sastana",
"schnorr",
"screencasts",
"scure",
"sdiv",
"SEMRESATTRS",
"Semrush",
"servedbyadbutler",
"Shavuha",
"Shavukha",
"shibarium",
"shibariumscan",
"SHVKH",
"sidechain",
"Signatur",
"simonsiefke",
"siwe",
"slise",
"sload",
"smartmuv",
"smod",
"smol",
"sokol",
"solidityscan",
"Sourcify",
"Succ",
"superchain",
"tabler",
"TBXN",
"testnetv",
"thinsp",
"Tokenary",
"tokenpocket",
"tokentxns",
"tonumber",
"trustwallet",
"Twitterbot",
"txns",
"typegen",
"uidotdev",
"unfinalized",
"UNKN",
"unparse",
"unstaked",
"usehooks",
"utia",
"utka",
"utko",
"UUPS",
"VCALENDAR",
"vercel",
"verifreg",
"VEVENT",
"viem",
"vitalik",
"vitest",
"wagmi",
"warpcast",
"watchlist",
"webacy",
"Winky",
"WOWG",
"WPOA",
"xdai",
"XDEFI",
"xname",
"xstar",
"yatki",
"zerion",
"zerossl",
"zetachain",
"zetavaloper",
"zilliqa",
"zkevm",
"zksolc",
"zksync",
"zora"
]
}
-1
View File
@@ -2,7 +2,6 @@
"name": "affected-tests",
"version": "1.0.0",
"main": "index.js",
"author": "Vasilii (tom) Goriunov <tom@ohhhh.me>",
"license": "MIT",
"dependencies": {
"dependency-tree": "10.0.9"
+3 -1
View File
@@ -103,7 +103,6 @@ const schema = yup
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: yup.boolean(),
NEXT_PUBLIC_ADVANCED_FILTER_ENABLED: yup.boolean(),
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(),
NEXT_PUBLIC_DEX_POOLS_ENABLED: yup.boolean()
.when('NEXT_PUBLIC_CONTRACT_INFO_API_HOST', {
is: (value: string) => Boolean(value),
@@ -150,6 +149,7 @@ const schema = yup
// Misc
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
NEXT_PUBLIC_API_KEYS_ALERT_MESSAGE: yup.string(),
})
.concat(apisSchema)
.concat(chainSchema)
@@ -159,11 +159,13 @@ const schema = yup
.concat(uiSchemas.footerSchema)
.concat(uiSchemas.miscSchema)
.concat(uiSchemas.viewsSchema)
.concat(featuresSchemas.accountSchema)
.concat(featuresSchemas.address3rdPartyWidgetsConfigSchema)
.concat(featuresSchemas.adsSchema)
.concat(featuresSchemas.apiDocsSchema)
.concat(featuresSchemas.beaconChainSchema)
.concat(featuresSchemas.bridgedTokensSchema)
.concat(featuresSchemas.crossChainTxsSchema)
.concat(featuresSchemas.defiDropdownSchema)
.concat(featuresSchemas.highlightsConfigSchema)
.concat(featuresSchemas.marketplaceSchema)
@@ -0,0 +1,22 @@
import { AuthProvider } from 'types/client/account';
import * as yup from 'yup';
export const accountSchema = yup
.object()
.shape({
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(),
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER: yup
.string<AuthProvider>()
.when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', {
is: (value: boolean) => value === true,
then: (schema) => schema.oneOf([ 'auth0', 'dynamic' ]),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not defined'),
}),
NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID: yup
.string()
.when('NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER', {
is: (value: AuthProvider) => value === 'dynamic',
then: (schema) => schema.required(),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID can only be used if NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER is set to \'dynamic\' '),
}),
});
@@ -0,0 +1,19 @@
import * as yup from 'yup';
import { urlTest } from '../../utils';
export const crossChainTxsSchema = yup
.object()
.shape({
NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED: yup.boolean(),
NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST: yup
.string()
.when('NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED', {
is: (value: boolean) => value,
then: (schema) => schema.test(urlTest),
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST can only be used with NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED',
value => value === undefined,
),
}),
});
@@ -1,8 +1,10 @@
export * from './account';
export * from './address3rdPartyWidgets';
export * from './ads';
export * from './apiDocs';
export * from './beaconChain';
export * from './bridgedToken';
export * from './crossChainTxs';
export * from './defiDropdown';
export * from './highlights';
export * from './marketplace';
@@ -10,3 +10,6 @@ NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED=false
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://example.com
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG={'img_url': {'small': 'https://example.com/promo-sm.png', 'large': 'https://example.com/promo-lg.png'}, 'link_url': 'https://example.com'}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic
NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID=xxx
@@ -0,0 +1,2 @@
NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED=true
NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST=https://api.example.com
@@ -0,0 +1,18 @@
[
{
"id": "8021",
"name": "Numine",
"explorer_url": "https://example.com/blockchain/numine",
"route_templates": {
"tx": "/transaction/{hash}",
"address": "/wallet/{hash}",
"token": "/address/{hash}"
}
},
{
"id": "43114",
"name": "C-Chain",
"logo": "https://example.com/logo.svg",
"explorer_url": "https://example.com/blockchain/c"
}
]
@@ -4,11 +4,10 @@ import { dirname, resolve as resolvePath } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Worker } from 'node:worker_threads';
import * as viemChains from 'viem/chains';
import { pick } from 'es-toolkit';
import { pick, uniq, delay } from 'es-toolkit';
import { EssentialDappsConfig } from 'types/client/marketplace';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
import { uniq } from 'es-toolkit';
import currentChainConfig from 'configs/app';
import appConfig from 'configs/app';
import { EssentialDappsChainConfig } from 'types/client/marketplace';
@@ -61,14 +60,15 @@ function trimChainConfig(config: typeof appConfig, logoUrl: string | undefined)
}
async function computeChainConfig(url: string): Promise<unknown> {
return new Promise((resolve, reject) => {
const workerPath = resolvePath(currentDir, 'worker.js');
const workerPath = resolvePath(currentDir, 'worker.js');
const worker = new Worker(workerPath, {
workerData: { url },
env: {} // Start with empty environment
});
const worker = new Worker(workerPath, {
workerData: { url },
env: {} // Start with empty environment
});
const controller = new AbortController();
const configPromise = new Promise((resolve, reject) => {
worker.on('message', (config) => {
resolve(config);
});
@@ -79,11 +79,17 @@ async function computeChainConfig(url: string): Promise<unknown> {
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${ code }`));
}
controller.abort();
reject(new Error(`Worker stopped with exit code ${ code }`));
});
});
return Promise.race([
configPromise,
delay(30_000, { signal: controller.signal })
]).finally(() => {
worker.terminate();
})
}
async function run() {
@@ -116,7 +122,15 @@ async function run() {
const explorerUrls = chainscoutInfo.externals.map(({ explorerUrl }) => explorerUrl).filter(Boolean);
console.log(`️ For ${ explorerUrls.length } chains explorer url was found in static config. Fetching parameters for each chain...`);
const chainConfigs = await Promise.all(explorerUrls.map(computeChainConfig)) as Array<typeof appConfig>;
const chainConfigs: Array<typeof appConfig> = [];
for (const explorerUrl of explorerUrls) {
const chainConfig = (await computeChainConfig(explorerUrl)) as typeof appConfig | undefined;
if (!chainConfig) {
throw new Error(`Failed to fetch chain config for ${ explorerUrl }`);
}
chainConfigs.push(chainConfig);
}
const result = {
chains: [ currentChainConfig, ...chainConfigs ].map((config) => {
@@ -2,8 +2,10 @@ 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';
import { delay } from 'es-toolkit';
import { ClusterChainConfig } from 'types/multichain';
import appConfig from 'configs/app';
const currentFilePath = fileURLToPath(import.meta.url);
const currentDir = dirname(currentFilePath);
@@ -48,14 +50,15 @@ async function getChainscoutInfo(chainIds: Array<string>) {
}
async function computeChainConfig(url: string): Promise<unknown> {
return new Promise((resolve, reject) => {
const workerPath = resolvePath(currentDir, 'worker.js');
const workerPath = resolvePath(currentDir, 'worker.js');
const worker = new Worker(workerPath, {
workerData: { url },
env: {} // Start with empty environment
});
const worker = new Worker(workerPath, {
workerData: { url },
env: {} // Start with empty environment
});
const controller = new AbortController();
const configPromise = new Promise((resolve, reject) => {
worker.on('message', (config) => {
resolve(config);
});
@@ -66,11 +69,18 @@ async function computeChainConfig(url: string): Promise<unknown> {
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${ code }`));
}
controller.abort();
reject(new Error(`Worker stopped with exit code ${ code }`));
});
});
return Promise.race([
configPromise,
delay(30_000, { signal: controller.signal })
]).finally(() => {
worker.terminate();
})
}
async function getExplorerUrls() {
@@ -105,15 +115,27 @@ async function run() {
throw new Error('No chains found in the cluster.');
}
const configs = await Promise.all(explorerUrls.map(computeChainConfig));
const chainscoutInfo = await getChainscoutInfo(configs.map((config) => config.chain.id));
const configs: Array<typeof appConfig> = [];
for (const url of explorerUrls) {
const chainConfig = (await computeChainConfig(url)) as typeof appConfig | undefined;
if (!chainConfig) {
throw new Error(`Failed to fetch chain config for ${ url }`);
}
configs.push(chainConfig);
}
const chainscoutInfo = await getChainscoutInfo(
configs
.map((config) => config.chain.id)
.filter((chainId) => chainId !== undefined)
);
const config = {
chains: configs.map((config, index) => {
const chainId = config.chain.id;
const chainName = (config as { chain: { name: string } })?.chain?.name ?? `Chain ${ chainId }`;
return {
id: chainId,
id: chainId || '',
name: chainName,
logo: chainscoutInfo.find((chain) => chain.id === chainId)?.logoUrl,
explorer_url: explorerUrls[index],
+18 -3
View File
@@ -112,7 +112,7 @@ Also, be aware that if you customize the name of the currency or any of its deno
| NEXT_PUBLIC_NETWORK_RPC_URL | `string \| Array<string>` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference. Can contain a single string value, or an array of urls. | - | - | `https://core.poa.network` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of the smallest unit of the native currency (e.g., 'wei' for Ethereum, where 1 ETH = 10^18 wei). Used for displaying gas prices and transaction fees in the smallest denomination. | - | `wei` | `duck` | v1.23.0+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_GWEI_NAME | `string` | Name of the giga-unit of the native currency (e.g., 'gwei' for Ethereum, where 1 gwei = 10^9 of the smallest unit). Used for displaying gas prices in a more readable format throughout the UI. | - | `gwei` | `gduck` | v2.5.0+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_GWEI_NAME | `string` | Name of the giga-unit of the native currency (e.g., 'gwei' for Ethereum, where 1 gwei = 10^9 of the smallest unit). Used for displaying gas prices in a more readable format throughout the UI. | - | `gwei` | `gDuck` | v2.5.0+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | v1.29.0+ |
@@ -380,6 +380,7 @@ Settings for meta tags, OG tags and SEO
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ |
| NEXT_PUBLIC_HIDE_NATIVE_COIN_PRICE | `boolean` | Set to `true` to hide the native coin price in the top bar | - | `false` | `true` | v2.4.0+ |
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ |
| NEXT_PUBLIC_API_KEYS_ALERT_MESSAGE | `string` | Used for displaying custom alerts on the API keys page. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v2.7.0+ |
| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ |
| NEXT_PUBLIC_COLOR_THEME_OVERRIDES | `string` | Color overrides for the default theme; pass a JSON-like string that represents a subset of the `DEFAULT_THEME_COLORS` object (see `toolkit/theme/foundations/colors.ts`) to customize the app's main colors. See [here](https://www.figma.com/design/4In0X8UADoZaTfZ34HaZ3K/Blockscout-design-system?node-id=29124-23813&t=XOv4ahHUSsTDlNkN-4) the Figma worksheet with description of available color tokens. | - | - | `{'text':{'primary':{'_light':{'value':'rgba(16,17,18,0.80)'},'_dark':{'value':'rgba(222,217,217)'}}}}` | v2.3.0+ |
| NEXT_PUBLIC_FONT_FAMILY_HEADING | `FontFamily`, see full description [below](#font-family-configuration-properties) | Special typeface to use in page headings (`<h1>`, `<h2>`, etc.) | - | - | `{'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}` | v1.35.0+ |
@@ -427,7 +428,9 @@ Settings for meta tags, OG tags and SEO
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](#google-recaptcha) | Required | - | `<your-secret>` | v1.0.x+ |
| NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER | `auth0 \| dynamic` | Auth provider that enables basic user authentication. | - | `auth0` | `dynamic` | upcoming |
| NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID | `string` | Environment ID of the Dynamic project. | Required, if provider is `dynamic` | - | `<your-secret>` | upcoming |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](#google-recaptcha) | Required, if provided is `auth0` | - | `<your-secret>` | v1.0.x+ |
&nbsp;
@@ -536,7 +539,7 @@ Ads are enabled by default on all self-hosted instances. If you would like to di
| NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ |
| NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED | `boolean` | Enables "Output roots" page (Optimistic stack only) | - | `false` | `true` | v1.37.0+ |
| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN | `ParentChain`, see details [below](#parent-chain-configuration-properties) | Configuration parameters for the parent chain. | - | - | `{'baseUrl':'https://explorer.duckchain.io'}` | v1.38.0+ |
| NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE | `string` | Hex-string for creating a link to the transaction batch on the Seleneium explorer. "0x"-format and 60 symbol length. Available only for Arbitrum roll-ups. | - | - | `0x00000000000000000000000000000000000000ca1de12a9905be97beaf` | v1.38.0+ |
| NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE | `string` | Hex-string for creating a link to the transaction batch on the [Celenium explorer](https://celenium.io). "0x"-format and 60 symbol length. Available only for Arbitrum roll-ups. | - | - | `0x00000000000000000000000000000000000000ca1de12a9905be97beaf` | v1.38.0+ |
| NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL | `string` | URL for the Selenium explorer. It is used to create links to the Data Availability Blobs page. The URL should contain the full path without any search parameters related to the blob, as these will be constructed at runtime for each blob separately. Available only for Optimistic or Arbitrum roll-ups. | - | - | `https://mocha.celenium.io/blob` | v2.0.2+ |
#### Parent chain configuration properties
@@ -1070,6 +1073,18 @@ This feature enables the application to act as an explorer of multiple blockchai
| NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST | `string` | Multichain aggregator API service host | Required | - | `https://multichain-aggregator.k8s-dev.blockscout.com` | v2.5.0+ |
| NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST | `string` | Multichain statistics API service host | Required | - | `http://multichain-search-stats.k8s-dev.blockscout.com` | v2.5.0+ |
&nbsp;
### Cross-chain transactions
This feature enables cross-chain transaction tracking and visualization, allowing users to view transactions that span multiple blockchains. It provides detailed information about cross-chain operations and links to related transactions on different chains.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED | `boolean` | The flag that enables the feature | Required | - | `true` | upcoming |
| NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST | `string` | Interchain indexer API service host used to fetch cross-chain transaction data and metadata | Required | - | `https://interchain-indexer.k8s-dev.blockscout.com` | upcoming |
&nbsp;
### Badge claim link
-10
View File
@@ -7,7 +7,6 @@ import consistentDefaultExportNamePlugin from 'eslint-plugin-consistent-default-
import importPlugin from 'eslint-plugin-import';
import importHelpersPlugin from 'eslint-plugin-import-helpers';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import noCyrillicStringPlugin from 'eslint-plugin-no-cyrillic-string';
import playwrightPlugin from 'eslint-plugin-playwright';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
@@ -325,15 +324,6 @@ export default tseslint.config(
},
},
{
plugins: {
'no-cyrillic-string': noCyrillicStringPlugin,
},
rules: {
'no-cyrillic-string/no-cyrillic-string': 'error',
},
},
{
plugins: {
'jsx-a11y': jsxA11yPlugin,
Vendored
+2 -2
View File
@@ -22,8 +22,8 @@ declare global {
};
abkw: string;
__envs: Record<string, string>;
__multichainConfig: MultichainConfig;
__essentialDappsChains: { chains: Array<EssentialDappsChainConfig> };
__multichainConfig?: MultichainConfig;
__essentialDappsChains?: { chains: Array<EssentialDappsChainConfig> };
}
namespace NodeJS {
+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 3.177H4.6a2.34 2.34 0 0 0-2.34 2.34v9.9a2.34 2.34 0 0 0 2.34 2.34h10.8a2.34 2.34 0 0 0 2.34-2.34v-9.9H19v9.9l-.005.186a3.6 3.6 0 0 1-3.41 3.41l-.185.004H4.6a3.6 3.6 0 0 1-3.6-3.6v-9.9a3.6 3.6 0 0 1 3.6-3.6H10v1.26ZM8.987 6.814c.098 0 .186.061.219.153l2.614 7.2a.234.234 0 0 1-.22.314h-.89a.235.235 0 0 1-.22-.158l-.638-1.828H7.015l-.639 1.828a.235.235 0 0 1-.22.158h-.892a.234.234 0 0 1-.22-.314l2.615-7.2a.233.233 0 0 1 .219-.153h1.11Zm4.913 0c.129 0 .233.104.233.233v7.2a.233.233 0 0 1-.233.234h-.841a.234.234 0 0 1-.234-.234v-7.2c0-.129.105-.233.234-.233h.841Zm-6.495 4.562h2.057L8.434 8.428l-1.029 2.948Zm7.9-9.324a.129.129 0 0 1 .24 0l.556 1.5a.13.13 0 0 0 .076.076l1.5.555a.13.13 0 0 1 0 .242l-1.5.555a.13.13 0 0 0-.076.076l-.555 1.5a.129.129 0 0 1-.241 0l-.556-1.5a.13.13 0 0 0-.076-.076l-1.5-.555a.13.13 0 0 1 0-.242l1.5-.555a.13.13 0 0 0 .076-.076l.556-1.5Zm-2.58-.645a.13.13 0 0 1 .243 0l.206.559a.13.13 0 0 0 .076.076l.56.207a.13.13 0 0 1 0 .242l-.56.206a.13.13 0 0 0-.076.077l-.206.558a.13.13 0 0 1-.242 0l-.207-.558a.13.13 0 0 0-.077-.077l-.558-.206a.13.13 0 0 1 0-.242l.558-.207a.13.13 0 0 0 .077-.076l.207-.559Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -2
View File
@@ -1,4 +1,3 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.26 17.5H4.027a.47.47 0 0 1-.47-.469V5.312c0-.26.21-.469.47-.469h2.137V2.969c0-.259.21-.469.468-.469h5.89l.015.003.014.003a.449.449 0 0 1 .14.03.463.463 0 0 1 .156.095l3.452 3.31-.002.002a.466.466 0 0 1 .146.337v7.3c0 .87-.708 1.577-1.577 1.577h-1.029v.766c0 .87-.707 1.577-1.577 1.577Zm2.823-11.69L12.99 3.794v1.379a.64.64 0 0 0 .638.639h1.454Zm-2.755-2.648H6.826v11.333h8.316a.64.64 0 0 0 .639-.64V6.474H13.63c-.87 0-1.302-.432-1.302-1.301v-2.01ZM6.164 5.505H4.22v11.333h8.316c.352 0 .639-.563.639-.915v-.766H6.632a.469.469 0 0 1-.468-.469V5.505ZM8.85 8.5a.25.25 0 1 0 0 .5h4.7a.25.25 0 1 0 0-.5h-4.7Zm-.25 1.95a.25.25 0 0 1 .25-.25h4.7a.25.25 0 1 1 0 .5h-4.7a.25.25 0 0 1-.25-.25ZM8.85 12a.25.25 0 1 0 0 .5h4.7a.25.25 0 1 0 0-.5h-4.7Z" fill="currentColor"/>
<path d="M6.164 4.843v.35h.35v-.35h-.35Zm6.373-2.34-.098.336h.003l.095-.336Zm0 0 .099-.336h-.003l-.095.336Zm.014.003.02-.35-.02.35Zm.123.025.12-.33h-.004l-.115.33Zm.017.005.101-.335-.101.335Zm.025.009.145-.319h-.002l-.143.319Zm.13.086.243-.253-.243.252ZM16.3 5.94l.247.248.253-.253-.258-.247-.242.252Zm-.002.002-.247-.248-.252.252.257.248.242-.252Zm-2.46 9.214v-.35h-.35v.35h.35Zm-.846-11.364.242-.252-.593-.572v.824h.35Zm2.092 2.018v.35h.867l-.624-.602-.243.252ZM6.826 3.162v-.35h-.35v.35h.35Zm5.502 0h.35v-.35h-.35v.35ZM6.826 14.495h-.35v.35h.35v-.35Zm8.955-8.022h.35v-.35h-.35v.35ZM4.22 5.505v-.35h-.35v.35h.35Zm1.944 0h.35v-.35h-.35v.35ZM4.22 16.838h-.35v.35h.35v-.35Zm8.955-1.68h.35v-.35h-.35v.35ZM4.027 17.85h8.233v-.7H4.027v.7Zm-.82-.819a.82.82 0 0 0 .82.819v-.7a.12.12 0 0 1-.12-.119h-.7Zm0-11.719v11.719h.7V5.312h-.7Zm.82-.819a.82.82 0 0 0-.82.819h.7a.12.12 0 0 1 .12-.119v-.7Zm2.137 0H4.027v.7h2.137v-.7Zm-.35-1.524v1.874h.7V2.969h-.7Zm.818-.819a.819.819 0 0 0-.818.819h.7c0-.066.053-.119.118-.119v-.7Zm5.89 0h-5.89v.7h5.89v-.7Zm.114.017s-.015-.005-.033-.008a.389.389 0 0 0-.081-.009v.7a.313.313 0 0 1-.065-.007c-.013-.002-.022-.006-.018-.004l.197-.672Zm-.003 0-.191.673.191-.674Zm-.063-.01a.308.308 0 0 1 .053.007l.013.003-.197.672.027.007a.39.39 0 0 0 .066.01l.038-.7Zm.22.043a.8.8 0 0 0-.218-.044l-.041.7a.1.1 0 0 1 .028.005l.231-.66Zm.002 0 .001.001-.237.659c.015.005.029.01.033.01l.203-.67Zm.067.025c-.032-.014-.061-.022-.067-.024l-.203.67h.004-.002a.208.208 0 0 1-.018-.007l.286-.639Zm.23.153a.812.812 0 0 0-.228-.152l-.29.637a.11.11 0 0 1 .033.02l.485-.505Zm3.452 3.31-3.452-3.31-.485.505 3.453 3.311.484-.505Zm.004.502.001-.001-.495-.495-.001.001.495.495Zm.248.09a.818.818 0 0 0-.253-.59l-.485.505a.117.117 0 0 1 .038.085h.7Zm0 7.3v-7.3h-.7v7.3h.7Zm-1.927 1.927a1.929 1.929 0 0 0 1.927-1.927h-.7c0 .677-.551 1.227-1.227 1.227v.7Zm-1.029 0h1.029v-.7h-1.029v.7Zm.35.416v-.766h-.7v.766h.7ZM12.26 17.85a1.929 1.929 0 0 0 1.927-1.927h-.7c0 .676-.55 1.227-1.227 1.227v.7Zm.488-13.805 2.092 2.018.486-.504-2.093-2.018-.486.504Zm.593 1.127v-1.38h-.7v1.38h.7Zm.288.289a.29.29 0 0 1-.288-.29h-.7a.99.99 0 0 0 .988.99v-.7Zm1.454 0h-1.454v.7h1.454v-.7ZM6.826 3.512h5.502v-.7H6.826v.7Zm.35 10.983V3.162h-.7v11.333h.7Zm7.966-.35H6.826v.7h8.316v-.7Zm.289-.29a.29.29 0 0 1-.29.29v.7a.99.99 0 0 0 .99-.99h-.7Zm0-7.382v7.383h.7V6.473h-.7Zm-1.801.35h2.15v-.7h-2.15v.7Zm-1.652-1.651c0 .488.122.918.427 1.224.306.306.737.427 1.225.427v-.7c-.382 0-.602-.095-.73-.222-.127-.127-.222-.348-.222-.73h-.7Zm0-2.01v2.01h.7v-2.01h-.7ZM4.22 5.855h1.944v-.7H4.22v.7Zm.35 10.983V5.505h-.7v11.333h.7Zm7.966-.35H4.22v.7h8.316v-.7Zm.289-.565a.862.862 0 0 1-.134.404c-.099.156-.167.16-.155.16v.7c.364 0 .615-.276.748-.489.143-.229.24-.52.24-.775h-.7Zm0-.766v.766h.7v-.766h-.7Zm-6.193.35h6.543v-.7H6.632v.7Zm-.818-.819c0 .452.365.82.818.82v-.7a.119.119 0 0 1-.118-.12h-.7Zm0-9.183v9.183h.7V5.505h-.7ZM8.95 8.75a.1.1 0 0 1-.1.1v-.7a.6.6 0 0 0-.6.6h.7Zm-.1-.1a.1.1 0 0 1 .1.1h-.7a.6.6 0 0 0 .6.6v-.7Zm4.7 0h-4.7v.7h4.7v-.7Zm-.1.1a.1.1 0 0 1 .1-.1v.7a.6.6 0 0 0 .6-.6h-.7Zm.1.1a.1.1 0 0 1-.1-.1h.7a.6.6 0 0 0-.6-.6v.7Zm-4.7 0h4.7v-.7h-4.7v.7Zm0 1a.6.6 0 0 0-.6.6h.7a.1.1 0 0 1-.1.1v-.7Zm4.7 0h-4.7v.7h4.7v-.7Zm.6.6a.6.6 0 0 0-.6-.6v.7a.1.1 0 0 1-.1-.1h.7Zm-.6.6a.6.6 0 0 0 .6-.6h-.7a.1.1 0 0 1 .1-.1v.7Zm-4.7 0h4.7v-.7h-4.7v.7Zm-.6-.6a.6.6 0 0 0 .6.6v-.7a.1.1 0 0 1 .1.1h-.7Zm.7 1.8a.1.1 0 0 1-.1.1v-.7a.6.6 0 0 0-.6.6h.7Zm-.1-.1a.1.1 0 0 1 .1.1h-.7a.6.6 0 0 0 .6.6v-.7Zm4.7 0h-4.7v.7h4.7v-.7Zm-.1.1a.1.1 0 0 1 .1-.1v.7a.6.6 0 0 0 .6-.6h-.7Zm.1.1a.1.1 0 0 1-.1-.1h.7a.6.6 0 0 0-.6-.6v.7Zm-4.7 0h4.7v-.7h-4.7v.7Z" fill="currentColor"/>
<path d="M18.942 4.505c-.045-.107-1.15-2.617-4.799-2.617A5.336 5.336 0 0 0 10 3.546a5.333 5.333 0 0 0-4.144-1.658c-3.649 0-4.755 2.51-4.8 2.617L1 4.78v12.674l1.36.284c.032-.07.82-1.758 3.496-1.758 2.674 0 3.462 1.686 3.491 1.751l1.304.006c.032-.07.82-1.757 3.494-1.757 2.674 0 3.463 1.686 3.492 1.751L19 17.455V4.781l-.058-.276ZM17.58 15.58a5.677 5.677 0 0 0-3.436-1.018A5.347 5.347 0 0 0 10 16.216a5.347 5.347 0 0 0-4.144-1.654 5.677 5.677 0 0 0-3.437 1.018V4.953c.226-.391 1.13-1.646 3.437-1.646 2.306 0 3.214 1.261 3.434 1.645v8.955h1.42V4.952c.226-.39 1.129-1.645 3.435-1.645 2.307 0 3.216 1.261 3.436 1.645V15.58Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 724 B

-10
View File
@@ -1,10 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.111 21H4.891a.674.674 0 0 1-.673-.674v-9.698a.674.674 0 0 1 .674-.674h14.22a.674.674 0 0 1 .673.674v9.698a.674.674 0 0 1-.674.674ZM5.566 19.652h12.871v-8.35H5.566v8.35Z" fill="currentColor"/>
<path d="M19.111 21H4.891a.674.674 0 0 1-.673-.674v-9.698a.674.674 0 0 1 .674-.674h14.22a.674.674 0 0 1 .673.674v9.698a.674.674 0 0 1-.674.674ZM5.566 19.652h12.871v-8.35H5.566v8.35Z" fill="currentColor"/>
<path d="M20.328 11.302H3.674A.674.674 0 0 1 3 10.628V7.006a.674.674 0 0 1 .674-.674h16.654a.674.674 0 0 1 .675.674v3.622a.674.674 0 0 1-.675.674ZM4.348 9.954h15.307V7.68H4.348v2.274Z" fill="currentColor"/>
<path d="M20.328 11.302H3.674A.674.674 0 0 1 3 10.628V7.006a.674.674 0 0 1 .674-.674h16.654a.674.674 0 0 1 .675.674v3.622a.674.674 0 0 1-.675.674ZM4.348 9.954h15.307V7.68H4.348v2.274Z" fill="currentColor"/>
<path d="M12 7.68a.674.674 0 0 1-.415-.144l-3.903-3.07a.55.55 0 0 0-.89.432v2.108a.674.674 0 1 1-1.348 0V4.898a1.898 1.898 0 0 1 3.071-1.491l3.902 3.07A.674.674 0 0 1 12 7.68Z" fill="currentColor"/>
<path d="M12 7.68a.674.674 0 0 1-.415-.144l-3.903-3.07a.55.55 0 0 0-.89.432v2.108a.674.674 0 1 1-1.348 0V4.898a1.898 1.898 0 0 1 3.071-1.491l3.902 3.07A.674.674 0 0 1 12 7.68Z" fill="currentColor"/>
<path d="M12.001 7.68a.674.674 0 0 1-.416-1.204l3.901-3.07a1.898 1.898 0 0 1 3.072 1.492v2.108a.674.674 0 1 1-1.348 0V4.898a.55.55 0 0 0-.89-.432l-3.902 3.07A.674.674 0 0 1 12 7.68Z" fill="currentColor"/>
<path d="M12.001 7.68a.674.674 0 0 1-.416-1.204l3.901-3.07a1.898 1.898 0 0 1 3.072 1.492v2.108a.674.674 0 1 1-1.348 0V4.898a.55.55 0 0 0-.89-.432l-3.902 3.07A.674.674 0 0 1 12 7.68Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 1a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h12ZM4.2 2.2a2 2 0 0 0-2 2v11.6a2 2 0 0 0 2 2h11.6a2 2 0 0 0 2-2V4.2a2 2 0 0 0-2-2H4.2Z" fill="currentColor"/>
<rect x="4" y="5" width="12" height="1.4" rx=".7" fill="currentColor"/>
<rect x="4" y="8" width="8" height="1.4" rx=".7" fill="currentColor"/>
<rect x="4" y="10.801" width="12" height="1.4" rx=".7" fill="currentColor"/>
<rect x="4" y="13.6" width="9" height="1.4" rx=".7" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 572 B

+9
View File
@@ -10,6 +10,12 @@ import { CONTRACT_INFO_API_RESOURCES } from './services/contractInfo';
import type { ContractInfoApiPaginationFilters, ContractInfoApiResourceName, ContractInfoApiResourcePayload } from './services/contractInfo';
import { GENERAL_API_RESOURCES } from './services/general';
import type { GeneralApiResourceName, GeneralApiResourcePayload, GeneralApiPaginationFilters, GeneralApiPaginationSorting } from './services/general';
import type {
InterchainIndexerApiPaginationFilters,
InterchainIndexerApiResourceName,
InterchainIndexerApiResourcePayload,
} from './services/interchainIndexer';
import { INTERCHAIN_INDEXER_API_RESOURCES } from './services/interchainIndexer';
import type { MetadataApiResourceName, MetadataApiResourcePayload } from './services/metadata';
import { METADATA_API_RESOURCES } from './services/metadata';
import type {
@@ -43,6 +49,7 @@ export const RESOURCES = {
clusters: CLUSTERS_API_RESOURCES,
contractInfo: CONTRACT_INFO_API_RESOURCES,
general: GENERAL_API_RESOURCES,
interchainIndexer: INTERCHAIN_INDEXER_API_RESOURCES,
metadata: METADATA_API_RESOURCES,
multichainAggregator: MULTICHAIN_AGGREGATOR_API_RESOURCES,
multichainStats: MULTICHAIN_STATS_API_RESOURCES,
@@ -79,6 +86,7 @@ R extends BensApiResourceName ? BensApiResourcePayload<R> :
R extends ClustersApiResourceName ? ClustersApiResourcePayload<R> :
R extends ContractInfoApiResourceName ? ContractInfoApiResourcePayload<R> :
R extends GeneralApiResourceName ? GeneralApiResourcePayload<R> :
R extends InterchainIndexerApiResourceName ? InterchainIndexerApiResourcePayload<R> :
R extends MetadataApiResourceName ? MetadataApiResourcePayload<R> :
R extends MultichainAggregatorApiResourceName ? MultichainAggregatorApiResourcePayload<R> :
R extends MultichainStatsApiResourceName ? MultichainStatsApiResourcePayload<R> :
@@ -118,6 +126,7 @@ R extends BensApiResourceName ? BensApiPaginationFilters<R> :
R extends ClustersApiResourceName ? ClustersApiPaginationFilters :
R extends GeneralApiResourceName ? GeneralApiPaginationFilters<R> :
R extends ContractInfoApiResourceName ? ContractInfoApiPaginationFilters<R> :
R extends InterchainIndexerApiResourceName ? InterchainIndexerApiPaginationFilters<R> :
R extends MultichainAggregatorApiResourceName ? MultichainAggregatorApiPaginationFilters<R> :
R extends TacOperationLifecycleApiResourceName ? TacOperationLifecycleApiPaginationFilters<R> :
R extends ZetaChainApiResourceName ? ZetaChainApiPaginationFilters<R> :
+3
View File
@@ -58,6 +58,9 @@ export const GENERAL_API_ACCOUNT_RESOURCES = {
auth_logout: {
path: '/api/account/auth/logout',
},
auth_dynamic: {
path: '/api/account/v2/authenticate_via_dynamic',
},
} satisfies Record<string, ApiResource>;
export type GeneralApiAccountResourceName = `general:${ keyof typeof GENERAL_API_ACCOUNT_RESOURCES }`;
+71
View File
@@ -0,0 +1,71 @@
import type { ApiResource } from '../types';
import type * as interchainIndexer from '@blockscout/interchain-indexer-types';
import type { CrossChainMessageFilters, CrossChainTransferFilters } from 'types/api/interchainIndexer';
export const INTERCHAIN_INDEXER_API_RESOURCES = {
messages: {
path: '/api/v1/interchain/messages',
filterFields: [ 'q' as const ],
paginated: true,
},
message: {
path: '/api/v1/interchain/messages/:id',
pathParams: [ 'id' as const ],
},
tx_messages: {
path: '/api/v1/interchain/messages\\:byTx/:hash',
pathParams: [ 'hash' as const ],
filterFields: [ 'q' as const ],
paginated: true,
},
address_messages: {
path: '/api/v1/interchain/messages\\:byAddress/:hash',
pathParams: [ 'hash' as const ],
paginated: true,
},
transfers: {
path: '/api/v1/interchain/transfers',
filterFields: [ 'q' as const ],
paginated: true,
},
tx_transfers: {
path: '/api/v1/interchain/transfers\\:byTx/:hash',
pathParams: [ 'hash' as const ],
filterFields: [ 'q' as const ],
paginated: true,
},
address_transfers: {
path: '/api/v1/interchain/transfers\\:byAddress/:hash',
pathParams: [ 'hash' as const ],
paginated: true,
},
stats_daily: {
path: '/api/v1/stats/daily',
},
stats_common: {
path: '/api/v1/stats/common',
},
} satisfies Record<string, ApiResource>;
export type InterchainIndexerApiResourceName = `interchainIndexer:${ keyof typeof INTERCHAIN_INDEXER_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type InterchainIndexerApiResourcePayload<R extends InterchainIndexerApiResourceName> =
R extends 'interchainIndexer:messages' ? interchainIndexer.GetMessagesResponse :
R extends 'interchainIndexer:message' ? interchainIndexer.InterchainMessage :
R extends 'interchainIndexer:tx_messages' ? interchainIndexer.GetMessagesResponse :
R extends 'interchainIndexer:address_messages' ? interchainIndexer.GetMessagesResponse :
R extends 'interchainIndexer:transfers' ? interchainIndexer.GetTransfersResponse :
R extends 'interchainIndexer:tx_transfers' ? interchainIndexer.GetTransfersResponse :
R extends 'interchainIndexer:address_transfers' ? interchainIndexer.GetTransfersResponse :
R extends 'interchainIndexer:stats_daily' ? interchainIndexer.GetDailyStatisticsResponse :
R extends 'interchainIndexer:stats_common' ? interchainIndexer.GetCommonStatisticsResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type InterchainIndexerApiPaginationFilters<R extends InterchainIndexerApiResourceName> =
R extends 'interchainIndexer:messages' ? CrossChainMessageFilters :
R extends 'interchainIndexer:transfers' ? CrossChainTransferFilters :
never;
/* eslint-enable @stylistic/indent */
+1 -1
View File
@@ -1,5 +1,5 @@
export type ApiName =
'general' | 'admin' | 'bens' | 'contractInfo' | 'clusters' | 'external' |
'general' | 'admin' | 'bens' | 'contractInfo' | 'clusters' | 'external' | 'interchainIndexer' |
'metadata' | 'multichainAggregator' | 'multichainStats' | 'rewards' | 'stats' | 'tac' |
'userOps' | 'visualize' | 'zetachain';
+5
View File
@@ -0,0 +1,5 @@
import type React from 'react';
export const FallbackProvider = ({ children }: { children: React.ReactNode }) => {
return children;
};
+1
View File
@@ -26,6 +26,7 @@ export enum NAMES {
UUID = 'uuid',
SHOW_SCAM_TOKENS = 'show_scam_tokens',
APP_PROFILE = 'app_profile',
TABLE_VIEW_ON_MOBILE = 'table_view_on_mobile',
}
/**
+3 -3
View File
@@ -39,8 +39,8 @@ dayjs.extend(utc);
dayjs.updateLocale('en', {
formats: {
llll: `MMM DD YYYY HH:mm:ss A (Z${ nbsp }UTC)`,
lll: 'MMM D, YYYY h:mm A',
llll: `MMM DD YYYY HH:mm:ss (Z${ nbsp }UTC)`,
lll: 'MMM D, YYYY H:mm',
},
relativeTime: {
s: '1s',
@@ -68,5 +68,5 @@ export default dayjs;
export const FORMATS = {
// the "lll" format with seconds
lll_s: 'MMM D, YYYY h:mm:ss A',
lll_s: 'MMM D, YYYY H:mm:ss',
};
+8
View File
@@ -0,0 +1,8 @@
export default function getErrorStack(error: Error | undefined): string | undefined {
return (
error && 'stack' in error &&
typeof error.stack === 'string' && error.stack !== null &&
error.stack
) ||
undefined;
}
+1
View File
@@ -7,6 +7,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
export interface GrowthBookFeatures {
test_value: string;
txns_view_exp: 'table_view' | 'list_view';
}
export const initGrowthBook = (uuid: string) => {
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
import * as cookies from 'lib/cookies';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import * as mixpanel from 'lib/mixpanel';
export default function useTableViewValue() {
const cookieValue = cookies.get(cookies.NAMES.TABLE_VIEW_ON_MOBILE);
const [ value, setValue ] = React.useState<boolean | undefined>(cookieValue ? cookieValue === 'true' : undefined);
const { value: featureFlag, isLoading: isFeatureLoading } = useFeatureValue('txns_view_exp', 'list_view');
const onToggle = React.useCallback(() => {
setValue((prev) => {
const nextValue = !prev;
cookies.set(cookies.NAMES.TABLE_VIEW_ON_MOBILE, nextValue ? 'true' : 'false');
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, {
Type: 'Txn view switch',
Info: nextValue ? 'Table view' : 'List view',
Source: 'Address page',
});
return nextValue;
});
}, []);
React.useEffect(() => {
if (!isFeatureLoading) {
setValue((prev) => {
if (prev === undefined) {
return featureFlag === 'table_view';
}
return prev;
});
}
}, [ featureFlag, isFeatureLoading ]);
return React.useMemo(() => {
if (value !== undefined) {
return {
value,
isLoading: false,
onToggle,
};
}
return { value: featureFlag === 'table_view', isLoading: isFeatureLoading, onToggle };
}, [ featureFlag, isFeatureLoading, onToggle, value ]);
}
+1
View File
@@ -69,6 +69,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/operations': 'Root page',
'/operation/[id]': 'Regular page',
'/cc/tx/[hash]': 'Regular page',
'/cross-chain-tx/[id]': 'Regular page',
// multichain routes
'/chain/[chain_slug]/accounts/label/[slug]': 'Root page',
+1
View File
@@ -72,6 +72,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/operations': DEFAULT_TEMPLATE,
'/operation/[id]': DEFAULT_TEMPLATE,
'/cc/tx/[hash]': DEFAULT_TEMPLATE,
'/cross-chain-tx/[id]': DEFAULT_TEMPLATE,
// multichain routes
'/chain/[chain_slug]/accounts/label/[slug]': DEFAULT_TEMPLATE,
+1
View File
@@ -73,6 +73,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/operations': '%network_name% operations',
'/operation/[id]': '%network_name% operation %id%',
'/cc/tx/[hash]': '%network_name% cross-chain transaction %hash% details',
'/cross-chain-tx/[id]': '%network_name% cross-chain transaction %id% details',
// multichain routes
'/chain/[chain_slug]/accounts/label/[slug]': '%network_name% addresses search by label',
+1
View File
@@ -67,6 +67,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/operations': 'Operations',
'/operation/[id]': 'Operation details',
'/cc/tx/[hash]': 'Cross-chain transaction details',
'/cross-chain-tx/[id]': 'Cross-chain transaction details',
// multichain routes
'/chain/[chain_slug]/accounts/label/[slug]': 'Chain addresses search by label',
+3 -3
View File
@@ -15,7 +15,7 @@ import * as userProfile from './userProfile';
const opSuperchainFeature = config.features.opSuperchain;
export default function useMixpanelInit() {
const [ isInited, setIsInited ] = React.useState(false);
const [ isInitialized, setIsInitialized ] = React.useState(false);
const router = useRouter();
const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug));
@@ -57,11 +57,11 @@ export default function useMixpanelInit() {
'First Time Join': dayjs().toISOString(),
});
setIsInited(true);
setIsInitialized(true);
if (debugFlagQuery.current && !debugFlagCookie) {
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
}
}, [ ]);
return isInited;
return isInitialized;
}
+3 -3
View File
@@ -15,7 +15,7 @@ import getTabName from './getTabName';
import logEvent from './logEvent';
import { EventTypes } from './utils';
export default function useLogPageView(isInited: boolean) {
export default function useLogPageView(isInitialized: boolean) {
const router = useRouter();
const pathname = usePathname();
@@ -25,7 +25,7 @@ export default function useLogPageView(isInited: boolean) {
const { colorMode } = useColorMode();
React.useEffect(() => {
if (!config.features.mixpanel.isEnabled || !isInited) {
if (!config.features.mixpanel.isEnabled || !isInitialized) {
return;
}
@@ -44,5 +44,5 @@ export default function useLogPageView(isInited: boolean) {
// but we still want to log page view
// so we use pathname from 'next/navigation' instead of router.pathname from 'next/router' as deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isInited, page, pathname, tab, colorMode ]);
}, [ isInitialized, page, pathname, tab, colorMode ]);
}
+5 -1
View File
@@ -73,7 +73,7 @@ Type extends EventTypes.LOGIN ? (
Source: 'Email';
} | {
Action: 'Success';
Source: 'Email' | 'Wallet';
Source: 'Email' | 'Wallet' | 'Dynamic';
}
) :
Type extends EventTypes.ACCOUNT_LINK_INFO ? {
@@ -147,6 +147,10 @@ Type extends EventTypes.PAGE_WIDGET ? (
Type: 'Chain switch';
Info: string;
Source: 'Revoke essential dapp';
} | {
Type: 'Txn view switch';
Info: 'Table view' | 'List view';
Source: 'Address page';
}
) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
+40
View File
@@ -0,0 +1,40 @@
import { useDynamicContext, useUserWallets } from '@dynamic-labs/sdk-react-core';
import React from 'react';
import type { UseAccountReturnType } from 'wagmi';
export default function useAccountDynamic(): UseAccountReturnType {
const userWallets = useUserWallets();
const { primaryWallet } = useDynamicContext();
const address = (primaryWallet?.address || userWallets[0]?.address) as `0x${ string }` | undefined;
return React.useMemo(() => {
if (!address) {
return {
address: undefined,
addresses: undefined,
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: false,
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
status: 'disconnected',
};
}
return {
address,
addresses: [ address ],
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: true,
isConnecting: false,
isDisconnected: false,
isReconnecting: false,
status: 'connected',
} as unknown as UseAccountReturnType;
}, [ address ]);
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import type { UseAccountReturnType } from 'wagmi';
export default function useAccountFallback(): UseAccountReturnType {
return React.useMemo(() => ({
address: undefined,
addresses: undefined,
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: false,
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
status: 'disconnected',
}), []);
}
+10 -3
View File
@@ -4,7 +4,11 @@ import appConfig from 'configs/app';
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
import multichainConfig from 'configs/multichain';
const getChainInfo = (config: Partial<typeof appConfig> = appConfig, contracts?: Chain['contracts']): Chain | undefined => {
const getChainInfo = (
config: Partial<typeof appConfig> = appConfig,
contracts?: Chain['contracts'],
logoUrl?: string,
): Chain | undefined => {
if (!config.chain || !config.app) {
return;
}
@@ -30,6 +34,9 @@ const getChainInfo = (config: Partial<typeof appConfig> = appConfig, contracts?:
},
testnet: config.chain.isTestnet,
contracts,
custom: {
logoUrl: logoUrl ?? config.UI?.navigation.icon.default,
},
};
};
@@ -74,7 +81,7 @@ export const clusterChains: Array<Chain> | undefined = (() => {
return;
}
return config.chains.map(({ app_config: config }) => getChainInfo(config)).filter(Boolean);
return config.chains.map(({ app_config: config, logo }) => getChainInfo(config, undefined, logo)).filter(Boolean);
})();
export const essentialDappsChains: Array<Chain> | undefined = (() => {
@@ -84,7 +91,7 @@ export const essentialDappsChains: Array<Chain> | undefined = (() => {
return;
}
return config.chains.map(({ app_config: config, contracts }) => getChainInfo(config, contracts)).filter(Boolean);
return config.chains.map(({ app_config: config, contracts, logo }) => getChainInfo(config, contracts, logo)).filter(Boolean);
})();
export const chains = (() => {
+8 -19
View File
@@ -1,23 +1,12 @@
import type { UseAccountReturnType } from 'wagmi';
import { useAccount } from 'wagmi';
import config from 'configs/app';
function useAccountFallback(): UseAccountReturnType {
return {
address: undefined,
addresses: undefined,
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: false,
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
status: 'disconnected',
};
}
const feature = config.features.blockchainInteraction;
const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback;
// eslint-disable-next-line no-nested-ternary
const useAccount = (feature.isEnabled && feature.connectorType === 'dynamic') ?
(await import('./account/useAccountDynamic')).default :
(feature.isEnabled && feature.connectorType === 'reown') ?
(await import('wagmi')).useAccount :
(await import('./account/useAccountFallback')).default;
export default hook;
export default useAccount;
+9 -60
View File
@@ -1,63 +1,12 @@
import { useAppKit, useAppKitState } from '@reown/appkit/react';
import React from 'react';
import { useDisconnect, useAccountEffect } from 'wagmi';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import useAccount from 'lib/web3/useAccount';
const feature = config.features.blockchainInteraction;
interface Params {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
onConnect?: () => void;
}
// eslint-disable-next-line no-nested-ternary
const useWallet = (feature.isEnabled && feature.connectorType === 'dynamic') ?
(await import('./wallet/useWalletDynamic')).default :
(feature.isEnabled && feature.connectorType === 'reown') ?
(await import('./wallet/useWalletReown')).default :
(await import('./wallet/useWalletFallback')).default;
export default function useWeb3Wallet({ source, onConnect }: Params) {
const { open: openModal } = useAppKit();
const { open: isOpen } = useAppKitState();
const { disconnect } = useDisconnect();
const [ isOpening, setIsOpening ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
const isConnectionStarted = React.useRef(false);
React.useEffect(() => {
setIsClientLoaded(true);
}, []);
const handleConnect = React.useCallback(async() => {
setIsOpening(true);
await openModal();
setIsOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
isConnectionStarted.current = true;
}, [ openModal, source ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
if (!isReconnected && isConnectionStarted.current) {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
mixpanel.userProfile.setOnce({
'With Connected Wallet': true,
});
onConnect?.();
}
isConnectionStarted.current = false;
}, [ source, onConnect ]);
const handleDisconnect = React.useCallback(() => {
disconnect();
}, [ disconnect ]);
useAccountEffect({ onConnect: handleAccountConnected });
const account = useAccount();
const address = account.address;
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
return React.useMemo(() => ({
connect: handleConnect,
disconnect: handleDisconnect,
isOpen: isOpening || isOpen,
isConnected,
isReconnecting: account.isReconnecting,
address,
openModal,
}), [ handleConnect, handleDisconnect, isOpening, isOpen, isConnected, account.isReconnecting, address, openModal ]);
}
export default useWallet;
+4 -3
View File
@@ -19,7 +19,7 @@ const getChainTransportFromConfig = (config: Partial<typeof appConfig> | undefin
return {
[config.chain.id]: fallback(
config.chain.rpcUrls
.concat(readOnly && config.apis?.general ? `${ config.apis.general.endpoint }/api/eth-rpc` : '')
.concat(readOnly && config.apis?.general ? `${ config.apis.general.endpoint }${ config.apis.general.basePath ?? '' }/api/eth-rpc` : '')
.filter(Boolean)
.map((url) => http(url, { batch: { wait: 100, batchSize: 5 } })),
),
@@ -47,7 +47,7 @@ const reduceExternalChainsToTransportConfig = (readOnly: boolean): Record<string
const wagmi = (() => {
if (!feature.isEnabled) {
if (!feature.isEnabled || feature.connectorType === 'dynamic') {
const wagmiConfig = createConfig({
chains: chains as [Chain, ...Array<Chain>],
transports: {
@@ -57,6 +57,7 @@ const wagmi = (() => {
},
ssr: true,
batch: { multicall: { wait: 100, batchSize: 5 } },
multiInjectedProviderDiscovery: feature.isEnabled && feature.connectorType === 'dynamic' ? false : true,
});
return { config: wagmiConfig, adapter: null };
@@ -70,7 +71,7 @@ const wagmi = (() => {
...(parentChain ? { [parentChain.id]: http() } : {}),
...reduceExternalChainsToTransportConfig(false),
},
projectId: feature.walletConnect.projectId,
projectId: feature.reown.projectId,
ssr: true,
batch: { multicall: { wait: 100, batchSize: 5 } },
syncConnectedChain: false,
+16
View File
@@ -0,0 +1,16 @@
import type * as mixpanel from 'lib/mixpanel/index';
export interface Params {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
onConnect?: () => void;
}
export interface Result {
connect: () => void;
disconnect: () => void;
isOpen: boolean;
isConnected: boolean;
isReconnecting: boolean;
address: string | undefined;
openModal: () => void;
}
+72
View File
@@ -0,0 +1,72 @@
import { useDynamicContext, useDynamicEvents, useUserWallets } from '@dynamic-labs/sdk-react-core';
import React from 'react';
import { useAccountEffect } from 'wagmi';
import type { Params, Result } from './types';
import * as mixpanel from 'lib/mixpanel/index';
import useAccountDynamic from '../account/useAccountDynamic';
export default function useWalletDynamic({ source, onConnect }: Params): Result {
const isConnectionStarted = React.useRef(false);
const [ isOpen ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
const { setShowDynamicUserProfile, setShowAuthFlow, setAuthMode, removeWallet } = useDynamicContext();
const openModal = React.useCallback(() => {
setShowDynamicUserProfile(true);
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_ACCESS, { Action: 'Dropdown open' });
}, [ setShowDynamicUserProfile ]);
useDynamicEvents('authFlowOpen', async() => {
if (!isConnectionStarted.current) {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
isConnectionStarted.current = true;
}
});
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
if (!isReconnected && isConnectionStarted.current) {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
mixpanel.userProfile.setOnce({
'With Connected Wallet': true,
});
onConnect?.();
}
isConnectionStarted.current = false;
}, [ source, onConnect ]);
const handleConnect = React.useCallback(() => {
setAuthMode('connect-only');
setShowAuthFlow(true);
}, [ setAuthMode, setShowAuthFlow ]);
const userWallets = useUserWallets();
const primaryWalletId = userWallets[0]?.id;
const handleDisconnect = React.useCallback(() => {
primaryWalletId && removeWallet(primaryWalletId);
}, [ primaryWalletId, removeWallet ]);
useAccountEffect({ onConnect: handleAccountConnected });
React.useEffect(() => {
setIsClientLoaded(true);
}, []);
const account = useAccountDynamic();
const address = account.address;
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
return React.useMemo(() => ({
connect: handleConnect,
disconnect: handleDisconnect,
isOpen,
isConnected,
isReconnecting: false,
address,
openModal,
}), [ handleConnect, handleDisconnect, isOpen, isConnected, address, openModal ]);
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
import type { Result } from './types';
export default function useWalletFallback(): Result {
return React.useMemo(() => ({
connect: () => {},
disconnect: () => {},
isOpen: false,
isConnected: false,
isReconnecting: false,
address: undefined,
openModal: () => {},
}), []);
}
+59
View File
@@ -0,0 +1,59 @@
import { useAppKit, useAppKitState } from '@reown/appkit/react';
import React from 'react';
import { useAccountEffect, useAccount, useDisconnect } from 'wagmi';
import type { Params, Result } from './types';
import * as mixpanel from 'lib/mixpanel/index';
export default function useWalletReown({ source, onConnect }: Params): Result {
const { open: openModal } = useAppKit();
const { open: isOpen } = useAppKitState();
const { disconnect } = useDisconnect();
const [ isOpening, setIsOpening ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
const isConnectionStarted = React.useRef(false);
React.useEffect(() => {
setIsClientLoaded(true);
}, []);
const handleConnect = React.useCallback(async() => {
setIsOpening(true);
await openModal();
setIsOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
isConnectionStarted.current = true;
}, [ openModal, source ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
if (!isReconnected && isConnectionStarted.current) {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
mixpanel.userProfile.setOnce({
'With Connected Wallet': true,
});
onConnect?.();
}
isConnectionStarted.current = false;
}, [ source, onConnect ]);
const handleDisconnect = React.useCallback(() => {
disconnect();
}, [ disconnect ]);
useAccountEffect({ onConnect: handleAccountConnected });
const account = useAccount();
const address = account.address;
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
return React.useMemo(() => ({
connect: handleConnect,
disconnect: handleDisconnect,
isOpen: isOpening || isOpen,
isConnected,
isReconnecting: account.isReconnecting,
address,
openModal,
}), [ handleConnect, handleDisconnect, isOpening, isOpen, isConnected, account.isReconnecting, address, openModal ]);
}
+1 -1
View File
@@ -218,7 +218,7 @@ export const withBlobTxs: Block = {
blob_gas_used: '393216',
burnt_blob_fees: '8461393325064192',
excess_blob_gas: '79429632',
blob_transaction_count: 1,
blob_transactions_count: 1,
};
export const withWithdrawals: Block = {
+27
View File
@@ -0,0 +1,27 @@
import type { ExternalChain } from 'types/externalChains';
export const config: Array<ExternalChain> = [
{
id: '43114',
name: 'C-Chain',
logo: 'https://c-chain.com/logo.svg',
explorer_url: 'https://c-chain.com/explorer',
route_templates: {
tx: '/transaction/{hash}',
address: '/wallet/{hash}',
token: '/address/{hash}',
},
},
{
id: '8021',
name: 'Numine',
logo: 'https://numine.com/logo.svg',
explorer_url: 'https://numine.com',
},
{
id: '4444',
name: 'Duck chain',
logo: undefined,
explorer_url: 'https://duck.com',
},
];
+73
View File
@@ -0,0 +1,73 @@
import type { GetTransfersResponse, InterchainTransfer } from '@blockscout/interchain-indexer-types';
import { MessageStatus } from '@blockscout/interchain-indexer-types';
import { config } from './config';
export const transferA = {
bridge: {
name: 'Avalanche ICTT',
ui_url: 'https://app.avax.network/',
},
message_id: '0x057b42bbbfbb4900e155a554ae67632cb21e6f5a64d815fcad7f33abe552c059',
status: MessageStatus.MESSAGE_STATUS_COMPLETED,
source_chain: config[0],
destination_chain: config[1],
source_token: {
address_hash: '0x33a31e0f62c0ddf25090b61ef21a70d5f48725b7',
name: 'Wrapped AVAX',
symbol: 'WAVAX',
decimals: '18',
icon_url: 'https://app.avax.network/logo.svg',
},
source_amount: '509700000000000000',
source_transaction_hash: '0x866a70cb1c8c33d259c819473d7b419c0de67770755bf07dee14dd2d0c6dc8ab',
sender: {
hash: '0xd7e63822d0e386fb65f28f41e8c1aa8d844ba018',
ens_domain_name: 'kitty.kitty.kitty.kitty.cat.eth',
},
send_timestamp: '2022-01-13T12:06:24.000Z',
destination_token: {
address_hash: '0x012cb6651cb29c7d5dc96173756a773f7fb87cfb',
name: 'Wrapped AVAX',
symbol: 'WAVAX',
decimals: '18',
},
destination_amount: '509700000000000000',
destination_transaction_hash: '0xdbdf690cfde8af2ee855bb90bfa9977a2d8ba36c9ae1a2010c67fe3774832213',
recipient: {
hash: '0xd7e63822d0e386fb65f28f41e8c1aa8d844ba018',
},
receive_timestamp: '2022-01-13T12:06:30.000Z',
} satisfies InterchainTransfer;
export const transferB = {
...transferA,
source_chain: {
id: '420',
name: 'Unknown chain',
logo: undefined,
explorer_url: undefined,
},
source_transaction_hash: '0x866a70cb1c8c33d259c819473d7b419c0de67770755bf07dee14dd2d0c6dc800',
source_token: {
address_hash: '0x33a31e0f62c0ddf25090b61ef21a70d5f48725b7',
name: 'Circle USD',
symbol: 'USDC',
decimals: '6',
},
destination_transaction_hash: '0xdbdf690cfde8af2ee855bb90bfa9977a2d8ba36c9ae1a2010c67fe3774832214',
status: MessageStatus.MESSAGE_STATUS_FAILED,
bridge: {
name: 'Optimism Superchain',
},
} satisfies InterchainTransfer;
export const listResponse = {
items: [
transferA,
transferB,
],
next_page_params: {
page_token: 'token',
},
} satisfies GetTransfersResponse;
+75
View File
@@ -0,0 +1,75 @@
import type { GetMessagesResponse, InterchainMessage } from '@blockscout/interchain-indexer-types';
import { MessageStatus } from '@blockscout/interchain-indexer-types';
import { config } from './config';
import { transferA, transferB } from './transfers';
/* eslint-disable max-len */
export const base = {
bridge: {
name: 'Avalanche ICTT',
ui_url: 'https://app.avax.network/',
},
message_id: '0x057b42bbbfbb4900e155a554ae67632cb21e6f5a64d815fcad7f33abe552c059',
status: MessageStatus.MESSAGE_STATUS_COMPLETED,
source_chain: config[0],
send_timestamp: '2022-01-13T12:06:24.000Z',
sender: {
hash: '0x33a31e0f62c0ddf25090b61ef21a70d5f48725b7',
},
source_transaction_hash: '0x866a70cb1c8c33d259c819473d7b419c0de67770755bf07dee14dd2d0c6dc8ab',
destination_chain: config[1],
receive_timestamp: '2022-02-13T12:06:30.000Z',
recipient: {
hash: '0x012cb6651cb29c7d5dc96173756a773f7fb87cfb',
ens_domain_name: 'duck-duck.eth',
},
destination_transaction_hash: '0xdbdf690cfde8af2ee855bb90bfa9977a2d8ba36c9ae1a2010c67fe3774832213',
payload: '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000d7e63822d0e386fb65f28f41e8c1aa8d844ba0180000000000000000000000000000000000000000000000000712d17312044000',
extra: {},
transfers: [
transferA,
transferB,
],
} satisfies InterchainMessage;
export const pending = {
...base,
message_id: '0x057b42bbbfbb4900e155a554ae67632cb21e6f5a64d815fcad7f33abe552c05a',
status: MessageStatus.MESSAGE_STATUS_INITIATED,
source_chain: config[1],
destination_chain: config[2],
destination_transaction_hash: undefined,
transfers: [
transferA,
],
} satisfies InterchainMessage;
export const failed = {
...base,
message_id: '0x057b42bbbfbb4900e155a554ae67632cb21e6f5a64d815fcad7f33abe552c05b',
status: MessageStatus.MESSAGE_STATUS_FAILED,
source_chain: {
id: '420',
name: 'Unknown chain',
logo: undefined,
explorer_url: undefined,
},
transfers: [
transferB,
],
bridge: {
name: 'Goose bridge',
},
} satisfies InterchainMessage;
export const listResponse = {
items: [
base,
pending,
failed,
],
next_page_params: {
page_token: '1',
},
} satisfies GetMessagesResponse;
+8
View File
@@ -27,6 +27,7 @@ export const token1: SearchResultToken = {
exchange_rate: null,
is_verified_via_admin_panel: true,
is_smart_contract_verified: true,
is_smart_contract_address: true,
reputation: 'ok',
};
@@ -43,6 +44,7 @@ export const token2: SearchResultToken = {
exchange_rate: '1.11',
is_verified_via_admin_panel: false,
is_smart_contract_verified: false,
is_smart_contract_address: false,
reputation: 'ok',
};
@@ -77,6 +79,7 @@ export const address1: SearchResultAddressOrContract = {
name: null,
type: 'address' as const,
is_smart_contract_verified: false,
is_smart_contract_address: false,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
@@ -85,6 +88,7 @@ export const address2: SearchResultAddressOrContract = {
name: null,
type: 'address' as const,
is_smart_contract_verified: false,
is_smart_contract_address: false,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b',
ens_info: {
address_hash: '0x1234567890123456789012345678901234567890',
@@ -99,6 +103,7 @@ export const contract1: SearchResultAddressOrContract = {
name: 'Unknown contract in this network',
type: 'contract' as const,
is_smart_contract_verified: true,
is_smart_contract_address: true,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
@@ -108,6 +113,7 @@ export const contract2: SearchResultAddressOrContract = {
type: 'contract' as const,
is_smart_contract_verified: true,
certified: true,
is_smart_contract_address: true,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
@@ -116,6 +122,7 @@ export const label1: SearchResultLabel = {
name: 'utko',
type: 'label' as const,
is_smart_contract_verified: true,
is_smart_contract_address: true,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
@@ -148,6 +155,7 @@ export const domain1: SearchResultDomain = {
names_count: 1,
},
is_smart_contract_verified: false,
is_smart_contract_address: false,
name: null,
type: 'ens_domain',
url: '/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
+9 -1
View File
@@ -25,6 +25,14 @@ const moduleExports = {
);
config.resolve.fallback = { fs: false, net: false, tls: false };
config.externals.push('pino-pretty', 'lokijs', 'encoding');
config.experiments = { ...config.experiments, topLevelAwait: true };
// Tell webpack the target supports async/await so it stops warning about top-level await
// Top-level await is belong to ES2017 specification that is adopted by all major browsers and Node.js.
config.output.environment = {
...config.output.environment,
asyncFunction: true,
};
return config;
},
@@ -36,7 +44,7 @@ const moduleExports = {
redirects,
headers,
output: 'standalone',
productionBrowserSourceMaps: true,
productionBrowserSourceMaps: false,
serverExternalPackages: ["@opentelemetry/sdk-node", "@opentelemetry/auto-instrumentations-node"],
experimental: {
staleTimes: {
+2 -2
View File
@@ -24,8 +24,8 @@ const PageNextJs = <Pathname extends Route['pathname']>(props: Props<Pathname>)
useAdblockDetect();
useNotifyOnNavigation();
const isMixpanelInited = mixpanel.useInit();
mixpanel.useLogPageView(isMixpanelInited);
const isMixPanelInitialized = mixpanel.useInit();
mixpanel.useLogPageView(isMixPanelInitialized);
return (
<>
+1 -1
View File
@@ -6,6 +6,7 @@ function generateCspPolicy(isPrivateMode = false) {
descriptors.app(isPrivateMode),
// Exclude tracking/analytics sources in private mode
isPrivateMode ? {} : descriptors.ad(),
isPrivateMode ? {} : descriptors.blockchainInteraction(),
descriptors.cloudFlare(),
descriptors.flashblocks(),
descriptors.gasHawk(),
@@ -23,7 +24,6 @@ function generateCspPolicy(isPrivateMode = false) {
descriptors.rollup(),
descriptors.safe(),
descriptors.usernameApi(),
isPrivateMode ? {} : descriptors.walletConnect(),
descriptors.zetachain(),
);
@@ -0,0 +1,63 @@
import type CspDev from 'csp-dev';
import config from 'configs/app';
import { KEY_WORDS } from '../utils';
const feature = config.features.blockchainInteraction;
export function blockchainInteraction(): CspDev.DirectiveDescriptor {
if (!feature.isEnabled) {
return {};
}
switch (feature.connectorType) {
case 'reown': {
return {
'connect-src': [
'*.web3modal.com',
'*.web3modal.org',
'*.walletconnect.com',
'*.walletconnect.org',
'wss://relay.walletconnect.com',
'wss://relay.walletconnect.org',
'wss://www.walletlink.org',
],
'frame-ancestors': [
'*.walletconnect.org',
'*.walletconnect.com',
],
'img-src': [
KEY_WORDS.BLOB,
'*.walletconnect.com',
],
};
}
case 'dynamic': {
return {
'connect-src': [
'https://dynamic-static-assets.com',
'https://app.dynamicauth.com',
'https://logs.dynamicauth.com',
],
'font-src': [
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-normal.woff2',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-normal.woff',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-italic.woff2',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-italic.woff',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-normal.woff2',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-normal.woff',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-italic.woff2',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-italic.woff',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-normal.woff2',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-normal.woff',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-italic.woff2',
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-italic.woff',
],
'style-src': [
'https://app.dynamic.xyz',
],
};
}
}
}
+1 -1
View File
@@ -1,5 +1,6 @@
export { ad } from './ad';
export { app } from './app';
export { blockchainInteraction } from './blockchainInteraction';
export { cloudFlare } from './cloudFlare';
export { flashblocks } from './flashblocks';
export { gasHawk } from './gasHawk';
@@ -17,5 +18,4 @@ export { rollbar } from './rollbar';
export { rollup } from './rollup';
export { safe } from './safe';
export { usernameApi } from './usernameApi';
export { walletConnect } from './walletConnect';
export { zetachain } from './zetachain';
-31
View File
@@ -1,31 +0,0 @@
import type CspDev from 'csp-dev';
import config from 'configs/app';
import { KEY_WORDS } from '../utils';
export function walletConnect(): CspDev.DirectiveDescriptor {
if (!config.features.blockchainInteraction.isEnabled) {
return {};
}
return {
'connect-src': [
'*.web3modal.com',
'*.web3modal.org',
'*.walletconnect.com',
'*.walletconnect.org',
'wss://relay.walletconnect.com',
'wss://relay.walletconnect.org',
'wss://www.walletlink.org',
],
'frame-ancestors': [
'*.walletconnect.org',
'*.walletconnect.com',
],
'img-src': [
KEY_WORDS.BLOB,
'*.walletconnect.com',
],
};
}
+17
View File
@@ -18,6 +18,15 @@ export const account: Guard = (chainConfig: typeof config) => async() => {
}
};
export const accountAuth0: Guard = (chainConfig: typeof config) => async() => {
const feature = chainConfig.features.account;
if (!feature.isEnabled || feature.authProvider !== 'auth0') {
return {
notFound: true,
};
}
};
export const verifiedAddresses: Guard = (chainConfig: typeof config) => async() => {
if (!chainConfig.features.addressVerification.isEnabled) {
return {
@@ -319,6 +328,14 @@ export const interopMessages: Guard = (chainConfig: typeof config) => async() =>
}
};
export const crossChainTxs: Guard = (chainConfig: typeof config) => async() => {
if (!chainConfig.features.crossChainTxs.isEnabled) {
return {
notFound: true,
};
}
};
export const opSuperchain: Guard = () => async() => {
if (!config.features.opSuperchain.isEnabled) {
return {
+2
View File
@@ -6,6 +6,7 @@ export const block = factory([ guards.notOpSuperchain ]);
export const tx = factory([ guards.notOpSuperchain ]);
export const token = factory([ guards.notOpSuperchain ]);
export const account = factory([ guards.account ]);
export const accountAuth0 = factory([ guards.accountAuth0 ]);
export const verifiedAddresses = factory([ guards.account, guards.verifiedAddresses ]);
export const userOps = factory([ guards.userOps ]);
export const marketplace = factory([ guards.marketplace ]);
@@ -31,6 +32,7 @@ export const publicTagsSubmit = factory([ guards.publicTagsSubmit ]);
export const pools = factory([ guards.pools ]);
export const megaEth = factory([ guards.megaEth ]);
export const zetaChainCCTX = factory([ guards.zetaChainCCTX ]);
export const crossChainTxs = factory([ guards.notOpSuperchain, guards.crossChainTxs ]);
// ROLLUPS
export const rollup = factory([ guards.rollup ]);
+1
View File
@@ -52,6 +52,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/chain/[chain_slug]/visualize/sol2uml", { "chain_slug": string }>
| StaticRoute<"/chakra">
| StaticRoute<"/contract-verification">
| DynamicRoute<"/cross-chain-tx/[id]", { "id": string }>
| StaticRoute<"/csv-export">
| StaticRoute<"/deposits">
| StaticRoute<"/dispute-games">
+1 -1
View File
@@ -13,7 +13,7 @@ export interface RouteParams {
export const route = (route: Route, params?: RouteParams | null) => {
const generatedRoute = nextjsRoute(routeParams(route, params));
if (params && params.chain && params.external) {
if (params && params.chain && params.external && params.chain.explorer_url) {
return stripTrailingSlash(params.chain.explorer_url) + generatedRoute;
}
+13 -3
View File
@@ -22,6 +22,8 @@
"lint:eslint": "eslint .",
"lint:eslint:fix": "eslint . --fix",
"lint:tsc": "tsc -p ./tsconfig.json",
"lint:cspell": "cspell . --no-must-find-files",
"lint:license:check": "license-report > ./license.json && license-report-check --source=./license.json --output=table --forbidden=n/a",
"lint:envs-validator:test": "cd ./deploy/tools/envs-validator && ./test.sh",
"prepare": "husky install",
"svg:format": "svgo -r --config svgo.config.format.js ./icons ./toolkit/chakra/assets",
@@ -42,6 +44,7 @@
},
"dependencies": {
"@blockscout/bens-types": "1.4.1",
"@blockscout/interchain-indexer-types": "0.0.10",
"@blockscout/multichain-aggregator-types": "1.6.3-alpha.3",
"@blockscout/points-types": "1.4.0-alpha.1",
"@blockscout/stats-types": "2.11.1",
@@ -49,6 +52,9 @@
"@blockscout/visualizer-types": "0.2.0",
"@blockscout/zetachain-cctx-types": "^1.0.0-rc.6",
"@chakra-ui/react": "3.15.0",
"@dynamic-labs/ethereum": "4.52.2",
"@dynamic-labs/sdk-react-core": "4.52.2",
"@dynamic-labs/wagmi-connector": "4.52.2",
"@emotion/react": "11.14.0",
"@growthbook/growthbook-react": "0.21.0",
"@helia/verified-fetch": "2.6.12",
@@ -85,6 +91,7 @@
"blo": "^1.1.1",
"brotli-compress": "1.3.3",
"crypto-js": "^4.2.0",
"cspell": "9.6.4",
"d3": "^7.6.1",
"dappscout-iframe": "0.4.0",
"dayjs": "^1.11.5",
@@ -100,7 +107,7 @@
"magic-bytes.js": "1.8.0",
"mixpanel-browser": "2.67.0",
"monaco-editor": "^0.34.1",
"next": "15.5.9",
"next": "15.5.10",
"next-themes": "0.4.4",
"nextjs-routes": "^1.0.8",
"node-fetch": "^3.2.9",
@@ -166,7 +173,6 @@
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import-helpers": "2.0.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-no-cyrillic-string": "^1.0.5",
"eslint-plugin-playwright": "2.0.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.0.0",
@@ -175,6 +181,8 @@
"globals": "15.12.0",
"husky": "^8.0.0",
"jsdom": "27.3.0",
"license-report": "6.8.1",
"license-report-check": "0.1.2",
"lint-staged": ">=10",
"mockdate": "^3.0.5",
"style-loader": "^3.3.1",
@@ -204,7 +212,9 @@
"@reown/appkit-adapter-wagmi/**/node-forge": "1.3.2",
"eslint/**/brace-expansion": "1.1.12",
"swagger-ui-react/**/js-yaml": "4.1.1",
"dappscout-iframe/**/ws": "8.17.1"
"dappscout-iframe/**/ws": "8.17.1",
"viem/**/@noble/hashes": "1.8.0",
"@walletconnect/ethereum-provider/**/@reown/appkit/**/@noble/hashes": "1.8.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+17 -12
View File
@@ -3,6 +3,7 @@ import { GrowthBookProvider } from '@growthbook/growthbook-react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';
import React from 'react';
import type { NextPageWithLayout } from 'nextjs/types';
@@ -11,8 +12,8 @@ import config from 'configs/app';
import getSocketUrl from 'lib/api/getSocketUrl';
import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app';
import { FallbackProvider } from 'lib/contexts/fallback';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { initGrowthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
@@ -20,13 +21,15 @@ import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/
import { SocketProvider } from 'lib/socket/context';
import { Provider as ChakraProvider } from 'toolkit/chakra/provider';
import { Toaster } from 'toolkit/chakra/toaster';
import RewardsLoginModal from 'ui/rewards/login/RewardsLoginModal';
import RewardsActivityTracker from 'ui/rewards/RewardsActivityTracker';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import Layout from 'ui/shared/layout/Layout';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import Web3Provider from 'ui/shared/web3/Web3Provider';
const RewardsContextProvider = dynamic(() => import('lib/contexts/rewards').then(module => module.RewardsContextProvider), { ssr: false });
const RewardsLoginModal = dynamic(() => import('ui/rewards/login/RewardsLoginModal'), { ssr: false });
const RewardsActivityTracker = dynamic(() => import('ui/rewards/RewardsActivityTracker'), { ssr: false });
import 'lib/setLocale';
// import 'focus-visible/dist/focus-visible';
@@ -88,6 +91,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
);
})();
const RewardsProvider = config.features.rewards.isEnabled ? RewardsContextProvider : FallbackProvider;
const socketUrl = !config.features.opSuperchain.isEnabled ? getSocketUrl() : undefined;
return (
@@ -97,25 +102,25 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
{ ...ERROR_SCREEN_STYLES }
Container={ AppErrorGlobalContainer }
>
<Web3ModalProvider>
<AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<QueryClientProvider client={ queryClient }>
<Web3Provider>
<AppContextProvider pageProps={ pageProps }>
<GrowthBookProvider growthbook={ growthBook }>
<SocketProvider url={ socketUrl }>
<RewardsContextProvider>
<RewardsProvider>
<MarketplaceContextProvider>
<SettingsContextProvider>
{ content }
</SettingsContextProvider>
</MarketplaceContextProvider>
</RewardsContextProvider>
</RewardsProvider>
</SocketProvider>
</GrowthBookProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
</AppContextProvider>
</Web3ModalProvider>
</AppContextProvider>
</Web3Provider>
</QueryClientProvider>
</AppErrorBoundary>
</RollbarProvider>
</ChakraProvider>
+37
View File
@@ -1,10 +1,26 @@
import type { NextPageContext } from 'next';
import NextErrorComponent from 'next/error';
import React from 'react';
import Rollbar from 'rollbar';
import type { Props as ServerSidePropsCommon } from 'nextjs/getServerSideProps/handlers';
import config from 'configs/app';
import * as cookies from 'lib/cookies';
const rollbarFeature = config.features.rollbar;
const rollbar = rollbarFeature.isEnabled ? new Rollbar({
accessToken: rollbarFeature.clientToken,
environment: rollbarFeature.environment,
payload: {
code_version: rollbarFeature.codeVersion,
app_instance: rollbarFeature.instance,
},
maxItems: 10,
captureUncaught: true,
captureUnhandledRejections: true,
}) : undefined;
type Props = ServerSidePropsCommon & {
statusCode: number;
};
@@ -14,4 +30,25 @@ const CustomErrorComponent = (props: Props) => {
return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>;
};
CustomErrorComponent.getInitialProps = async(context: NextPageContext) => {
const { res, err, req } = context;
const baseProps = await NextErrorComponent.getInitialProps(context); // Extract cookies from the request headers
const statusCode = res?.statusCode ?? err?.statusCode;
const cookies = req?.headers?.cookie || '';
if (rollbar) {
rollbar.error(err?.message ?? 'Unknown error', {
cause: err?.cause,
stack: err?.stack,
});
}
return {
...baseProps,
statusCode,
cookies,
};
};
export default CustomErrorComponent;
+1 -1
View File
@@ -15,4 +15,4 @@ const Page: NextPage = () => {
export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps/main';
export { accountAuth0 as getServerSideProps } from 'nextjs/getServerSideProps/main';
+20
View File
@@ -0,0 +1,20 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps/handlers';
import PageNextJs from 'nextjs/PageNextJs';
const TxCrossChain = dynamic(() => import('ui/crossChain/tx/TxCrossChain'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/cross-chain-tx/[id]" query={ props.query }>
<TxCrossChain/>
</PageNextJs>
);
};
export default Page;
export { crossChainTxs as getServerSideProps } from 'nextjs/getServerSideProps/main';
+4
View File
@@ -15,6 +15,10 @@ const Transactions = dynamic(() => {
return import('ui/pages/TransactionsZetaChain');
}
if (config.features.crossChainTxs.isEnabled) {
return import('ui/crossChain/txs/Transactions');
}
return import('ui/pages/Transactions');
}, { ssr: false });
+16 -1
View File
@@ -35,7 +35,7 @@ const config: PlaywrightTestConfig = defineConfig({
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: process.env.CI ? 'blob' : 'html',
expect: {
toHaveScreenshot: {
@@ -68,6 +68,21 @@ const config: PlaywrightTestConfig = defineConfig({
// https://github.com/storybookjs/builder-vite/issues/409#issuecomment-1152848986
sourcemap: false,
minify: false,
rollupOptions: {
output: {
// Ensure __envs exists in every chunk so code (e.g. next/router, configs/app) that
// runs before index.ts/envs.js doesn't throw. Real envs are set when envs.js runs.
//
// Explanation:
// With await import(...) in useAccount/useWallet, Vite turns those into separate chunks.
// In Playwright CT, the components chunk (and its dependency graph, including useAccount → config → getEnvValue → __envs)
// can load and run before the main entry that runs envs.js.
// So when that chunk runs, window.__envs isnt set yet and you get ReferenceError: __envs is not defined.
//
// https://rollupjs.org/configuration-options/#output-banner-output-footer
banner: '(function(){if(typeof window!==\'undefined\')window.__envs=window.__envs||{};})();',
},
},
},
resolve: {
alias: [
+4
View File
@@ -136,4 +136,8 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
colorThemeOverrides: [
[ 'NEXT_PUBLIC_COLOR_THEME_OVERRIDES', '{"bg":{"primary":{"_light":{"value":"rgba(254,253,253)"},"_dark":{"value":"rgba(25,25,26)"}}},"text":{"primary":{"_light":{"value":"rgba(16,17,18,0.80)"},"_dark":{"value":"rgba(222,217,217)"}},"secondary":{"_light":{"value":"rgba(138,136,136)"},"_dark":{"value":"rgba(133,133,133)"}}},"hover":{"_light":{"value":"rgba(104,200,158)"},"_dark":{"value":"rgba(104,200,158)"}},"selected":{"control":{"text":{"_light":{"value":"rgba(25,25,26)"},"_dark":{"value":"rgba(247,250,252)"}},"bg":{"_light":{"value":"rgba(242,239,239)"},"_dark":{"value":"rgba(255,255,255,0.06)"}}},"option":{"bg":{"_light":{"value":"rgba(84,75,75)"},"_dark":{"value":"rgba(87,87,87)"}}}},"icon":{"primary":{"_light":{"value":"rgba(138,136,136)"},"_dark":{"value":"rgba(133,133,133)"}},"secondary":{"_light":{"value":"rgba(176,176,176)"},"_dark":{"value":"rgba(105,103,103)"}}},"button":{"primary":{"_light":{"value":"rgba(105,103,103)"},"_dark":{"value":"rgba(133,133,133)"}}},"link":{"primary":{"_light":{"value":"rgba(57,146,108)"},"_dark":{"value":"rgba(57,146,108)"}}},"graph":{"line":{"_light":{"value":"rgba(105,103,103)"},"_dark":{"value":"rgba(57,146,108)"}},"gradient":{"start":{"_light":{"value":"rgba(105,103,103,0.3)"},"_dark":{"value":"rgba(57,146,108,0.3)"}},"stop":{"_light":{"value":"rgba(105,103,103,0)"},"_dark":{"value":"rgba(57,146,108,0)"}}}},"stats":{"bg":{"_light":{"value":"rgba(242,239,239)"},"_dark":{"value":"rgba(255,255,255,0.06)"}}},"topbar":{"bg":{"_light":{"value":"rgba(242,239,239)"},"_dark":{"value":"rgba(255,255,255,0.06)"}}},"navigation":{"text":{"selected":{"_light":{"value":"rgba(25,25,26)"},"_dark":{"value":"rgba(247,250,252)"}}},"bg":{"selected":{"_light":{"value":"rgba(242,239,239)"},"_dark":{"value":"rgba(255,255,255,0.06)"}}}},"tabs":{"text":{"primary":{"_light":{"value":"rgba(25,25,26)"},"_dark":{"value":"rgba(222,217,217)"}}}}}' ],
],
crossChainTxs: [
[ 'NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST', 'http://localhost:3014' ],
[ 'NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED', 'true' ],
],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+2 -1
View File
@@ -3,6 +3,7 @@
export type IconName =
| "ABI"
| "advanced-filter"
| "AI"
| "API"
| "apps_list"
| "apps"
@@ -47,7 +48,6 @@
| "cross"
| "delete"
| "docs"
| "donate"
| "dots"
| "edit"
| "email"
@@ -85,6 +85,7 @@
| "lightning"
| "link_external"
| "link"
| "list_view"
| "lock"
| "merits_colored"
| "merits_with_dot"

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More