feat: migrate bridge backend from Bridge Hub API to aggkit bridge service (+ bridge tracker UI, rc5) - #24
feat: migrate bridge backend from Bridge Hub API to aggkit bridge service (+ bridge tracker UI, rc5)#24arnaubennassar wants to merge 49 commits into
Conversation
Swap the three REST call sites to the aggkit bridge service via the `@agglayer/sdk` AggkitBridgeAggregator: - Config: replace `bridgeHubApiBaseUrl`/`proofApiSuffix` with per-mode `aggkitBridgeApis` (map networkId -> url) + `NEXT_PUBLIC_AGGKIT_BRIDGE_APIS` env override; Zod schema + validator updated. - Services (transactions/claimProof/tokenMetadata) rewritten as thin SDK wrappers preserving the hook-facing shapes; adapter maps aggkit fields to the stable `Transaction`/`TransactionStatus` types. - Fix useClaimExecution: source isClaimed's deposit_count separately from the claim-proof leaf index (previously conflated). - Per-network partial-failure notice in the activity view; ready-to-claim badge tolerates partial failure. - Docs, .env sample, wrangler, and CI workflow env var updated to aggkit. - Add scripts/kurtosisDevnetEnv.mjs and retarget the Playwright E2E suite to a local kurtosis + aggkit devnet (env-parameterized, testnet fallback). Local SDK linked via pnpm-workspace override (file:../sdk) for development; switch to the published @agglayer/sdk beta at release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
…tus polling - wallet.tsx: on connect, steer the wallet to the default source chain (DEFAULT_WAGMI_CHAIN) instead of leaving it on whatever it connected with (e.g. Ethereum mainnet), so the network add/switch prompt happens at connect time rather than only when a bridge is initiated. - kurtosisDevnetEnv.mjs: set appModes.default = "devnet" locally (devnet-prep tool; committed config keeps testnet), and write the devnet chains' rpcUrl as the enclave CORS proxy paths (/l1rpc, /l2rpc) instead of the direct EL ports (which send no CORS headers). Direct ports are still used for the script's own host-side chainId/bytecode checks. - useTransactions / useReadyToClaimCount: aggkit has no push and status is derived per fetch, so poll to keep the activity view live — fast (5s) while any loaded tx is non-terminal, slower idle (10s) otherwise so newly-submitted deposits still appear; ready-to-claim badge polls every 15s. Fixes pending spinners never resolving and L2->L1 deposits taking a long time to appear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
When an autoclaim service is expected to claim a route on the user's behalf,
avoid showing "Claim tokens" immediately once a deposit is READY_TO_CLAIM.
- config: optional per-route `autoclaim` block (l1_to_l2 / l2_to_l1 / l2_to_l2),
each { expectedAutoclaim, waitForAutoclaimMs }, Zod-validated; app/config.ts
applies defaults for any omitted route/field. Defaults: L1->L2 expected/60s,
L2->L1 not expected, L2->L2 expected/120s.
- Route type derived from the tx's recording source + destination network
(L1 == networkId 0).
- transactionListItem: for a READY_TO_CLAIM deposit ->
* expectedAutoclaim false: show "Claim tokens" as before.
* within the grace window: show "Waiting for auto claim, claim manually now"
(the link runs the same claim action).
* grace elapsed: show "Claim tokens" + "auto claim is taking more time than
expected, you can claim manually instead".
- Grace measured from when the deposit is first observed READY_TO_CLAIM
(persisted in localStorage so it survives refreshes); flips waiting->overdue
via a one-shot timer. Pure route/gate logic unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
…dation Discovers L2 suffixes and the haproxy proxy service from a single `kurtosis enclave inspect` call (with --l2-suffixes/--proxy-service escape hatches) instead of hardcoding `agglayer-dev-ui-proxy-001`, which no longer exists in the S5 bring-up (the live service is `-002`). Every chain id / bridge-contract check now runs through the same haproxy route that ends up in config.json (never a direct EL/aggkit port), and the aggkit health probe -- which 404s through the proxy -- is replaced by the per-network `sync-status?network_id=N` check, turning "networkId = Number(deployment_suffix)" from an assumed convention into a verified fact per run. The script's own config.json write is now formatted through the repo's prettier config so re-runs never introduce formatting-only diff noise. Adds chains<->aggkitBridgeApis cross-field validation (config/configValidator.mjs) so a chain configured without a matching aggkit backend (or vice versa) fails config validation loudly instead of silently dropping rows for one network. Updates the committed config.json: devnet's placeholder block now models two L2s (DEVNET_L2_001/DEVNET_L2_002) instead of the single stale DEVNET_L2 entry, and the autoclaim grace windows are retuned to the measured idle-enclave figures (l1_to_l2 120s, l2_to_l2 300s) now that the destination-injected-leaf gate lands READY_TO_CLAIM later than before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…of literal "all" The status Dropdown maps its null "no filter" sentinel to the string 'all' for display, but onSelect never mapped it back -- so selecting "All transactions" sent status: 'all' all the way through to services/transactions.ts, which compared it against every real TransactionStatus value (never matching) and rendered "No transactions found" even though the total-count stat was nonzero. Manual S10 validation reproduced this against a live devnet aggkit backend before fixing it. Also adds the claim-button/grace-note data-test-ids design.md already specified (claim-tokens-button, claim-manually-now-button, autoclaim-waiting-note, autoclaim-overdue-note) but that were never wired up -- needed to drive the manual L2->L1 claim journey and will save S11 from re-deriving selectors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…im specs Adds E2E_TO_CHAIN_ID/E2E_L2_CHAIN_IDS consumption and two new route-budget timeouts to app/constants/e2e.ts, additive chain-selector/status testids (Dropdown, from/to sections, TransactionStatusBadge), and page-model support for explicit chain-pair selection (destination excludes source, from-then-to ordering enforced). New specs: l2-to-l2.spec.ts (L2-1->L2-2, autoclaimed end state, 300s budget) and manual-claim.spec.ts (L2-1->L1, non-autoclaiming by config, real click->CLAIMED, 600s ready + 150s claim budgets). Existing bridge specs (smoke/native-bridge/token-selector/erc20-approve-bridge/claim-autoclaim) now click both chain selectors explicitly instead of relying on config.json defaults. claim-autoclaim.spec.ts's obsolete 0.36s-race comment is rewritten to point at manual-claim.spec.ts. preflight.spec.ts now iterates every aggkitBridgeApis entry with sync-status?network_id=N (plus network_id=0 once) and drops the health- endpoint check, which 404s through aggkit-proxy in this topology (verified live against the devnet enclave). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…uous Close selector
manual-claim.spec.ts's L2-1->L1 withdrawal deterministically reverted with
LocalBalanceTreeUnderflow whenever it ran right after l2-to-l2.spec.ts in the
same enclave: claim-autoclaim.spec.ts credits L2-1's AgglayerBridgeL2
LocalBalanceTree with exactly one E2E_NATIVE_BRIDGE_AMOUNT (a claimed L1->L2-1
deposit), and l2-to-l2.spec.ts spends exactly one back out, netting to zero
before this spec's own withdrawal needs to spend from it. Confirmed via `cast
4byte-decode` against the eth_estimateGas revert data during S12 triage.
Spec now funds its own L1->L2-1 top-up deposit first, making the withdrawal
assertion independent of run order and other specs' side effects.
Also fixes a strict-mode-violating `getByRole('button', { name: 'Close' })`
(substring-matched both the Modal's own "Close modal" icon button and the
CTA's "Close" text) with `exact: true`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
Document the 2-L2 devnet setup, devnet script discovery flags, proxy URL semantics, and E2E testing across multiple chains: README.md: - Clarify that kurtosisDevnetEnv.mjs discovers L2 suffixes automatically (default 001,002) - Explain proxy URL semantics: same URL for multiple networkIds, routed via ?network_id= - Add escape hatch flags: --l2-suffixes, --proxy-service, --enclave - Document 2-L2 E2E-specific env vars (E2E_TO_CHAIN_ID, E2E_L2_CHAIN_IDS) - Add per-spec breakdown table and testing notes for all devnet specs - Clarify autoclaim latencies and certificate-cadence timeout risks - Note shared wallet state accumulation across runs (benign for current suite) docs/config.md: - Clarify that devnet aggkitBridgeApis uses identical proxy URL under multiple keys - Show RPC route map for haproxy (/l1rpc, /l2rpc*, /aggkitapi) - Distinguish between devnet mode (proxy + auto-discovery) and mainnet/testnet modes - Document manual setup steps for advanced use cases These docs enable E2E testing of 2-L2 bridging with minimal setup overhead via the automated devnet script, while documenting edge cases (certificate cadence, wallet state) found during S12 testing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…uplicate networkIds S14 adversarial review. Four findings, each verified against running code rather than inferred. 1. l2-to-l2.spec.ts had the SAME unfixed LocalBalanceTree dependency that commit 5138771 fixed in manual-claim.spec.ts -- the debit side of the same ledger. Its L2-1->L2-2 native bridge spends L2-1's origin-network-0 credit, which is created only by a *claimed* inbound deposit, and it silently relied on claim-autoclaim.spec.ts having run first (true only because the filenames sort that way). Standalone, sharded, -g filtered, or after any rename that reorders, it reverts LocalBalanceTreeUnderflow inside eth_estimateGas and surfaces as an opaque waitForBridgeSuccess timeout. 5138771's own commit message named this spec as the spender, so the ledger was understood and only half fixed. The top-up is now a shared BridgePage.fundLocalBalanceTree helper (one implementation, documenting why the credit cannot be inherited); manual-claim.spec.ts moves onto it too, replacing its inline copy. 2. E2E_L2_TO_L2_CLAIM_TIMEOUT_MS default 300s -> 600s. l2-to-l2 failed on the first post-fix run at 300s; the enclave showed why -- the deposit's /l1-info-tree-index still 500'd ("not been included on the L1 Info Tree yet") at 321s, and aggkit-001's certificate 14 covering the deposit's block settled only at ~329s. The dominant term is source-side certificate settlement, not autoclaim, and it grows with enclave age plus any certificate already in flight -- which the new top-up deliberately creates. Sampled range across rounds is now 87s..330s. The old comment's premise was also wrong: MinimumNewCertificateInterval is a maximum-idle heartbeat, not a floor on certificate spacing. 3. partial-failure.spec.ts asserted "Total transactions:", which transactionsView renders only when totalCount > 0 over a wallet-filtered fan-out -- another accumulated-history dependency, failing on a fresh enclave or when run as the only project. Now accepts either settled branch of transactionList (populated summary or the empty state), which is what the partial-failure contract actually claims: the healthy network's query resolves rather than hanging. 4. configValidator: two chains in one app mode could share a networkId and pass. networkId -- not chain id -- keys aggkitBridgeApis and the SDK's per-network clients, so that shape collapses two chains onto one backend and merges one chain's rows into the other's, while satisfying both existing cross-field rules (every chain finds a key, every key finds a chain). This is the failure the rules exist to prevent, reached from the other side, and kurtosisDevnetEnv.mjs deriving networkId from the deployment suffix is exactly how it would arise. Added the check, two unit tests, and the rule table in docs/config.md. L1 chains (networkId 0) stay exempt. Verified: pnpm run check green (validate:config, lint 0 errors/3 pre-existing BigNumber warnings, typecheck, 28 unit tests -- up from 26). Against the live 2-L2 enclave, each affected spec run STANDALONE, which is the property that was broken: l2-to-l2 passed in 6.0m, manual-claim in 6.8m, partial-failure in 8.0s. config.json's devnet-script mutation was reverted, not committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…mi's store chain useEnforceCorrectChain compared useChainId() (wagmi's app-store chain) against the target before switching. With a real extension wallet (observed with Rabby) the store chain diverges from the wallet's per-dapp chain, so the guard skipped the switch and the subsequent sendTransaction threw ChainMismatch before the wallet was asked to sign — surfacing as a generic 'Transaction failed' for any L2-origin bridge (the connect-time auto-switch pins the wallet to L1, so only L1-origin sends happened to match). useAccount().chainId is the same source the send path's live check uses, so guard and enforcement now agree. Not caught by E2E: the mock wallet keeps both in sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
The transaction modal intentionally collapses every failure into a generic message, which made real-wallet failures undiagnosable — the thrown error was neither displayed nor logged anywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…polling
Polls AggkitBridgeAggregator.getBridgeTracking per row, keyed by the
transaction's recording network + hash. Stops once tracking_status is
'finished' or the tracker gave up ('error' with bridge_status still
null); keeps polling through step-level errors and registered-state
regressions. Only mounted for currently rendered, non-CLAIMED rows at
the same 5s cadence as useTransactions, to stay under the proxy's
50 req/IP/s budget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ooltips Non-completed transaction rows now mount a TrackerProgressBar (wraps useBridgeTracking) below the row body: one dot per all_steps entry, filled green when done, pulsing blue when inProgress, hollow when pending, red on error, with a connector line filled up to the last done dot. A new CSS-only Tooltip primitive (app/components/ui/tooltip.tsx) shows humanized step copy from app/utils/trackerSteps.ts on hover. Renders nothing while all_steps is still null (registered/giving-up) and nothing for CLAIMED rows, since the hook disables its query for those. Verified live against the cdk enclave: a fresh L1->L2 bridge shows 4 dots, L2->L2 shows 7, and the parked L2-1->L1 WaitingClaim row shows 6, all advancing over successive polls; CLAIMED rows confirmed bar-free across multiple rows. Screenshots under /home/brolygon/repos/plans/bridge-tracker/evidence-s7/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ils modal Adds a TrackerDetail section to TransactionDetailsModal for any non-CLAIMED row: overall tracking status + bridge typology, then a vertical timeline of every all_steps entry (label, status, start/end dates, per-step result detail -- GER/LER hashes, certificate id/status/settlement tx, claim tx -- and step-level ErrorStep as a non-terminal warning). Reuses useBridgeTracking (react-query dedupes with the row's own poll) and trackerSteps.ts's label helper. CLAIMED rows render no tracker section and never poll (query stays disabled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Covers useBridgeTracking's refetchInterval terminal/non-terminal gating (finished, giving-up error, step-level error) plus trackerProgressBar and trackerDetail's null-steps/CLAIMED-row rendering, per-typology dot counts, tooltip copy, and per-step result detail -- using trimmed local copies of the captured aggkit tracker fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ition useBridgeTracking disables its query once a transaction's status is CLAIMED, but disabling a react-query query only stops future refetches -- it does not clear already-cached data for that query key. A row that transitions live from non-CLAIMED to CLAIMED while mounted (as opposed to loading already-CLAIMED, which never populates the cache in the first place) kept serving its last-fetched, fully-done all_steps from cache, so TrackerProgressBar/TrackerDetail kept rendering instead of disappearing as intended. Found via the S10 E2E spec exercising this transition against the live enclave. Guard both components on transaction.status === 'CLAIMED' directly rather than relying on data/steps becoming falsy. Added a regression test to each component's suite that keeps the mocked hook returning the same finished fixture across a rerender (simulating the stale-cache behavior) and asserts the guard hides the bar/timeline anyway; confirmed both fail without the guard and pass with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…modal detail Two devnet-only specs against the live enclave: L1->L2 (4-step bar renders in order, at least one dot reaches done, bar disappears once the row completes) and L2-1->L2-2 (7-step bar, modal tracker-detail asserted mid-flight against the route's real step labels, bar disappears on completion). Mid-state dot transitions are recorded as test annotations rather than asserted, since exact per-step timing races the tracker's own 5s poll interval; the modal-detail check lives on the L2->L2 leg because its multi-minute window is what makes catching it mid-flight reliable (L1->L2 can autoclaim in ~35s). Adds page-model locator factories used by the new spec: getTrackerBar, getTrackerStep, trackerDetail/getTrackerDetailStep, openTransactionDetails/closeTransactionDetailsModal. Purely additive -- existing bridge specs are unaffected. This spec is what surfaced the live-CLAIMED-transition bug fixed in the prior commit; both tracker.spec.ts tests and the full chromium project (14 tests) pass against the live enclave with that fix in place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Add a "Bridge Tracking" section covering: what useBridgeTracking polls and at what cadence, the progress-bar/detail components, the dot color legend (done/inProgress/pending/error), the CLAIMED and null-all_steps render-guard rules, the step_name -> label mapping table from trackerSteps.ts, and the data-test-id list for E2E authors. Also adds the tracker.spec.ts row to the "E2E Specs by Mode" table. No CHANGELOG exists in this repo, so none is touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…or' per aggkit aggkit v0.11.0-rc4 derives tracking_status from the step at step_index (bridgetracker/domain/tracking_data.go), so a step in 'error' reports tracking_status 'error' with bridge_status still populated -- the synthesized step-error fixture wrongly kept 'running', which meant the bridge_status-null conjunct of isTrackingTerminal's giving-up check was never exercised (dropping it survived the whole suite). With the fixture corrected, that mutation now fails the step-error polling test. Also corrects the hook's step-error comment to match, and rewrites the rate-limit budget comment to account for the unvirtualized infinite scroll accumulating mounted rows across loaded pages (the previous per-page math understated worst-case traffic). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
A query error (react-query exhausting its default retries on a transient proxy blip or rate-limit burst) permanently stopped that row's tracker polling until remount, silently freezing the bar for a passively-watching user. Drop the error early-return so refetchInterval falls through to isTrackingTerminal, which is already undefined-safe; only genuine terminal states (plus CLAIMED via `enabled`) stop polling now. Accepted trade-off: a permanently-failing request would poll harmlessly every 5s instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Measured a live L2->L1 bridge on the cdk devnet enclave: the tracker's
WaitingClaim step (previously labeled "Ready — waiting for the
claim…") reliably fires ~22s before the row's READY_TO_CLAIM status
(and thus the "Claim tokens" button) actually becomes true. Root
cause is upstream in aggkit, not a dev-ui poll/cache bug: the
tracker's WaitL1SettledGER resolver reads the settlement tx's own L1
receipt directly, while READY_TO_CLAIM and the real claim-proof fetch
both depend on the origin bridge-service's separate, slower
L1-info-tree sync. The button gating is correct and unchanged; only
the misleading "Ready" copy is fixed, to "Finalizing claim data for
{destination}". See plans/bridge-tracker/tracker-vs-claim-lag.md for
the full measured timeline and root-cause writeup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…real project id Derives WalletConnect's metadata.url from window.location.origin (SSR-safe fallback to the production domain) instead of a hardcoded literal, so local dev no longer trips AppKit's "metadata.url differs from the actual page url" warning (console-triage.md row 7). Also implements the plan's standing decision for a placeholder/empty NEXT_PUBLIC_PROJECT_ID: rather than requiring a real WalletConnect Cloud id, createAppKit now passes `basic: true` when isPlaceholderProjectId() detects one, skipping AppKit's remote-config fetch and trimming its modal-open asset prefetch (verified against the installed @reown/appkit sources not to touch EIP-6963/injected wallet detection, which is what this app's connect flow actually depends on). The remaining Reown/ WalletConnect calls that no public AppKit option can suppress stay documented as environmental in console-triage.md. Adds tests/bridge/console-hygiene.spec.ts, a Playwright regression spec that fails on any unexpected console error/warning during the core transactions journey, with an explicit allowlist citing each triage row it covers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ix stale docs - Vendor the apps-team CLAUDE.md gist as docs/team-standards.md (reviewed, benign) and replace the live-fetch directive with a pointer + revision hash. - Graduate plans/bridge-tracker/bridge-ui-aggkit-proxy-deploy.md into docs/deployment.md: re-pin all aggkit links to v0.11.0-rc5, rewrite the two gotchas for rc5 behavior (#1782 fixed, #1783 unenforced-by-design), fix the dangling bridgetracker.md link, and add a Rollback section per layer. Link it from README next to the Configuration section. - Rewrite the useBridgeTracking rate-limit comment: aggkit enforces no in-process limit (#1783); the load math is now framed as courtesy/capacity, not a hard budget. - Document NEXT_PUBLIC_PROJECT_ID's optional/degraded-mode semantics in .env.example, README, and docs/config.md; drop it from the E2E-required list. Fix .env.production's placeholder so an unset build secret degrades gracefully instead of shipping an invalid project id. - Replace dangling plans/-path and design.md/enclave-notes.md references (README, trackerSteps.ts, tracker.spec.ts, console-hygiene.spec.ts) with durable upstream issue links or self-contained comments. - Cross-link the kurtosis guide's enclave-reset recovery section from README's Kurtosis devnet bring-up section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
…ailure cause The old comment said "revert to a semver range once the aggkit SDK surface ships to npm (tracked for S15)" but didn't mention that the file:../sdk override is the direct reason `pnpm install --frozen-lockfile` fails on GitHub CI (ENOENT scandir on the missing sibling ../sdk checkout) for both the lint/typecheck/test and e2e workflows on PR #24. Rewrite the comment to state that plainly and spell out the dependency ordering to clear it: agglayer/sdk#28 merges -> a tracker-API sdk version is published via workflow_dispatch -> swap this override to that published semver. Local dev and local CI-equivalent checks are unaffected since a sibling ../sdk checkout is present here; only GitHub's runners lack one. Comment-only change, no functional effect: lint/typecheck/test (54 tests) stay green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
The comment's "first-match wins" caveat about L1 (networkId 0) colliding with the SDK's pre-registered mainnet/Sepolia defaults was already resolved upstream by sdk commit 8094f922 (consumer-registered chains now win networkId collisions) -- confirmed fixed and consumed here via the file:../sdk workspace dependency. The design-doc citation sweep (ede8c0e) kept this caveat faithful-minus-citation rather than silently rewriting its semantics; this follow-up updates the claim itself to match current SDK behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Move configuration from a build-time import to a runtime fetch, so one prebuilt image can be repointed at any network by replacing config.json at container start. Add the dev-ui's own Dockerfile and GHCR publishing. Config is now fetched from /config.json at runtime instead of being imported and baked into the bundle. app/config.ts becomes an accessor facade over a module store; AppConfigGate resolves config before any provider mounts, so wagmi/AppKit construction no longer happens at import time. A shared validate-and-normalize path serves both browser (fetch) and Node (fs) callers. Origin-relative aggkitBridgeApis values are supported and resolved against window.location.origin. The container is nginx:alpine serving a prebuilt static export (106MB, ~280ms to first response). config.json is served no-store while _next/static stays immutable; the entrypoint validates a mounted config structurally and fails fast, warning loudly when none is mounted. Every absolute URL in config.json is constrained to http/https. zod's .url() accepts any scheme, so a javascript: value in explorerUrl reached window.open() and executed with the app's origin. Until @agglayer/sdk publishes a version carrying the aggkit APIs, the image builds the sdk from a pinned commit staged into the build context. See plans/dev-ui-docker-ghcr/d2-adr-dependency-strategy.md for the removal trigger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
A workflow_dispatch or release trigger is only registered once the workflow file exists on the repo's default branch, so dispatching docker-publish.yaml from a feature branch fails with: HTTP 404: workflow docker-publish.yaml not found on the default branch This is separate from, and blocks earlier than, the already-documented limitation that dispatch cannot target a bare commit SHA. PR-triggered builds are unaffected because they run from the PR head, which is why docker-pr.yaml worked while dispatch did not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Restructure e2e.yaml to check out dev-ui into a subdir with a sibling agglayer/sdk checkout pinned at SDK_REF (mirrors docker-pr.yaml's parity-checked pattern), so pnpm-workspace.yaml's `file:../sdk` override resolves and `pnpm install --frozen-lockfile` no longer fails by design. Add config/config.ci.devnet.json (devnet-mode fixture, URLs already pointed at the future vendored bundle's 127.0.0.1:8555 proxy) and scripts/devnetReady.mjs (a kurtosis-CLI-free readiness gate replicating kurtosisDevnetEnv.mjs's chainId/bridge-bytecode/sync-status probes against fixed ports, upgraded to require is_active alongside is_synced). Add a test.skip mode-gate to preflight.spec.ts so a devnet-less run skips instead of failing. Neither the devnet compose bring-up nor the literal env values are wired into the workflow yet -- that lands separately, alongside the vendored bundle it depends on.
Vendor tests/devnet/{docker-compose.yml,summary.json} from
0xPolygon/kurtosis-cdk's snapshot-devui.yml (run 31616965584, artifact
devui-snapshot-snapshot-d5dea8709c8d): a self-contained, anvil-based
11-service devnet (L1, 2 anvil L2s, agglayer, aggkit x2 + bridge sidecars,
aggkit-proxy, the CORS haproxy, dev-ui) with no bind mounts or volumes.
Repoint the compose file's default image references at the published,
pinned GHCR tag (ghcr.io/0xpolygon/kurtosis-cdk-snapshot-<service>:
snapshot-d5dea8709c8d -- all 11 packages public, verified pullable after
`docker logout ghcr.io`) instead of the run's local build tag. Document
the regenerate-and-bump procedure in tests/devnet/README.md.
Rewire e2e.yaml to actually use it: after install, bring the bundle up
(`docker compose ... up -d --wait`), gate on scripts/devnetReady.mjs,
then stamp in the devnet config fixture (`cp config/config.ci.devnet.json
config.json && pnpm run validate:config`) before either Playwright
invocation. Replace the secrets-backed env block with literal, public
devnet-fixture values (the E2E signer key is a well-known Kurtosis/
Foundry devnet key usable only against this job's own throwaway
containers; E2E_ERC20_ADDRESS is read from summary.json), drop
`environment: production` (nothing here needs an approval gate), and
raise timeout-minutes to 75 to cover the bridge suite's slow, variable
L2->L1 certificate-settlement leg. Add an always()-run devnet-logs dump
and teardown step, uploaded alongside the Playwright report.
Verified locally: anonymous pull of all 11 pinned images, `docker compose
up -d --wait` healthy in 24s, devnetReady.mjs green against the live
bundle, and both Playwright invocations (preflight full run, bridge
suite --list) resolve cleanly under the exact literal env this workflow
now sets. actionlint clean; no `secrets.*` references remain for any
devnet value.
A fresh actions/checkout of agglayer/sdk at SDK_REF has no dist/ (it's gitignored -- the repo ships bun.lock and TypeScript source only), so pnpm-workspace.yaml's `file:../sdk` override resolves to a package whose package.json `main`/`exports` point at a directory that doesn't exist yet. `pnpm install --frozen-lockfile` doesn't catch this (it only symlinks node_modules/@agglayer/sdk -> ../sdk), but Next's dev server then fails every route with "Module not found: Can't resolve '@agglayer/sdk'", timing out Playwright's webServer wait. This was invisible in every prior local rehearsal on this dev box because its sibling sdk/ checkout already has a built dist/ from earlier SDK development -- a genuine scratch clone (two fresh actions/checkout- equivalent clones, no reused sibling state) is what surfaces it, exactly the kind of CI-only gap plans/dev-ui-ci-snapshot-plan.md warns about. Fix: add a bun-based build step for the sibling sdk/ checkout, mirroring the Dockerfile's sdk-builder stage (`FROM oven/bun:1 ... RUN bun install --frozen-lockfile && bun run build`) bit-for-bit so the two build paths for this pinned SDK_REF never diverge. Verified: a from-scratch two-repo clone (dev-ui + sdk, no shared state) now installs, builds the sdk, brings up the vendored devnet, passes devnetReady.mjs, and runs both Playwright invocations under the workflow's literal env -- see plans/dev-ui-ci-snapshot/s13-evidence/.
The checkout subdir restructuring (S12) moved package.json to dev-ui/, but pnpm/action-setup@v4 defaults to reading package.json from $GITHUB_WORKSPACE root, which no longer has one -- the action failed with "No pnpm version is specified." on the first live e2e.yaml run against this branch (run 31620612259). Point it at dev-ui/package.json (packageManager: pnpm@10.30.3), mirroring the node-version-file: dev-ui/.nvmrc pattern already used for actions/setup-node below. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
…ink the snapshot doc (S16) README's Testing section gains a "CI-devnet quick start" subsection for the vendored tests/devnet/ compose bundle (complements, not replaces, the live Kurtosis bring-up above it). docs/config.md documents config/config.ci.devnet.json's shape and purpose. tests/devnet/README.md gains a cross-link to kurtosis-cdk's new anvil-devui-snapshot.md doc for the bundle's topology/hazards/summary.json reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Independent review pass over the vendored-devnet e2e work. CI - e2e.yaml: add an automated parity guard asserting the job's hand-copied E2E_* literals and the compose file's pinned snapshot tag match tests/devnet/summary.json. Nothing in the repo read summary.json programmatically, so a half-done bundle bump would have left the suite silently testing stale values. Same drift class -- and same treatment -- as the existing SDK_REF parity check. Verified to pass on the current bundle and to fail on a mutated address. - e2e.yaml: upload artifacts with `if: always()`. `!cancelled()` skipped the upload on a timeout cancellation, which is exactly the case the `if: always()` devnet log dump exists for -- the logs were collected and then discarded. - e2e.yaml: `|| true` on the SDK_REF grep. Under `set -e` a non-matching grep aborted the assignment, making the friendly ::error:: branch unreachable. Docs vs behavior - e2e.yaml: the images are pinned by a mutable TAG containing a sha, not by a digest -- the comment claimed "immutable sha tag". - e2e.yaml: the readiness-gate rationale was wrong. The aggkit-proxy image's own healthcheck already asserts is_synced && is_active for networks 0/1/2, so `up -d --wait` cannot return before that holds. The gate's real added value is the chainId/bytecode probes THROUGH haproxy, which no healthcheck covers. Boot-to-ready comments said ~24s; CI measured ~65s including the image pull. - docs/config.md: config.ci.devnet.json differs from the committed config.json in exactly ONE key (appModes.default); the doc described three chain entries and a whole appModes block as additions that are already in the committed file. Also removed a stale note asserting the committed config.json defaults to "devnet" -- it defaults to "testnet", which the section immediately below it said. - README.md: the local quick start could not work as written -- playwright.config.ts throws without E2E_PRIVATE_KEY / NEXT_PUBLIC_PROJECT_ID / NEXT_PUBLIC_AGGKIT_BRIDGE_APIS, and without E2E_ERC20_ADDRESS globalSetup falls back to deploying its own ERC20 via `sudo docker run`. Added the exports and a warning that a stale .env.local overrides them via loadEnvConfig(). - tests/devnet/README.md: "exactly two files pulled verbatim" contradicted its own next paragraph; summary.json carries no digests and no historical_states; `down` -> `down -v`; `jq -er` so a null erc20_address fails instead of printing "null" to paste. - devnetReady.mjs: the aggkit-proxy does read network_id; 0 and 1 simply map to the same upstream in this bundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Final layer: hermetic e2e CI
Before this, the workflow had no backend at all: it referenced secrets pointing at a devnet that doesn't exist in CI, What changed
Verified: 12/12 bridge tests green locally and in CI, across three independent CI runs. Review notes
Full write-up: |
…80d83 Replaces the local file:../sdk override in pnpm-workspace.yaml (added while the bridge tracker API was unpublished) with the now-published snapshot build, which carries the same commit as SDK_REF in the Docker workflows (5680d837b168c...). This is the "swap this override to that published semver" step the override's own comment called for. pnpm install --frozen-lockfile now succeeds without a sibling ../sdk checkout, which is what was failing the CI - lint / typecheck / test check on PR #24 -- no source changes were needed, the SDK's public API surface used by app/** is unchanged between the local build and the published snapshot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…lication
Devnet was declaring the same aggkit-proxy URL under every L2 networkId in
aggkitBridgeApis ("1": url, "2": url, ...) because a per-network map was the
only field available, even though one proxy fronts every network there.
Bridge API and proxy are not the same thing -- a bridge API is one aggkit
REST backend for one network, a proxy is a superset that also fronts the
tracker and multiplexes every network behind one URL -- so introduce
aggkitProxy as a single-URL field for that shape and keep aggkitBridgeApis
for modes with genuinely distinct per-network backends (mainnet/testnet).
The two are validated as mutually exclusive per mode (never both; neither is
the existing "not configured" escape hatch), and the pre-existing
chains<->map cross-check and duplicate-networkId check now apply only to the
map form, since per-chain agreement is meaningless for a single proxy.
Devnet config.json / config.ci.devnet.json and scripts/kurtosisDevnetEnv.mjs
switch to aggkitProxy (schema-only change; hosts/ports unchanged).
kurtosis-cdk's generated config keeps using the map form for now (untouched,
cross-repo, out of scope here) since aggkitBridgeApis is retained rather than
renamed. Adds NEXT_PUBLIC_AGGKIT_PROXY as the env-override successor,
alongside the unchanged NEXT_PUBLIC_AGGKIT_BRIDGE_APIS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
D0b converted only devnet to the single aggkitProxy shape; the user asked for the same treatment on mainnet/testnet so all three modes share one shape. Every mainnet/testnet aggkitBridgeApis URL was already a PLACEHOLDER- value (no real backend deployed), so collapsing each mode to one aggkitProxy is lossless. - config.json: mainnet -> aggkitProxy "https://PLACEHOLDER-mainnet-aggkit-proxy", testnet -> aggkitProxy "https://PLACEHOLDER-testnet-aggkit-proxy". No mode in config.json declares aggkitBridgeApis anymore. - config/config.ci.devnet.json: mirrored identically, preserving the documented "byte-identical to config.json except appModes.default" invariant (docs/config.md). - The aggkitBridgeApis map form stays fully supported in the schema (configSchema.mjs) and validator (configValidator.mjs) -- untouched -- because kurtosis-cdk's dev-ui template still generates it and the container test fixtures deliberately exercise it. What changed is only which form this repo's own config.json uses. - docs/config.md, docs/docker.md: updated the now-stale claims that mainnet/testnet use the map form; documented that the chains<->map and duplicate-networkId checks are inherently map-form-only, so with every shipped mode now on aggkitProxy those checks no longer guard the committed config.json at all -- by design, not a gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
User directive (mid-step scope change on D0c): "you need to update
kurtosis to work with the new config. Clean the old stuff for good."
Every mode this app ships now goes through a single aggkit-proxy, so the
per-network aggkitBridgeApis map -- and the NEXT_PUBLIC_AGGKIT_BRIDGE_APIS
env override that fed it -- no longer models anything this repo's own
config.json needs.
- config/configSchema.mjs: drop aggkitBridgeApisSchema and the
aggkitBridgeApis field; jsonAppModeConfigSchema now has only aggkitProxy
(optional), so the mutual-exclusion superRefine is gone too (nothing
left to be mutually exclusive with). The schema's .strict() now rejects
aggkitBridgeApis as an unrecognized key.
- config/configValidator.mjs: remove the chains<->aggkitBridgeApis
cross-check and the duplicate-networkId check -- both were map-form-only
and have no target left.
- config/configLoader.mjs: resolveAggkitBridgeApiUrl -> resolveAggkitProxyUrl;
only resolves a mode's aggkitProxy now.
- app/config.ts, app/types/config.ts: drop the NEXT_PUBLIC_AGGKIT_BRIDGE_APIS
override path and JsonAggkitBridgeApis type. The RESOLVED runtime shape
(AppModeConfig.aggkitBridgeApis: Record<number, string>, fanned out from
aggkitProxy) is unchanged -- every downstream consumer (AggkitBridgeAggregator,
app/utils/appMode.ts) still gets one URL per network; only the config.json
surface and the env override are gone.
- scripts/kurtosisDevnetEnv.mjs: writes NEXT_PUBLIC_AGGKIT_PROXY instead of
a hand-built per-network JSON map.
- playwright.config.ts: requires NEXT_PUBLIC_AGGKIT_PROXY instead of
NEXT_PUBLIC_AGGKIT_BRIDGE_APIS; removed the "partial-failure" project,
whose bogus-per-network-override mechanism no longer exists.
tests/bridge/partial-failure.spec.ts is now skipped with a comment
explaining why and what a redesign would need.
- tests/container/fixtures/config-{a,b,invalid-schema,invalid-structural}.json:
migrated off aggkitBridgeApis to aggkitProxy (their unrelated deliberate
invalidities -- bad rpcUrl, bad default mode -- are untouched). Rebuilt
the local C-1 image and reran the full container Playwright suite (6/6
pass) to prove the fixtures and the schema/entrypoint contract still work
end to end.
- .github/workflows/deploy.yaml: NEXT_PUBLIC_AGGKIT_BRIDGE_APIS secret ->
NEXT_PUBLIC_AGGKIT_PROXY (only runs on push to main / manual dispatch, not
on this PR's CI checks). If an actual GH secret named
NEXT_PUBLIC_AGGKIT_BRIDGE_APIS is configured, an org admin needs to add
NEXT_PUBLIC_AGGKIT_PROXY as its replacement.
- docs/config.md, docs/docker.md, docs/deployment.md, README.md: rewritten
to describe aggkitProxy as the only field, document the removal (with a
"History" section keeping the retired map-form rules' rationale), and
flag kurtosis-cdk's dev-ui template as a known, temporary, tracked skew
-- that template still emits aggkitBridgeApis and will fail this schema
until it's migrated kurtosis-cdk-side (tracked separately, not touched
here per instruction).
Deliberately NOT touched: kurtosis-cdk (any repo), tests/devnet/**,
.github/workflows/e2e.yaml (still sets the now-dead
NEXT_PUBLIC_AGGKIT_BRIDGE_APIS literal -- flagged, not fixed, since that
file is D1's territory; D1's own plan note already anticipated updating
this literal for a different reason and now needs to for this one too).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… removal) Covers both commits landed for D0c: the original scope (mainnet/testnet -> aggkitProxy, 2da5992) and the mid-step scope change to delete the aggkitBridgeApis map form entirely (8563dd4). Records pnpm run check output, the rewritten validator/loader test suites, live failure-provable schema demonstrations against the real config.json, the rebuilt local container image + full container Playwright suite (6/6), both GHCR publish runs with the final tag and an anonymous-pull proof, and PR #24's CI status -- including why the e2e check fails (a documented, expected cross-step skew with .github/workflows/e2e.yaml, D1's territory, not touched here). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…al-failure spec D0c removed the aggkitBridgeApis map form, leaving e2e.yaml exporting the retired NEXT_PUBLIC_AGGKIT_BRIDGE_APIS while playwright.config.ts requires NEXT_PUBLIC_AGGKIT_PROXY, turning PR #24's e2e check red. Swap the env literal to the single-URL form and extend the "assert workflow literals match summary.json" step to check it against .aggkit_proxy.rest_url_via_proxy so the assertion still checks something. Restore tests/bridge/partial-failure.spec.ts, which D0c left silently skipped, via Playwright route interception on the real aggkit-proxy backend (fail /bridges?network_id=2 for the real Devnet L2-002 network) instead of the retired per-network env override -- same partial-failure contract, verified end to end, with the coverage delta (the 'Unknown network' label fallback for unregistered networkIds) called out explicitly rather than left as a silent skip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… ambiguity First CI run of the restored spec (run 31785182452) proved the route- interception mechanism itself works -- the banner correctly rendered "We couldn't load activity from Devnet L2-002" -- but a loose getByText(/devnet l2-002/i) also matched an unrelated transaction row (an earlier spec's L2-to-L2 transfer touching network 2), tripping Playwright's strict-mode single-element check. Scope the assertion to the banner's own message text instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… result All PR #24 checks green including e2e (run 31785962610): 4/4 preflight, 12/12 bridge tests -- matching the plan's 12-tests/10-files baseline exactly, so the partial-failure.spec.ts restoration (see partial-failure-decision.md) carries no hidden suite-count coverage loss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
The prior evidence commit (ad292ff) itself triggered one more CI cycle on push (run 31786588506) -- update pr24-checks-final.txt and bridge-suite-result-final.txt to point at that run, which is the one that actually validates the commit these evidence files live in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
D0b/D0c/D0d committed 22 evidence files under
plans/snapshot-v2-aggkit-e2e/D0{b,c,d}-evidence/, which showed up in
PR #24's diff as orchestration noise unrelated to the actual code
change. Relocate that content (byte-for-byte, verified via sha256)
to /home/brolygon/repos/plans/snapshot-v2-aggkit-e2e/ outside the
repo, matching the convention already used on the kurtosis-cdk side.
Add /plans/ to .gitignore so this cannot recur.
…ocker
NEXT_PUBLIC_PROJECT_ID was inlined into the JS bundle at build time
(Next.js static export), so a published Docker image was permanently
stuck with whatever project id was present when the image was built --
docker run -e could not change it. Since the runtime nginx stage has
no Node, the fix moves this prod-required value into the existing
runtime config.json mechanism instead of adding envsubst/templating
to entrypoint.sh (which its own header comment forbids).
- config/configSchema.mjs: add a required top-level `walletConnect:
{ projectId }` field to the schema.
- app/config.ts: resolve walletConnect.projectId from the loaded
runtime config, with NEXT_PUBLIC_PROJECT_ID as a local-dev/Playwright
override only (same precedence pattern already used for
NEXT_PUBLIC_AGGKIT_PROXY). New getWalletConnectProjectId() accessor.
- app/context/wallet.tsx: read the resolved projectId from config
inside WalletProvider/ensureAppKit instead of a module-scope
process.env read, since it's no longer available until
AppConfigGate resolves.
- entrypoint.sh: extend the structural jq validation to require
walletConnect.projectId as a non-empty string.
- config.json / config/config.ci.devnet.json: add the walletConnect
block (placeholder value, preserving graceful AppKit degradation).
- .env.production: no longer sets NEXT_PUBLIC_PROJECT_ID at all, so a
production build never has an override to freeze in -- Cloudflare's
deploy.yaml still works since it sets the var as a real shell env,
which Next's env loader never overrides with a .env file value.
- Docs (docker.md, config.md, deployment.md, README.md, .env.example):
document the new field, its runtime-vs-build-time precedence, and
fix docker.md/deployment.md's stale "no image is published yet"
status (several dispatch-tagged images have since been published
and verified pullable anonymously).
- Tests: cover the new field's required-ness (missing/empty/wrong
type all rejected) and the env-override precedence, and add
walletConnect to every existing config fixture.
Verified: one built image run twice with two different mounted
config.json files (disjoint walletConnect.projectId values) produces
two different outbound AppKit network requests carrying each run's
own project id -- proving the app reads the mounted value at runtime,
not a build-time constant. A config.json missing walletConnect (or
with an empty projectId) makes the container exit non-zero with a
FATAL log line, same as every other required field.
…-image path Replaces tests/devnet/docker-compose.yml and summary.json with the K1-merged, digest-pinned v2 bundle from 0xPolygon/kurtosis-cdk (publish run 31787941750): the aggkit bridge is now a component of the main aggkit-00X process (no more aggkit-00X-bridge), 9 services total, every image pinned by @sha256 digest instead of a mutable snapshot-<sha> tag. agglayer-dev-ui-002 (the baked dev-ui, manual use only) ships behind the `devui` compose profile so CI never starts it; the proxy's depends_on edge on it is dropped and its compose-level healthcheck overridden to stop probing bare `/` (already baked into the emitted bundle upstream). e2e.yaml: updates the literals-assertion step to compare the compose file's full set of pinned image digests against summary.json's images.services[] digests (replacing the old single SNAPSHOT_IMAGE_TAG comparison, which no longer applies now that there's no tag-based override). Adds the built-image path per T4's design: builds the PR's own Docker image (reusing docker-pr.yaml's SDK_REF build-arg pattern) and runs tests/container/ against it, with an explicit comment on why tests/bridge/* cannot run there (build:production never sets NEXT_PUBLIC_E2E_ENABLED). Also updates tests/devnet/README.md's regenerate-and-bump procedure for the new digest scheme, and fixes stale -bridge/11-service/old-doc-path references in README.md, docs/config.md, and scripts/devnetReady.mjs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
D0e (89f3917) made entrypoint.sh's structural validation require walletConnect.projectId, but tests/container/fixtures/*.json predates that change and none of the four fixtures carry it -- every container-suite spec that mounts one of these configs would fail fast with "FATAL - mounted config ... failed validation. Refusing to start." This was never caught by CI because nothing wired tests/container/ into a workflow until this PR's built-image step (see the previous commit). Uses the repo's own checked-in placeholder value (YOUR_PROJECT_ID_HERE, see app/utils/reownConfig.ts) so AppKit's basic-mode degradation kicks in and the existing console-noise allowlist in container-app.spec.ts still holds -- an arbitrary non-placeholder id was verified locally to 403 outside basic mode and log an unallowlisted "[Reown Config] Failed to fetch remote project configuration" warning. config-invalid-structural.json's own deliberate defect (appModes.default with no matching appModes.configs entry) is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
What & why
This PR migrates dev-ui from the legacy Bridge Hub API to aggkit's bridge service through the new aggkit module in sdk #28. The migration retains the app's bridge, claim, tracker, partial-failure, wallet/devnet, and runtime-config work, while replacing Bridge Hub-specific configuration and clients with per-network aggkit APIs.
The PR also owns a portable GHCR image with runtime
/config.jsonvalidation and documents its mount/config contract. Its production build intentionally does not enableNEXT_PUBLIC_E2E_ENABLED;LocalWalletProvideris consequently compiled out. The bridge-flow suite must therefore continue to usepnpm dev, while the built image is exercised by the dedicated container suite.Hermetic v2 E2E
e2e.yamlnow tests PR source, not a stale baked application: it checks out and builds the sibling SDK atSDK_REF5680d837b168cd3b250110660332aa110eb88aae(SDK package1.0.0-snapshot-5680d83), builds this PR's dev-ui image, runs the six container tests against that image, then starts the vendored devnet and serves the application withpnpm devfor the bridge flow.The vendored snapshot is the current v2 artifact from kurtosis-cdk #929: nine services, every Compose image pinned by
@sha256:<digest>, noaggkit-00X-bridgeservice, andagglayer-dev-ui-002behind the optionaldevuiprofile. The workflow asserts the complete digest set againstsummary.json; it does not use asnapshot-<sha>tag override. Default Compose starts the eight backend services, while--profile devuiadds the ninth app container.The E2E job is green at the current PR head
07e25e933b33fd40de974d29f3a8a97604c65336: run 31792978666 completed in 11m23s, with all 12 bridge tests, 6 built-image container tests, and 4 preflight tests passing. The added built-image path accounts for roughly 1m53s of the timing increase; the full job remains hermetic and uses no repository secrets.References and merge order
anvil-2chainsE2E environment.Testing
pnpm run checkis clean.pnpm devbridge path, preserving the production-buildNEXT_PUBLIC_E2E_ENABLEDlimitation as an explicit, tested contract.