From c66c6e73a9bc8762b1a87540a434a6591943f702 Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 4 Aug 2026 18:36:07 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Build=20production=20bundles=20with=20webpa?= =?UTF-8?q?ck=20=E2=80=94=20Turbopack=20miscompiles=20the=20Dynamic-labs?= =?UTF-8?q?=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turbopack's scope hoisting emits code that reads the SDK's UserFieldEditorContext through the wrong binding. useContext then receives a non-context value, returns undefined, and the SDK throws from its own useUpdateUserWithModal: useUserUpdateRequest can only be used inside the context of DynamicContextProvider The thrower is the SDK's internal SyncAuthFlow, which the SDK itself renders inside UserFieldEditorContextProvider — so in a correct build that context cannot be missing. A webpack build of the same source is fine, so the defect is Turbopack's, not ours. Impact: a hard crash on the initial load of every page, on any instance with NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic. Invisible in dev (unminified, no hoisting) and found only by running the v2.10.0 image locally, so v2.10.0 would have broken every dynamic-auth instance on rollout. Bisected to #3574; not fixed by Next 16.3.0, and not reducible to a single import — the trigger is an emergent property of the module graph. webpack turns out to be the better bundle anyway, not just the correct one. Against the Turbopack build it cuts JS-before-FCP 1038 -> 697 KB (-33%) and emitted chunk bytes 49.2 -> 21.4 MB, at 2-3x the build time. The other fix, experimental.turbopackScopeHoisting:false, was measured and rejected: it nearly doubles FCP and triples blocking time while leaving M6 almost unchanged. Dev stays on Turbopack — it is ~3x faster to compile and this crash class only exists in a minified production build. - build, build:next and prod.preset.sh now pass --webpack (build:analyze and profile:preset already did). prod:preset matters because it is the measurement path: profiling a bundler we no longer ship would be misleading. - resolve.fallback maps @react-native-async-storage/async-storage to false. webpack surfaces that unresolvable optional import inside @metamask/sdk, which a browser bundle never needs; the build is warning-free. Introduces .agents/adr/ for decisions that are expensive to rediscover, indexed from AGENTS.md. This is record 0001 and carries the full evidence. Also automates the perf-trace capture used to make the call: trace.mjs records a DevTools-format trace over CDP so trace-metrics.py can consume several runs per variant. Its README now warns that M6 alone is not a sufficient gate — that is exactly how the scope-hoisting option looked cheap. Co-Authored-By: Claude Opus 5 --- .agents/AGENTS.md | 13 +++ .../adr/0001-webpack-for-production-builds.md | 108 ++++++++++++++++++ .../tools/README.md | 32 +++++- .../tools/trace.mjs | 77 +++++++++++++ cspell.jsonc | 3 + next.config.js | 11 +- package.json | 4 +- src/features/connect-wallet/CONTEXT.md | 8 ++ tools/dev-server/prod.preset.sh | 5 +- 9 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 .agents/adr/0001-webpack-for-production-builds.md create mode 100644 .agents/tasks/3566-main-page-loading-perf/tools/trace.mjs diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 81cea00d45..20f59bcd62 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -61,6 +61,19 @@ Some directories have a `CONTEXT.md` documenting non-obvious patterns specific t If you encounter a `CONTEXT.md` not listed here, read it too (and consider adding it to this list). +## Architecture decision records + +Decisions with repo-wide consequences are recorded in `.agents/adr/`, named +`<0000>-.md`. Read the relevant one before changing what it decided — an ADR carries the +evidence and the trade-off, so it answers "why is it like this?" without a git archaeology session. + +- `0001-webpack-for-production-builds.md` — why production bundles are built with webpack while dev stays on Turbopack. + +Add a new record (next free number, and a line here) whenever a decision is expensive to rediscover: +it constrains future work, was reached by measurement or an investigation worth not repeating, or +looks wrong without its context. Supersede rather than rewrite — flip the old record's `Status` to +`superseded by ` and leave its reasoning intact. + ## Product task workflow Product tasks (GitHub issues) are worked through a spec-driven workflow, run by the `grill-the-task`, diff --git a/.agents/adr/0001-webpack-for-production-builds.md b/.agents/adr/0001-webpack-for-production-builds.md new file mode 100644 index 0000000000..5a666000d7 --- /dev/null +++ b/.agents/adr/0001-webpack-for-production-builds.md @@ -0,0 +1,108 @@ +# 0001 — webpack for production builds, Turbopack for dev + +| | | +| --- | --- | +| Status | accepted | +| Date | 2026-08-04 | +| Deciders | @tom2drum | +| Supersedes | — | + +## Decision + +**Production builds use webpack (`next build --webpack`). Dev keeps Turbopack (the Next 16 default).** + +Applies to every entry point that emits a production bundle: + +| Entry point | Bundler | +| --- | --- | +| `pnpm build` — what the `Dockerfile` runs for the shipped image | webpack | +| `pnpm build:next` | webpack | +| `pnpm prod:preset ` — local production build, incl. perf measurements | webpack | +| `pnpm build:analyze`, `pnpm profile:preset` | webpack (already were) | +| `pnpm dev`, `pnpm dev:preset`, `pnpm dev:local` | Turbopack | + +Dev stays on Turbopack because it is roughly 3× faster to compile and the crash class below only +manifests in a minified production build. Production-build regressions are caught in QA rather than +by making every local dev start slower. + +## Why + +### Turbopack miscompiles the Dynamic-labs SDK + +Turbopack's scope hoisting emits code that reads the SDK's `UserFieldEditorContext` through the +wrong binding. `useContext` therefore receives a non-context value, returns `undefined`, and the SDK +throws from its own `useUpdateUserWithModal`: + +``` +useUserUpdateRequest can only be used inside the context of DynamicContextProvider +``` + +The throwing component is the SDK's internal `SyncAuthFlow`, which the SDK itself renders *inside* +`UserFieldEditorContextProvider` — so in a correct build the context cannot be missing. It is a +bundler defect, not a provider-tree bug in our code. + +Impact: **a hard crash on the initial load of every page**, for any instance configured with +`NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic`. It is invisible in dev (unminified, no hoisting) and +was found only by running the `v2.10.0` image locally. The v2.10.0 release would have broken every +dynamic-auth instance on rollout; deployed instances were still on v2.9.4 and unaffected. + +Bisected to [#3574](https://github.com/blockscout/frontend/pull/3574) (wallet-stack deferral, +subtask 4 of [#3566](https://github.com/blockscout/frontend/issues/3566)) — parent commit good, that +commit bad. The trigger could **not** be reduced to a single import: reverting the lazy `import()` +wrappers, the `_app.tsx` provider restructure, and the `@wagmi/core` dependency each left it broken. +That fits the mechanism — scope hoisting groups modules across the whole graph, so the trigger is an +emergent property of how #3574 reshaped it, and any future graph change could re-trigger it +somewhere else. Next 16.3.0 does not fix it. + +### webpack is also the faster bundle + +Two options fixed the crash: `--webpack`, or `experimental.turbopackScopeHoisting: false`. The flag +turned out to be the expensive one. Production builds of `main`, medians of 3 +automated traces: + +| Metric | Turbopack | Turbopack, hoisting off | **webpack** | +| --- | --- | --- | --- | +| M1 FCP | 432 ms | 790 ms | **501 ms** | +| M2 first API request | 60 ms | 142 ms | **57 ms** | +| M5 blocking time | 133 ms | 408 ms | **155 ms** | +| M6 JS before FCP | 1038 KB | 1064 KB | **697 KB** | +| Emitted chunk bytes | 49.2 MB | 53.4 MB | **21.4 MB** | +| Build time | 48 s | 41 s | 2.4 min | + +Disabling scope hoisting nearly doubles FCP and triples blocking time while barely moving M6 (+2.5%) +— the cost lands in execution, not transfer, so M6 alone would not have caught it. webpack instead +*improves* pre-FCP JS by 341 KB (−33%) over the Turbopack build, more than any single lever in #3566 +delivered on its own. + +The measurement harness lives in +`.agents/tasks/3566-main-page-loading-perf/tools/` (see its README). Absolute values come from +headless Chromium on a local server and are not comparable to the numbers in that task's spec table; +the within-comparison deltas are what the decision rests on. + +## Consequences + +- **CI and image builds get slower** — webpack's compile step measured 84 s to 2.4 min across + machines and cache states, against 41–48 s for Turbopack, so budget roughly 2–3×. Accepted: + correctness plus a materially smaller bundle outweigh build latency. +- **Dev and production now use different bundlers.** A bug in either pipeline can only be caught on + that pipeline; production-only breakage will not appear in dev. QA runs against a real image. +- `next.config.js` must keep **both** the `webpack()` and `turbopack` sections in sync — it already + does, and this decision makes that non-optional. +- webpack surfaces one unresolvable import Turbopack silently tolerates: + `@react-native-async-storage/async-storage` inside `@metamask/sdk`, reached via + `@wagmi/connectors` → `@reown/appkit-adapter-wagmi` → `wagmi-config.ts`. It is an optional peer + dependency of a React Native code path a browser bundle never takes, so `next.config.js` maps it + to `false` in `resolve.fallback` (an empty module) and the build is warning-free. If a future + dependency bump introduces a similar optional import, extend that map rather than silencing + warnings wholesale. +- `next build --webpack` is a compatibility path in Next 16 and may eventually be removed. If that + happens before Turbopack is fixed, the fallback is `experimental.turbopackScopeHoisting: false` + and its performance cost. + +## Follow-ups + +- Report the miscompilation upstream to `vercel/next.js` with a minimal reproduction; the bisect + boundary and the flag that toggles it are the material. +- Re-test Turbopack on each Next upgrade. If a release fixes it, revisit — Turbopack's build speed + is worth reclaiming, but only with the M1/M5/M6 numbers above re-measured, not on the release + notes alone. diff --git a/.agents/tasks/3566-main-page-loading-perf/tools/README.md b/.agents/tasks/3566-main-page-loading-perf/tools/README.md index 9b0196913b..ed8adb3441 100644 --- a/.agents/tasks/3566-main-page-loading-perf/tools/README.md +++ b/.agents/tasks/3566-main-page-loading-perf/tools/README.md @@ -17,13 +17,25 @@ inflate everything 2–3×). Keep the **same preset** for every measurement — metrics M3/M4 depend on the instance's backend latency, so numbers from different presets are not comparable. -2. Open `http://localhost:3000/` in a **clean browser profile** (incognito, no extensions — - React DevTools alone adds ~150 ms of scripting). +2. Record the trace, either by hand or scripted. -3. DevTools → Performance → "Record and reload". Stop a couple of seconds after the - transactions/blocks lists show real data. Export the trace as JSON. + **By hand** — open `http://localhost:3000/` in a **clean browser profile** (incognito, no + extensions — React DevTools alone adds ~150 ms of scripting), then DevTools → Performance → + "Record and reload". Stop a couple of seconds after the transactions/blocks lists show real + data. Export the trace as JSON. -4. Extract the metrics: + **Scripted** — `trace.mjs` drives headless Chromium over CDP and writes the same JSON: + + ```bash + node trace.mjs http://localhost:3000/ /tmp/traces/before 3 # 3 runs -> before-1..3.json + ``` + + It records the same event categories the Performance panel does and uses a fresh browser + context per run (no extensions, cold cache), so it is the scripted equivalent of the clean + profile above. Prefer it whenever you need several runs per variant or a repeatable A/B; a + single exploratory trace is easier by hand, where you can also read the flame chart. + +3. Extract the metrics: ```bash python3 trace-metrics.py baseline.json # one trace @@ -40,3 +52,13 @@ inflate everything 2–3×). - The app under `prod:preset` proxies API calls through `localhost:3000/node-api/proxy` (the fetched config keeps `APP_ENV=development`). Both variants of an A/B pair share this hop, so deltas are valid — but do not compare absolute values against traces of a deployed instance. +- **Headless (`trace.mjs`) and headed absolute values are not comparable either** — same rule, + compare within one capture method. Do not mix them in a single row of the spec's table. +- **M6 is not a sufficient gate on its own.** A bundler change can leave the bytes-before-FCP + almost untouched while doubling FCP and tripling blocking time, because the cost is in executing + the code rather than transferring it. Always read M1 and M5 alongside M6 before concluding a + change is cheap — see `.agents/adr/0001-webpack-for-production-builds.md` for the case that + taught us this. +- `prod:preset` builds with the same bundler as the shipped image (webpack, per that ADR), so its + traces represent what users get. If you ever measure a build made another way, say so next to + the numbers. diff --git a/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs b/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs new file mode 100644 index 0000000000..9962d5f2f3 --- /dev/null +++ b/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs @@ -0,0 +1,77 @@ +// Record page-load performance traces without the DevTools UI. +// +// Produces the same JSON the Performance panel's "Record and reload" export produces, so the output +// feeds straight into trace-metrics.py. Use it when you want several runs per variant (M3/M4 need a +// median) or a repeatable A/B — the manual protocol in README.md is still fine for a one-off. +// +// Usage, against an already-running production server (`pnpm prod:preset `): +// +// node .agents/tasks/3566-main-page-loading-perf/tools/trace.mjs http://localhost:3000/ ./traces/after 3 +// python3 .agents/tasks/3566-main-page-loading-perf/tools/trace-metrics.py ./traces/before-2.json ./traces/after-2.json +// +// Writes -.json for n in 1..runs. + +import { chromium } from '@playwright/test'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; + +// Long enough for the transactions/blocks lists to fill with real data, which M3/M4 measure. +const SETTLE_MS = 10_000; + +// The capture set the Performance panel uses: devtools.timeline for resources/tasks/render commits, +// loading for navigationStart, blink.user_timing for paint marks, __metadata for thread names +// (trace-metrics.py needs those to tell CrRendererMain apart from other threads). +const CATEGORIES = [ + '-*', + 'devtools.timeline', + 'disabled-by-default-devtools.timeline', + 'disabled-by-default-devtools.timeline.frame', + 'blink.user_timing', + 'loading', + 'latencyInfo', + 'v8.execute', + '__metadata', +]; + +const [ url, outPrefix, runsArg ] = process.argv.slice(2); +if (!url || !outPrefix) { + console.error('Usage: node trace.mjs [runs=1]'); + process.exit(2); +} +const runs = Number(runsArg ?? 1); + +mkdirSync(dirname(outPrefix), { recursive: true }); + +// A fresh context per run is the scripted equivalent of the protocol's "clean browser profile": +// no extensions, no warm HTTP cache, no carried-over service worker. +const browser = await chromium.launch(); + +for (let run = 1; run <= runs; run++) { + const context = await browser.newContext(); + const page = await context.newPage(); + const client = await context.newCDPSession(page); + + const events = []; + client.on('Tracing.dataCollected', ({ value }) => events.push(...value)); + const complete = new Promise((resolve) => client.once('Tracing.tracingComplete', resolve)); + + // Tracing has to start before the navigation — that is what "Record and reload" does, and + // navigationStart is the zero point every metric is relative to. + await client.send('Tracing.start', { + transferMode: 'ReportEvents', + traceConfig: { includedCategories: CATEGORIES, recordMode: 'recordAsMuchAsPossible' }, + }); + + await page.goto(url, { waitUntil: 'load', timeout: 60_000 }); + await page.waitForTimeout(SETTLE_MS); + + await client.send('Tracing.end'); + await complete; + await context.close(); + + const out = `${ outPrefix }-${ run }.json`; + writeFileSync(out, JSON.stringify({ traceEvents: events })); + console.log(`${ out }: ${ events.length } events`); +} + +await browser.close(); diff --git a/cspell.jsonc b/cspell.jsonc index cef792f90e..40048533ba 100644 --- a/cspell.jsonc +++ b/cspell.jsonc @@ -167,6 +167,8 @@ "megaeth", "merkle", "metasuites", + "miscompilation", + "miscompiles", "mgas", "mload", "mmss", @@ -271,6 +273,7 @@ "uidotdev", "Ulyana", "unfinalized", + "unminified", "UNKN", "unparse", "unrs", diff --git a/next.config.js b/next.config.js index c3d10dbc88..4154a6399f 100644 --- a/next.config.js +++ b/next.config.js @@ -39,7 +39,16 @@ const moduleExports = { use: [ '@svgr/webpack' ], }, ); - config.resolve.fallback = { fs: false, net: false, tls: false }; + config.resolve.fallback = { + fs: false, + net: false, + tls: false, + // @metamask/sdk (reached via @wagmi/connectors -> @reown/appkit-adapter-wagmi) imports the + // React Native storage adapter unconditionally. It is an optional peer dep of a code path a + // browser bundle never takes, so resolve it to an empty module instead of letting webpack + // warn about it on every production build. + '@react-native-async-storage/async-storage': false, + }; config.externals.push('pino-pretty', 'lokijs', 'encoding'); config.experiments = { ...config.experiments, topLevelAwait: true }; diff --git a/package.json b/package.json index cd7826b4e1..aef954d403 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "profile:analyze": "node ./tools/profiling/aggregate-react-profile.mjs", "presets:sync": "node ./tools/dev-server/sync-preset-lists.mjs --write", "presets:lint": "node ./tools/dev-server/sync-preset-lists.mjs", - "build": "next build", - "build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && pnpm svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build", + "build": "next build --webpack", + "build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && pnpm svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build --webpack", "build:docker": "./tools/scripts/build.docker.sh", "build:analyze": "BUNDLE_ANALYZER=true next build --webpack", "start": "next start", diff --git a/src/features/connect-wallet/CONTEXT.md b/src/features/connect-wallet/CONTEXT.md index cbe6e6a8a0..2829f0acd2 100644 --- a/src/features/connect-wallet/CONTEXT.md +++ b/src/features/connect-wallet/CONTEXT.md @@ -56,6 +56,14 @@ disabled *fallback*. would wait forever for a readiness signal only the deferred path emits. Moving dynamic mode onto the deferred model is a known follow-up. +**Before changing anything in the dynamic-mode graph, read +`.agents/adr/0001-webpack-for-production-builds.md`.** Reshaping it once already tripped a Turbopack +scope-hoisting bug that mis-binds a context inside the Dynamic-labs SDK and hard-crashes every page +in a production build — invisible in dev. That is why production bundles are built with webpack. The +trigger was never reducible to a single import, so treat any graph change here as able to re-trigger +it: verify with a production build (`pnpm prod:preset `) against an instance whose +`NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic`, not just in dev. + ## Persisted connection Connection state is persisted in our **own** localStorage flag, not wagmi's diff --git a/tools/dev-server/prod.preset.sh b/tools/dev-server/prod.preset.sh index 11e56693f0..557c9aab9f 100755 --- a/tools/dev-server/prod.preset.sh +++ b/tools/dev-server/prod.preset.sh @@ -4,6 +4,9 @@ # Useful for performance measurements, where dev-mode overhead (React dev build, Turbopack # on-demand compile, StrictMode double-fetch) would skew the numbers. # +# Builds with `--webpack`, like the shipped image does — see .agents/adr/0001-webpack-for-production-builds.md. +# Measuring a Turbopack build here would not represent what users get. +# # Usage: pnpm prod:preset [--skip-build] # # --skip-build Start the server from the existing build (.next) without rebuilding. @@ -76,7 +79,7 @@ if [ "$skip_build" = false ]; then -v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \ -v NEXT_PUBLIC_ICON_SPRITE_HASH="${NEXT_PUBLIC_ICON_SPRITE_HASH}" \ "${env_args[@]}" \ - -- bash -c 'source ./deploy/scripts/export_pro_api_flag.sh && ./deploy/scripts/make_envs_script.sh && next build' || exit 1 + -- bash -c 'source ./deploy/scripts/export_pro_api_flag.sh && ./deploy/scripts/make_envs_script.sh && next build --webpack' || exit 1 echo "" else if [ ! -f ./.env.tmp ]; then From 1a39a49ce673c69fee3e90afe1e26838c38feb34 Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 4 Aug 2026 19:30:03 +0200 Subject: [PATCH 2/3] Silence no-console in the perf trace script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `trace.mjs` is a CLI tool — its stdout is the interface (usage hint, and one line per trace written). Uses a file-level disable with a reason rather than one per call site. Fixes the Code quality job on #3612. Co-Authored-By: Claude Opus 5 --- .agents/tasks/3566-main-page-loading-perf/tools/trace.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs b/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs index 9962d5f2f3..387166e194 100644 --- a/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs +++ b/.agents/tasks/3566-main-page-loading-perf/tools/trace.mjs @@ -11,6 +11,9 @@ // // Writes -.json for n in 1..runs. +/* eslint-disable no-console -- a CLI tool: stdout is its interface, for the usage hint and for + reporting each trace it wrote. */ + import { chromium } from '@playwright/test'; import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname } from 'node:path'; From ee4b368b46e692e5397590cc2d08dae07ee571e8 Mon Sep 17 00:00:00 2001 From: tom goriunov Date: Wed, 5 Aug 2026 12:58:27 +0200 Subject: [PATCH 3/3] Display the fee payer and batched calls on the transaction details page (#3611) * docs: spec for displaying fee payer and calls on the tx details page Refs #3607 Co-Authored-By: Claude Opus 5 * Display the fee payer and batched calls on the transaction details page Eden transactions of type 0x76 are sponsored batches: an executor submits an ordered list of calls and a separate sponsor pays the fee. Render both fields inside the collapsible details, gated on their presence in the response. Pin @blockscout/api-types to a beta built from the backend `dev` branch once `master` had been merged into it, which is the first published build carrying the eden chain type alongside the response shorthands the app relies on. Co-Authored-By: Claude Opus 5 * Tag a sponsored transaction in the details page header Transaction lists have no room for another badge, so `sponsored_transaction` is listed last in TYPES_ORDER with no label of its own. Without an entry there it scores -1 and sorts ahead of every real type, masking labels like "Contract call". Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .agents/GLOSSARY.md | 2 + .../3607-tx-details-fee-payer-calls/spec.md | 275 ++++++++++++++++++ .claude/launch.json | 6 + .github/workflows/deploy-review.yml | 1 + .vscode/tasks.json | 1 + cspell.jsonc | 1 + package.json | 2 +- pnpm-lock.yaml | 10 +- .../eden/pages/tx/TxDetailsEden.tsx | 115 ++++++++ src/slices/tx/components/TxType.spec.tsx | 25 ++ src/slices/tx/components/TxType.tsx | 4 + src/slices/tx/pages/details/Transaction.tsx | 4 + .../tx/pages/details/info/TxDetails.tsx | 3 + tools/dev-server/registry.json | 1 + 14 files changed, 444 insertions(+), 6 deletions(-) create mode 100644 .agents/tasks/3607-tx-details-fee-payer-calls/spec.md create mode 100644 src/features/chain-variants/eden/pages/tx/TxDetailsEden.tsx create mode 100644 src/slices/tx/components/TxType.spec.tsx diff --git a/.agents/GLOSSARY.md b/.agents/GLOSSARY.md index 12a8809111..3d2fddedd1 100644 --- a/.agents/GLOSSARY.md +++ b/.agents/GLOSSARY.md @@ -36,6 +36,7 @@ vars are documented in `docs/ENVS.md`. Architectural concepts like | **Connect Wallet** | feature | Lets users write to contracts, sign transactions, and connect a wallet to the explorer. Previously named `blockchain-interaction`; the current config key is `connectWallet`. Distinct from **Web3 Wallet**. | | **Dispute Games** | entity | Part of the Optimism **Fault Proof System**. On-chain games used to challenge and resolve disputed L2 output roots. | | **Easter Eggs** | feature | Hidden mini-games wired to claim links for badge rewards. | +| **Eden** | chain | A rollup built on `ev-reth` / evstack. Introduces the **Sponsored Transaction** type. | | **Epoch** | entity | A consensus time period specific to **Celo**. Has its own index and detail pages. Always refers to a Celo epoch in this codebase — not a generic blockchain concept. | | **Fault Proof System** | feature | Optimism's mechanism for proving the correctness of L2 state transitions on L1 via **Dispute Games**. | | **Flashblocks** | feature | MegaETH's sub-second block streaming mechanism. | @@ -52,6 +53,7 @@ vars are documented in `docs/ENVS.md`. Architectural concepts like | **Rewards** | feature | The Blockscout Merits program — a token rewards and incentives system operated by Blockscout. Entirely distinct from **Block Reward** (on-chain block-producer payouts). | | **Rollup** | concept | A chain that settles transactions on a parent (L1) chain. Introduces specific entities: deposits, withdrawals, transaction batches, output roots. Contrast with **Chain Variant**. | | **SolidityScan** | service | Third-party smart contract security vulnerability scanner integrated into contract detail pages. | +| **Sponsored Transaction** | entity | **Eden**-specific transaction type (EIP-2718 type `0x76`): an executor submits an ordered batch of calls, while a separate sponsor signs for and pays the fee. No equivalent on standard EVM chains. | | **SUAVE** | chain | MEV-focused chain developed by Flashbots, built around a trusted execution environment (TEE) architecture. Introduces the **Kettle** entity. | | **TAC (Ton Application Chain)** | chain | A chain that bridges the TON blockchain and EVM ecosystems. Introduces the **Operation** entity. | | **Tx Actions** | feature | Structured per-transaction action breakdown rendered on the tx details page — a first-party Blockscout interpretation of what a tx did. Distinct from **Tx Interpretation** (natural-language summary) and from raw calldata. | diff --git a/.agents/tasks/3607-tx-details-fee-payer-calls/spec.md b/.agents/tasks/3607-tx-details-fee-payer-calls/spec.md new file mode 100644 index 0000000000..3c3cbd7256 --- /dev/null +++ b/.agents/tasks/3607-tx-details-fee-payer-calls/spec.md @@ -0,0 +1,275 @@ +# Display fee payer and calls on the transaction details page + +| | | +| --- | --- | +| Issue | https://github.com/blockscout/frontend/issues/3607 | +| Status | `done` | +| Size | `small` | +| Feature branch | `issue-3607` | +| PM | Ulyana | +| Designer | — | +| Backend | Victor (issue author); v11.2.4+ | +| Slack channel | — (default routing per `to-spec`) | + +## Context & goal + +Eden is an `ev-reth` / evstack rollup that adds a custom EIP-2718 transaction type `0x76` (decimal `118`): +a **sponsored batch transaction**. An *executor* submits an ordered batch of calls, and a separate *sponsor* +signs for and pays the fee. The backend now indexes those transactions and exposes two new optional fields +on the transaction model ([blockscout#14590](https://github.com/blockscout/blockscout/issues/14590), +implemented in [blockscout#14643](https://github.com/blockscout/blockscout/pull/14643)): `fee_payer` and +`calls`. + +Neither field is rendered today, so an Eden sponsored transaction page silently omits the two things that +distinguish it — who actually paid, and what the batch executed. The goal is to display both on `/tx/:hash`, +and to omit them cleanly on every other chain (where they are absent from the response entirely). + +## Functional requirements + +1. When `fee_payer` is present, the transaction details page shows a **Fee payer** row with the address. + Hint copy: `Address that paid the transaction fee on behalf of the sender`. +2. When `calls` is present and non-empty, the page shows a **Calls** row with a table of the batched calls + in API order. Hint copy: `Ordered list of calls batched into this sponsored transaction`. +3. The Calls table has three columns — `To`, `Value`, `Input`: + - `To` — `AddressEntity` with `truncation="dynamic"`. When `to` is `null` the cell reads + `[ Contract creation ]` (the same string [`TxDetails.tsx:395`](../../../src/slices/tx/pages/details/info/TxDetails.tsx) + already uses). `to` being null is a real case, confirmed by the backend owner (**Q3**). + - `Value` — `NativeCoinValue` with symbol, no exchange-rate toggle. + - `Input` — `TruncatedText` + `CopyToClipboard`, matching the `Data` cell of + [`LogDecodedInputDataTable`](../../../src/slices/log/components/LogDecodedInputDataTable.tsx). +4. Both rows render inside the collapsible *View details* section, immediately after **Other** and before + **Raw input** — so the batched calls sit next to the raw/decoded input data that shares their visual + language. No `DetailedInfo.ItemDivider` around the block (`Other`, `Raw input` and `Decoded input data` + have none between them either). +5. Neither field is gated by an env var or a feature config. They are rendered on **field presence**, + the established pattern for chain-variant transaction fields: `execution_node` / `allowed_peekers` at + [`TxDetails.tsx:308`](../../../src/slices/tx/pages/details/info/TxDetails.tsx) do the same, even though + SUAVE has an `NEXT_PUBLIC_IS_SUAVE_CHAIN` flag for its nav and pages. `do_with_chain_type_fields` only + extends the response for `:eden`, so presence is a sufficient and self-maintaining gate. +6. A **Sponsored** tag appears in the transaction details page header when `transaction_types` includes + `sponsored_transaction`, alongside the other header tags. Transaction **lists** get no badge for it + (**Q1**) — there is no room. `sponsored_transaction` is still added to `TYPES_ORDER`, last and with no + label of its own: absent from that list, `indexOf` returns `-1` and sorts it ahead of every real type, + making a sponsored contract call read as the generic "Transaction" instead of "Contract call". + +## Data & API + +**Endpoint** — `GET /api/v2/transactions/{hash}` (existing `core:tx` resource; no new API resource needed). + +**Readiness** — merged to backend `master` on 2026-07-31 and already deployed on +`eden-testnet.blockscout.com` (`backend_version: v11.2.4.+commit.ac947295`). Ships in backend tag **11.2.4** +(Q3) — worth naming in the frontend release notes. + +**Field shapes** — read from +[`schemas/api/v2/transaction.ex`](https://github.com/blockscout/blockscout/pull/14643/files) and verified +against a live response: + +- `fee_payer` — a full `Address` object, `nullable: true`. +- `calls` — `Array<{ to: AddressHashNullable; value: IntegerString; input: HexString }>`, `nullable: true`. +- `required: [:fee_payer, :calls]` means the keys are always present on an Eden response, not that the + values are non-null. + +Sample (`0x35310fd76c45f1441226c102f4dc1070b41ac66cb1e6ed3354da78aa69824a67` on `eden-testnet`): + +```json +{ + "type": 118, + "transaction_types": [ "sponsored_transaction" ], + "fee_payer": { "hash": "0x32648e6529BfCacE20422a7AA1E7fB7Bd8F408d7", "is_contract": false, "…": "…" }, + "calls": [ { "to": "0xf97cDCF1e5C0955Ed5c2EA0afb2c4Bb4eD506505", "value": "0", "input": "0x" } ] +} +``` + +The call's `to` is `null` on a contract creation — confirmed by the backend owner (**Q3**), not defensive +typing. + +**Scope of each field across endpoints** — `calls` is rendered for single-transaction responses only +(`prepare_calls` returns `nil` otherwise, the same policy the backend applies to token transfers). +`fee_payer` *is* returned on list endpoints too, but showing it there is out of scope. + +**Types package** — pinned at `@blockscout/api-types@0.0.1-beta.8e1692a`, published from backend `dev` once +`master` had been merged into it (**Q4**). It carries `eden.schema`, both fields on the transaction, and the +`operations` / `paths` shorthands the app depends on. + +Because `merged.schema` marks chain-specific properties **optional**, the fields type as +`Address | null | undefined` and `Array | null | undefined`. Guards must handle `undefined` as well as +`null`. + +**Env vars / feature flags** — none added. + +## UI inventory + +- **Single surface**: `/tx/:hash` details tab — + [`src/slices/tx/pages/details/info/TxDetails.tsx`](../../../src/slices/tx/pages/details/info/TxDetails.tsx), + inside the `CollapsibleDetails` block, between `` and the `Raw input` label. +- **New component**: `src/features/chain-variants/eden/pages/tx/TxDetailsEden.tsx` — renders both label/value + pairs and returns `null` when both fields are absent, so `TxDetails.tsx` composes it unconditionally + (matching `TxDetailsSetMaxGasLimit` and `TxDetailsWithdrawalStatusArbitrum`, already there doing the same). + Eden-specific UI is a **feature**, not a slice — it cannot exist on a vanilla EVM chain — and + [`TxDetails.tsx:83`](../../../src/slices/tx/pages/details/info/TxDetails.tsx) carries a standing + `// REFACTOR: Put feature related parts under the feature folder` note. No `config.ts` in the feature + folder: only chain variants needing an env flag have one (stability and zilliqa have none). +- **No Figma mockups** — none linked on the issue, and none needed. The Calls table reuses the styles of + [`LogDecodedInputDataTable`](../../../src/slices/log/components/LogDecodedInputDataTable.tsx): background + `{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }`, `p={4}`, `mt={2}`, `columnGap`/`rowGap={5}`, + `textStyle="sm"`, and header cells at `fontWeight={600} pb={1}`. One deliberate difference: **all four + corners are rounded** (`borderRadius="md"`), where the reference rounds only the bottom two because + `LogDecodedInputDataHeader` sits above it. Column template is `repeat(3, minmax(0, 1fr))` — equal widths + to start, tuned during verification (leaf 4). +- **Also affected by leaf 5**: the page header tags in + [`Transaction.tsx`](../../../src/slices/tx/pages/details/Transaction.tsx), and + [`TxType`](../../../src/slices/tx/components/TxType.tsx), which renders in the txs list, the home page + latest-transactions widget, and the address transactions tab. +- No new routes, navigation entries, or cross-links. +- No custom Mixpanel events: the only interactive elements are `AddressEntity` links and `CopyToClipboard`, + neither tracked elsewhere; there is no new page (view tracking is auto-wired) and no hardcoded external + link needing UTM params. + +## Out of scope + +- **Adapting the standard fields whose Eden semantics differ.** On a sponsored transaction the backend + derives `to` and `raw_input` from **call 0 only**, `value` from the **sum** of all calls, and `from` is + the *executor* rather than the fee payer. So on a multi-call transaction the "To" and "Raw input" rows + show one call while the Calls table shows all of them. The backend issue's UI requirements ask to "hide or + adapt standard fields whose Eden semantics differ" and to "present gas fields only where they are + meaningful"; #3607 asks for none of it. Raised as **Q2**, not blocking. +- A **Fee payer column in the transactions list**, even though the field is available there. +- The **Eden mainnet** dev preset (`eden.blockscout.com`) — it has no sponsored transactions to look at. +- A **Playwright visual scenario and transaction mock**. Dropped deliberately: the two rows use generic + building blocks already covered elsewhere, and a mock in `src/slices/tx/mocks/details.ts` exists only to + feed a `*.pw.tsx` scenario, so without one it would be dead code. Verification is against live + `eden-testnet` data. +- Backend work of any kind — already shipped. + +## Task breakdown + +- [x] 1 `[agent]` Add `eden` and `sponsored transaction` to `.agents/GLOSSARY.md` — skill: `update-glossary` + - done: `Eden` (chain) and `Sponsored Transaction` (entity) rows, cross-referencing each other + - inputs: + - `eden` — the chain type (`CHAIN_TYPE=eden`): an `ev-reth` / evstack rollup, explorers at + `eden.blockscout.com` and `eden-testnet.blockscout.com` + - `sponsored transaction` — scoped to Eden: EIP-2718 type `0x76` (decimal `118`); an executor submits an + ordered batch of calls and a separate sponsor signs for and pays the fee + - Also gets `eden` past cSpell, which has no entry for it today +- [x] 2 `[agent]` Add the `eden_testnet` dev-server preset + - done: `tools/dev-server/registry.json` + `pnpm presets:sync` (`deploy-review.yml`, `.vscode/tasks.json`) + - inputs: + - `"eden_testnet": "https://eden-testnet.blockscout.com"` in `tools/dev-server/registry.json` + - then `pnpm presets:sync` — regenerates the marker-bracketed alias lists in + `.github/workflows/deploy-review.yml` and `.vscode/tasks.json`; CI fails on drift + - Ordered before the UI leaves so their verification has a preset to run against +- [x] 3 `[agent]` Get `fee_payer` / `calls` into the pinned API types + - inputs: + - First check whether `@blockscout/api-types@0.0.1-beta.bb45bf1` already contains them; if so just bump + the pin in `package.json` + - Otherwise publish a beta from backend `master` via the `publish-beta-types` skill and pin that + - Verify afterwards that `schemas['TransactionResponse']` exposes `fee_payer` and `calls`, and that + `transaction_types` includes `sponsored_transaction` + - done: pinned `0.0.1-beta.8e1692a`, published from `dev` after `master` was merged into it (**Q4**). + `schemas['TransactionResponse']` exposes `fee_payer` and `calls`, and `transaction_types` includes + `sponsored_transaction`; `pnpm lint:tsc` is clean, so the merge cost the app no type churn. The interim + `eden/types/api.ts` shim is gone — the component reads both fields off the pinned schema. +- [x] 4 `[agent]` `[verify]` Build `TxDetailsEden.tsx` and wire it into the details page — requirements 1–4 + - inputs: + - New file `src/features/chain-variants/eden/pages/tx/TxDetailsEden.tsx`; no `config.ts` + - Composed unconditionally in `TxDetails.tsx` after ``, before the `Raw input` label + - Fully styled from the `LogDecodedInputDataTable` reference (see **UI inventory**) — this is a code + reference, not a mockup, so there is no separate `[human]` style leaf; width and spacing tweaks happen + during verification + - verify: `pnpm dev:preset eden_testnet`, open + `/tx/0x35310fd76c45f1441226c102f4dc1070b41ac66cb1e6ed3354da78aa69824a67`, expand *View details*, confirm + the Fee payer and Calls rows render correctly between Other and Raw input; adjust styles if needed. Also + open any non-Eden preset (e.g. `eth`) and confirm neither row appears. + - implemented: `TxDetailsEden.tsx` in the new `eden` feature folder, composed in + `TxDetails.tsx`; `dev-eden-testnet` added to `.claude/launch.json`. Functional check done on + `eden_testnet`: both rows render between Other and Raw input on the sponsored transaction, and both are + absent on a type-2 one; the table's computed styles match the reference (16px padding, 20px gaps, 12px + radius, `whiteAlpha.50`, 14px text, three equal columns). Styles reviewed and accepted by the developer + on 2026-08-04, with the column template tuned to `minmax(140px, 1fr) minmax(50px, 1fr) 1fr`; the designer + signed them off on the interim demo the same day. +- [x] 5 `[agent]` `[verify]` Show the **Sponsored** tag in the page header — requirement 6 + - inputs: + - Push a `{ slug: 'sponsored', name: 'Sponsored', tagType: 'custom' }` tag in `Transaction.tsx`, next to + the `relay_tx` / `init_tx` pushes that already feed `MetadataTags` + - Add `sponsored_transaction` to `TYPES_ORDER` last, with no `switch` case, so lists keep showing no + badge for it while the type stops masking more useful labels + - verify: on `eden_testnet`, open a sponsored transaction and confirm the header tag; check `/txs` still + labels a sponsored contract call as "Contract call" + - implemented: the header tag keys off `transaction_types`, so it carries no Eden-specific coupling. + `TxType.spec.tsx` pins both ordering outcomes. The dev server would not hydrate in the agent's browser + pane (Next dev's `_clientMiddlewareManifest.js` is served as JSON), so the header tag is verified by + types and tests only — confirm it visually on the next demo. +- [x] 6 `[agent]` Deploy a demo — skill: `deploy-demo` + - inputs: + - Run last, once every other box is checked + - done: deployed on 2026-08-04 from `2442fb48c` with the `eden_testnet` preset — + https://review-issue-3607.k8s-dev.blockscout.com — and shared in the Q1/Q2 thread, where the designer + signed off the styles. It covers leaves 1–4; the developer waived a redeploy for leaf 5, so the + **Sponsored** header tag is not on the demo. + +## Open questions + +### Q1 — Should a sponsored transaction get its own badge in the transactions list? + +The backend added `sponsored_transaction` to `transaction_types`, and the backend issue's UI requirements +ask for "a tag/badge such as `Sponsored`". #3607 does not mention it. Today the value falls through +`TxType`'s `default` branch to a generic purple "Transaction". If a dedicated badge is wanted: what label, +what colour (`purple` is the fallback's; `green` is unused), and what priority relative to "Contract call" / +"Token transfer" when a transaction is both? + +- Owner: PM (Ulyana) +- Status: `resolved` +- Slack: https://blockscout.slack.com/archives/C03MMUTQDNU/p1785778160250149 (reminder with the interim demo + at https://blockscout.slack.com/archives/C03MMUTQDNU/p1785851465775779) +- Answer: 2026-08-04 — answered by Nikita S. rather than Ulyana: a **Sponsored** tag in the details page + header, skipped in lists where it would not fit. Tags are in the SoW, but how to render them was left to + the team. +- Blocks: leaf 5 + +### Q2 — Should the compatibility fields be adapted on a multi-call sponsored transaction? + +`to` and `raw_input` reflect **call 0 only**, `value` is the **sum** across calls, and `from` is the executor +rather than the payer — so those rows can be misread on a batch of more than one call. Should they be +hidden, relabelled, or annotated for Eden, as the backend issue's UI requirements suggest? Shipping narrow +for now. + +- Owner: PM (Ulyana) +- Status: `waived` +- Slack: https://blockscout.slack.com/archives/C03MMUTQDNU/p1785778160250149 +- Answer: 2026-08-04 — deferred out of this task. Shipping the narrow scope; the team waits for client + feedback on whether the compatibility fields mislead in practice, and adapts them only if it does. + +### Q3 — Which backend release ships the Eden transaction fields? + +Needed for the frontend release notes. The PR merged to `master` on 2026-07-31 and `eden-testnet` already +runs it, but no tagged release is identified. Bundled with this: confirmation that the call's address is +nullable for a contract-creation call (read from the backend source, worth hearing from the owner before the +UI relies on it), and — if so — a request to correct #3607, which names the field `address_hash` where the +API and the OpenAPI schema both use **`to`**. + +- Owner: Backend (Victor) +- Status: `resolved` +- Slack: https://blockscout.slack.com/archives/D040DB9J5QQ/p1785778264416959 +- Answer: 2026-08-03 — backend tag **11.2.4**, planned for release that week. A call's `to` is confirmed + `null` on a contract creation, and #3607's description was corrected to name the field `to`. + +### Q4 — Which backend ref can publish api-types with both the Eden fields and the response shorthands? + +`@blockscout/api-types` betas are published from `dev`, which has no `eden` chain type. A beta published from +`master` (`0.0.1-beta.cf4c6f5`) has `eden.schema` plus `fee_payer` / `calls`, but its `index.ts` lacks the +`operations` and `paths` shorthands added by +[blockscout#14515](https://github.com/blockscout/blockscout/pull/14515) — the app imports those in 60+ +modules, and pinning that build yields 384 type errors across 227 files. So neither ref serves the frontend. +Can `master` be merged into `dev` (or #14515 forward-ported to `master`) so one ref carries both? Until then +the two fields are declared locally in the `eden` feature. + +- Owner: Backend (Victor) +- Status: `resolved` +- Slack: https://blockscout.slack.com/archives/D040DB9J5QQ/p1785780964199699 (compile failure reported at + https://blockscout.slack.com/archives/D040DB9J5QQ/p1785781999313979, the `HexString` rename at + https://blockscout.slack.com/archives/D040DB9J5QQ/p1785838420460469) +- Answer: 2026-08-04 — `dev` is the ref, once `master` was merged into it. Two follow-up fixes were needed: + a compile break the merge left in `read_system_config/2` (`7b60189`), then the Eden call schema still + naming `General.HexString`, which `dev` had renamed to `General.HexData` + ([#14656](https://github.com/blockscout/blockscout/pull/14656)). The publish from `8e1692a` then succeeded. +- Blocks: leaf 3 diff --git a/.claude/launch.json b/.claude/launch.json index b58d7bb472..3d21960168 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -7,6 +7,12 @@ "runtimeArgs": ["dev:preset", "staging"], "port": 3000 }, + { + "name": "dev-eden-testnet", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["dev:preset", "eden_testnet"], + "port": 3000 + }, { "name": "dev-eth", "runtimeExecutable": "pnpm", diff --git a/.github/workflows/deploy-review.yml b/.github/workflows/deploy-review.yml index f38bdb4a99..2858ceb965 100644 --- a/.github/workflows/deploy-review.yml +++ b/.github/workflows/deploy-review.yml @@ -30,6 +30,7 @@ on: - blackfort_testnet - celo - celo_sepolia + - eden_testnet - eth - eth_sepolia - filecoin diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2b89ab52f1..b07e8c3fa1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -328,6 +328,7 @@ "blackfort_testnet", "celo", "celo_sepolia", + "eden_testnet", "eth", "eth_sepolia", "filecoin", diff --git a/cspell.jsonc b/cspell.jsonc index 40048533ba..589d075f6a 100644 --- a/cspell.jsonc +++ b/cspell.jsonc @@ -115,6 +115,7 @@ "Emelyanov", "Enkrypt", "esbuild", + "evstack", "explorable", "facebookexternalhit", "favicons", diff --git a/package.json b/package.json index aef954d403..f1c91edc98 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@blockscout/admin-rs-types": "1.5.0", - "@blockscout/api-types": "0.0.1-beta.82839e44ce", + "@blockscout/api-types": "0.0.1-beta.8e1692a", "@blockscout/bens-types": "1.7.1", "@blockscout/contracts-info-types": "1.5.2", "@blockscout/interchain-indexer-types": "1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8264ab44ef..cebcd73028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: 1.5.0 version: 1.5.0 '@blockscout/api-types': - specifier: 0.0.1-beta.82839e44ce - version: 0.0.1-beta.82839e44ce + specifier: 0.0.1-beta.8e1692a + version: 0.0.1-beta.8e1692a '@blockscout/bens-types': specifier: 1.7.1 version: 1.7.1 @@ -1321,8 +1321,8 @@ packages: '@blockscout/admin-rs-types@1.5.0': resolution: {integrity: sha512-QE+dpUaQDvOAb/wUJ98J3CBqfohbiBW/+AEqEmSdOOZ0XxPRUrke08krBnflq3GMACRwB+nwzQrsfl7KCrC1Eg==} - '@blockscout/api-types@0.0.1-beta.82839e44ce': - resolution: {integrity: sha512-DIyMKLqHqXmKCbf1eYCKbWsNY3VQ2NMXAlLyAgEkM7UYffEasUfAL0othyHfGyxvM4FMwU5tm9hlKHRFPmKYew==} + '@blockscout/api-types@0.0.1-beta.8e1692a': + resolution: {integrity: sha512-itkaQdbtJSwsJL6LQBAPQlSWuejDhuuUXxZTcfwDAup/6L/OOw0l0XwIEVEtqTaLVIxG1DyGOrr7gKz2fPrukQ==} '@blockscout/bens-types@1.7.1': resolution: {integrity: sha512-MNIvYbj1I2vcU2rb6otlRnVMIpFG2B1WDqC7HicNrt10DbM5yPZNfBcsNM+Mhk/965Aeg529Fd+BkV9zhINqjA==} @@ -14846,7 +14846,7 @@ snapshots: '@blockscout/admin-rs-types@1.5.0': {} - '@blockscout/api-types@0.0.1-beta.82839e44ce': {} + '@blockscout/api-types@0.0.1-beta.8e1692a': {} '@blockscout/bens-types@1.7.1': {} diff --git a/src/features/chain-variants/eden/pages/tx/TxDetailsEden.tsx b/src/features/chain-variants/eden/pages/tx/TxDetailsEden.tsx new file mode 100644 index 0000000000..0a78dbb689 --- /dev/null +++ b/src/features/chain-variants/eden/pages/tx/TxDetailsEden.tsx @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Flex, Grid } from '@chakra-ui/react'; +import React from 'react'; + +import type { schemas } from '@blockscout/api-types'; + +import AddressEntity from 'src/slices/address/components/entity/AddressEntity'; + +import * as DetailedInfo from 'src/shared/detailed-info/DetailedInfo'; +import CopyToClipboard from 'src/shared/texts/CopyToClipboard'; +import NativeCoinValue from 'src/shared/values/entity/NativeCoinValue'; + +import { Skeleton } from 'src/toolkit/chakra/skeleton'; +import { TruncatedText } from 'src/toolkit/components/truncation/TruncatedText'; + +/** One call of a sponsored batch transaction. `to` is `null` for a contract-creation call. */ +type TransactionEdenCall = NonNullable[number]; + +interface Props { + data: schemas['TransactionResponse']; + isLoading?: boolean; +} + +const HeaderItem = ({ children, isLoading }: { children: React.ReactNode; isLoading?: boolean }) => { + return ( + + { children } + + ); +}; + +const CallRow = ({ to, value, input, isLoading }: TransactionEdenCall & { isLoading?: boolean }) => { + return ( + <> +
+ { to ? + : + [ Contract creation ] + } +
+
+ +
+ + + + + + ); +}; + +const TxDetailsEden = ({ data, isLoading }: Props) => { + const { fee_payer: feePayer, calls } = data; + + if (!feePayer && !calls?.length) { + return null; + } + + return ( + <> + { feePayer && ( + <> + + Fee payer + + + + + + ) } + + { calls && calls.length > 0 && ( + <> + + Calls + + + + To + Value + Input + { calls.map((call, index) => ( + // a batch can repeat the same call, so the position in the batch is the only stable key + + )) } + + + + ) } + + ); +}; + +export default React.memo(TxDetailsEden); diff --git a/src/slices/tx/components/TxType.spec.tsx b/src/slices/tx/components/TxType.spec.tsx new file mode 100644 index 0000000000..f17169a9be --- /dev/null +++ b/src/slices/tx/components/TxType.spec.tsx @@ -0,0 +1,25 @@ +// @vitest-environment jsdom +// SPDX-License-Identifier: LicenseRef-Blockscout + +import React from 'react'; + +import { afterEach, describe, expect, it } from 'vitest'; +import { cleanup, render, screen } from 'vitest/lib'; + +import TxType from './TxType'; + +describe('TxType', () => { + afterEach(cleanup); + + it('prefers a more informative type over sponsored_transaction', () => { + render(); + + expect(screen.queryByText('Contract call')).not.toBeNull(); + }); + + it('falls back to the generic label when a transaction is only sponsored', () => { + render(); + + expect(screen.queryByText('Transaction')).not.toBeNull(); + }); +}); diff --git a/src/slices/tx/components/TxType.tsx b/src/slices/tx/components/TxType.tsx index 9a0436fd02..2ce14cc069 100644 --- a/src/slices/tx/components/TxType.tsx +++ b/src/slices/tx/components/TxType.tsx @@ -21,6 +21,10 @@ const TYPES_ORDER: schemas['Transaction']['transaction_types'] = [ 'token_transfer', 'contract_call', 'coin_transfer', + // Listed last and deliberately given no label of its own — the details page header carries the + // "Sponsored" tag instead, and lists have no room for it. An unlisted type would score -1 here and sort + // ahead of every real one, masking labels like "Contract call". + 'sponsored_transaction', ]; const TxType = ({ types, isLoading, ...rest }: Props) => { diff --git a/src/slices/tx/pages/details/Transaction.tsx b/src/slices/tx/pages/details/Transaction.tsx index bce0a3e233..2a14a6dc8e 100644 --- a/src/slices/tx/pages/details/Transaction.tsx +++ b/src/slices/tx/pages/details/Transaction.tsx @@ -108,6 +108,10 @@ const TransactionPageContent = () => { } } + if (data?.transaction_types?.includes('sponsored_transaction')) { + txTags.push({ slug: 'sponsored', name: 'Sponsored', tagType: 'custom' as const, ordinal: 0 }); + } + const protocolTags = data?.to?.metadata?.tags?.filter(tag => tag.tagType === 'protocol'); if (protocolTags && protocolTags.length > 0) { txTags.push(...protocolTags); diff --git a/src/slices/tx/pages/details/info/TxDetails.tsx b/src/slices/tx/pages/details/info/TxDetails.tsx index 77ff4bd067..fdc8c64818 100644 --- a/src/slices/tx/pages/details/info/TxDetails.tsx +++ b/src/slices/tx/pages/details/info/TxDetails.tsx @@ -24,6 +24,7 @@ import LogDecodedInputData from 'src/slices/log/components/LogDecodedInputData'; import TxSocketAlert from 'src/slices/tx/components/TxSocketAlert'; import getConfirmationDuration from 'src/slices/tx/utils/get-confirmation-duration'; +import TxDetailsEden from 'src/features/chain-variants/eden/pages/tx/TxDetailsEden'; import TxAllowedPeekers from 'src/features/chain-variants/suave/pages/tx/TxAllowedPeekers'; import TxDetailsTacOperation from 'src/features/chain-variants/tac/pages/tx/TxDetailsTacOperation'; import TxDetailsCrossChainMessages from 'src/features/cross-chain-txs/pages/tx/TxDetailsCrossChainMessages'; @@ -807,6 +808,8 @@ const TxDetails = ({ data, isLoading, socketStatus, noTxActions }: Props) => { + +