Repository navigation
feat(aggkit): aggkit bridge-service client + multi-network aggregator (incl. bridge tracker, rc5) - #28
Conversation
…egator Add a self-contained `src/aggkit/` module for the aggkit bridge service REST API (`/bridge/v1`), leaving the NATIVE module untouched: - `AggkitBridgeClient`: typed single-network client (bridges, claims, claim-proof, l1-info-tree-index, token-mappings, sync-status, health) with snake_case query serialization, page_size<=200 handling, BigInt-safe global_index parsing, and typed `AggkitApiError`. - `AggkitBridgeAggregator`: multi-network fan-out with partial-failure reporting, tiered status derivation (BRIDGED/LEAF_INCLUDED/ READY_TO_CLAIM/CLAIMED), ready-to-claim counting, claim-proof orchestration, and token metadata composition. - `ERC20.getMetadata()` on-chain read helper. - Fixture-driven unit tests + a live-enclave smoke script. Status derivation probes l1-info-tree-index by recording network (not token origin) and confirms READY_TO_CLAIM candidates via a targeted global_index claims query to avoid page-1-only false positives. Legacy bridge-hub `fetchMerkleProof` marked @deprecated (unused by dev-ui). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
…lt tolerance design.md (aggkit-proxy-l2l2 round) §2/§3 delta: - getInjectedL1InfoLeaf (client.ts): new destination-side GER-injection probe, message-matched 404 "not injected" vs the proxy's unrelated 404 "bridge service url not found" (§3.2). - getSyncStatus now sends network_id explicitly — unqualified requests 400 through aggkit-proxy (§2.3 gap G2). - resolveInjectedLeafIndex + Tier-2b gate in toTransaction: an L2-destination row only reaches READY_TO_CLAIM once the destination's GER is actually injected, closing the F1/F2 premature-READY window (measured >=8.02s live) that would otherwise build a claim proof that reverts GlobalExitRootInvalid on-chain (§3.1-§3.4). - getClaimInputs rewritten to build /claim-proof on the destination-injected index M (>= source index N), not the deposit's own L1-info-tree index — this was wrong for every L2-destination claim, including mainnet-relevant L1->L2 manual claims (§3.5). Adds an additive `sourceL1InfoTreeIndex` field. - getActivity/getReadyToClaimCount: guard every per-row Tier-2 probe so one network's failure degrades to `failedNetworks` instead of rejecting the whole call (§2.2 gap G1). - errors.ts: AggkitApiError.httpStatus JSDoc adds 502 (§2.5). Additive only: one new client method, one added return field, two behavioural fixes, one JSDoc line. No exports removed or renamed. Fixture-backed regression tests added per §3.7 (8 required cases): the L2->L2 premature-window/post-injection transition, L2->L1 skip-Tier-2b, synthetic M>N (S2 shape), the 404 message-matching hazard, the proxy-502 partial-failure path, and getSyncStatus's network_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…ross {0,1,2}
design.md §8 S8 item 7: point AGGKIT_URL at aggkit-proxy (direct port or
haproxy /aggkitapi), configure the aggregator with networks {1,2}, and skip
the getHealth check in proxy mode (§2.4 gap G3 — no root health route behind
the proxy).
Adds: full fan-out activity across the configured L2s, an L2->L2 row's
derived status (asserts CLAIMED for the known-autoclaimed L2-1->L2-2
deposit), getClaimInputs for a real L2->L2 AND a real L2->L1 deposit
(printing both sourceL1InfoTreeIndex and leafIndex), and an opt-in
(RUN_PARTIAL_FAILURE_TEST=true) proxy-502 partial-failure section that stops
aggkit-002-bridge, asserts getActivity resolves with failedNetworks naming
only network 2 while network 1's rows still render, then restarts it.
Verified live against the cdk enclave (direct proxy port + haproxy
/aggkitapi, both byte-identical) — all checks pass, enclave healthy
afterward. See handoff-sdk.md for the recorded output.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
Document AggKit proxy usage and transaction status state machine: src/aggkit/index.ts: - Add multi-network proxy configuration example showing same base URL under multiple networkId keys (enables devnet proxy multiplexing) - Clarify that routing happens via ?network_id= query parameter src/aggkit/types.ts: - Document AggkitTransactionStatus lifecycle (BRIDGED → LEAF_INCLUDED → READY_TO_CLAIM → CLAIMED) - Explain LEAF_INCLUDED state: source settled, destination GER not yet injected (common on fresh enclaves where L2 block height exceeds L1) - Clarify that LEAF_INCLUDED only occurs for L2 destinations (L1 has no injection) These docs describe the correct configuration pattern for 2-L2 devnets with aggkit-proxy frontends, and explain the timing windows developers may observe during claim-readiness polling (design.md §0.1–§3.3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…M_ADDRESS Re-running the S8 smoke script against the current enclave (recreated for the S12 E2E round) produced "4 CHECK(S) FAILED" with no indication why: the default FROM_ADDRESS is the EOA from one earlier round's lifecycle deposits, and bridge history does not survive `kurtosis enclave rm`. Sections 3/5/6/7 all failed for want of data, which reads identically to a genuine SDK regression. The script now prints an explicit diagnostic when the fan-out returns an empty activity page, naming the address and telling the operator to pass FROM_ADDRESS, and the header documents the default as enclave-specific. The assertions still fail (an empty page IS a smoke failure when you expected data) -- only the diagnosis is added. Confirmed with FROM_ADDRESS set to the dev-ui E2E wallet against the live 2-L2 enclave: every section passes, including the destination-injected getClaimInputs checks (L2->L2 sourceL1InfoTreeIndex=16/leafIndex=16, L2->L1 18/18). typecheck, lint and 93 unit tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
Types for aggkit v0.11.0-rc4's tracker/v1 REST API (docs/bridgetracker/API.md), carrying raw numeric enums alongside their _string companions verbatim. Adds AggkitBridgeClient.getBridgeTracking(txHash, networkId?) over the same fetchRawText/AggkitApiError plumbing as the bridge/v1 endpoints, and AggkitBridgeAggregator.getBridgeTracking(networkId, txHash), which routes L1 through a configured L2 instance via clientForNetworkOrL1 and passes networkId through explicitly to the URL path (that routed-through instance's own networkId is not 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ormat S4's tracker types were derived verbatim from docs/bridgetracker/API.md; a live capture off a real rc4 enclave (plans/bridge-tracker/fixtures/) proved the wire deviates further than that doc's own "numeric + _string companion" convention suggests. tracking_status, bridge_type, step_name (not `step`), and per-step status all ship as bare strings with no numeric/companion fields; BridgeStatus nests leaf_type/origin/destination/ amount under an undocumented `event` object; per-step start_date/end_date/ result/error are omitted keys (not null) until applicable; and CertificateData.settlement_tx_hash is omitted (not null) pre-Candidate. error_type and CertificateData.status keep their documented numeric + _string pairs, confirmed unchanged by the same fixtures. Copies a representative fixture subset into src/aggkit/__fixtures__ and adds src/aggkit/__tests__/tracker.test.ts covering URL construction (including network-0 L1 routing through the aggregator), the registered-only and giving-up-error shapes, and the resolved route (step count/order/per-step results) for all three typologies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Document getBridgeTracking's operational contract on both entry points (AggkitBridgeClient and AggkitBridgeAggregator) and in the module's top-level doc comment, matching the style of the earlier multi-network-proxy/status-lifecycle docs (2ec0d5e): - Terminal semantics: tracking_status === 'finished', or 'error' with bridge_status still null (the tracker giving up entirely). Per-step errors inside all_steps[i].error are explicitly non-terminal -- the tracker retries those on its own. - Polling guidance: no push/subscription channel exists, so callers must poll; ~5s is a good default (matches the dev-ui consumer), stopping once the terminal condition above is met. - Server-side registration/eviction: the first call for a (networkId, txHash) pair registers it (tracking_status: 'registered', all_steps: null); a RetentionPeriod eviction of a still-pending bridge causes the same 'registered'/null-steps response on the next poll, not an error -- treat it like the original registration, not a regression. - Cross-reference the aggregator's getBridgeTracking to AggkitBridgeClient's for the wire-format deviations already documented in types.ts (bare string enums, step_name vs step, etc.) rather than duplicating them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
… semantics Two JSDoc claims contradicted aggkit v0.11.0-rc4's actual behavior: - A per-step error does NOT leave tracking_status at 'running' -- aggkit 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 populated. This is exactly why the documented terminal check needs the bridge_status === null conjunct rather than tracking_status alone. - TrackingData.error is not "set only if the tracker gave up": the tracker_registered.json fixture carries a transient error at retry_count 1 while tracking_status is still 'registered' (retries in progress). Only the exhausted (2) case is the giving-up terminal. Doc-only; no runtime change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
v0.11.0-rc5 (agglayer/aggkit#1784) rewrote docs/bridgetracker/API.md to match the tracker's wire format exactly, closing #1781; the serializer itself never changed between rc4 and rc5, and live rc5 fixtures confirm identical shapes (plans/bridge-tracker-rc5/verification.md §4). Update the tracker JSDoc in types.ts/client.ts/aggregator.ts so it no longer frames the bare-string enums, event nesting, and omitted-optional-keys behavior as an open API.md drift/bug — upstream docs now agree with what the SDK already modeled from fixtures. No type-shape changes. Add an rc5 provenance note to the tracker fixture-loading test file confirming the rc4 captures remain valid, live-verified unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
`ChainRegistry.getChainByNetworkId()` previously returned the first insertion-order match, so the SDK's pre-seeded default chains (e.g. Ethereum mainnet at networkId 0) shadowed a consumer-registered chain sharing the same networkId (e.g. a devnet L1, also at networkId 0). Dormant in practice (console-triage.md) but real: an ERC-20 bridged from devnet L1 whose token isn't in a UI's static list would resolve mainnet's rpcUrl and hit https://eth.llamarpc.com instead of the devnet RPC. Track which chainIds are built-in defaults at construction time and prefer a non-default (consumer-registered) match on networkId lookup collisions, independent of registration order. No override still resolves defaults exactly as before. Public API is unchanged. Reconciles the TODO(aggkit-migration) at aggregator.ts:653-666. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
- Add a "Bridge Transaction Tracking" README subsection: getBridgeTracking usage sample, polling/terminal-state guidance, the bare-string enum shape note, and the #1786 claim-readiness caveat. - Replace local-only plans/ path citations in types.ts and tracker.test.ts with durable content: fixture provenance stated inline (no path), and agglayer/aggkit#1781/#1784 issue/PR references for the rc4->rc5 API.md correction, instead of a /home/brolygon/... verification doc path. - Opportunistically strip the /home/brolygon/... absolute-path prefix from the pre-existing design.md/handoff-sdk.md citations in types.ts and scripts/aggkit-smoke.ts (username leak fix; the broader design.md sweep remains deferred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
fea252e reframed the per-type JSDoc in types.ts after aggkit v0.11.0-rc5 (PR #1784) corrected docs/bridgetracker/API.md to match the wire format, but missed the module-level comment in this barrel, which still framed the SDK's types as "deviating" from upstream docs. Post-rc5 the docs agree with the types; only rc4's API.md described them differently. Comment-only change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…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
CodeQL flagged `config.baseUrl.replace(/\/+$/, '')` (client.ts:70) as a polynomial regex over library-supplied input (bridge check on PR #28): a `+` quantifier anchored at `$` against an attacker/consumer-controlled `baseUrl` is a recognized ReDoS shape, even though this specific pattern has no exploitable nested-quantifier or overlapping-alternation blowup. Replace the regex with `stripTrailingSlashes()`, a manual backward scan that strips trailing '/' characters in a single linear pass -- same semantics (trim any number of trailing slashes before appending /bridge/v1 or /tracker/v1), no regex engine involved. The tracker not-ready pattern matching (L1_INFO_TREE_INDEX_NOT_READY_PATTERNS / INJECTED_L1_INFO_LEAF_NOT_READY_PATTERNS) was already plain `.includes()` on a lowercased string -- not implicated by this alert -- and is unchanged. Add regression tests: multiple trailing slashes, and a 50k-slash input to cover the ReDoS-adjacent long-input case the original alert worried about. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…comments Removes citations to machine-local planning documents (design.md, handoff-sdk.md, manual-validation.md, RELEASE.md, console-triage.md, enclave-notes.md and their §-numbered sections) from comments, test titles, and diagnostic strings across the aggkit module. Every citation is replaced with the underlying fact inlined directly, or dropped where the surrounding text already carried it — no behavior change. Comment-only sweep; dist/ rebuild confirms every line that differs in emitted output is itself a comment or JSDoc line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Missed by the design-doc citation sweep (3879690): a bare `§` symbol with no section number, left over from before that PR. Names the actual regression coverage instead of a dangling symbol. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
MaximusHaximus
left a comment
There was a problem hiding this comment.
Added some things I noticed in-line -- also posted briefly in Slack about the global_index issue 💖
Fundamentally, I think it would be ideal if the client was defined and published from the aggkit repository that owns the actual REST API interface, and this SDK used a versioned copy of that client rather than the client existing here, built entirely into the SDK - it solves a lot of issues at once. I'm going to open a PR against Aggkit repo showing what that would look like as an example 👍
8094f92 made getChainByNetworkId() prefer a consumer-registered chain over a still-default one on a networkId collision, but only covered a consumer picking a brand-new chainId (e.g. a devnet L1). registerChain() never removed a chainId from defaultChainIds, so a consumer *reusing* one of the SDK's own default chainIds never won a collision -- both matches stayed flagged as default and getChainByNetworkId() fell back to the first one registered in initializeDefaultChains(). This bites agglayer-dev-ui in testnet mode: it registers its L1 with Sepolia's real chainId (11155111), which the SDK also pre-seeds as a default at networkId 0. Both Ethereum mainnet (chainId 1) and that Sepolia registration stayed default-flagged, so getChainByNetworkId(0) kept resolving to mainnet -- routing testnet's L1 token-metadata reads (AggkitBridgeAggregator.getTokenMetadata) to eth.llamarpc.com instead of Sepolia. Flagged in review on the companion dev-ui PR (agglayer/agglayer-dev-ui#24, discussion_r3862947494). registerChain() now deletes the chainId from defaultChainIds before registering, so any re-registration -- new chainId or reused default one -- graduates it to consumer-registered. registerDefaultChain() re-adds the flag right after calling registerChain(), so construction-time seeding is unaffected. Adds 3 tests reproducing the reused-chainId (testnet/Sepolia) scenario, the gap left by 8094f92's tests (which only exercised new chainIds 1337/ 9999). 118/118 tests pass, typecheck and lint clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses three zero-risk review comments on the aggkit bridge module:
- Mark src/aggkit/__fixtures__/** as linguist-generated in .gitattributes
so GitHub collapses fixture diffs out of review. (3847551495)
- Fix the non-compiling `networks:` example in the src/aggkit/index.ts
barrel JSDoc: AggkitAggregatorConfig.networks is Record<number, string>,
not a map of {baseUrl, networkId} objects -- the latter would hand
stripTrailingSlashes an object at runtime ("url.charAt is not a
function"). (3862897795)
- Wrap the retry-exhaustion throw in src/aggkit/httpRaw.ts with
{ cause: lastError } so the original fetch error (stack, code like
ECONNRESET) survives instead of being flattened into a message string.
(3862898418)
This required also adding "ES2022.Error" to tsconfig.json's `lib`
array: under the prior "lib": ["ES2020"], TypeScript's ErrorConstructor
only has a 0-1 arg overload, so `new Error(msg, { cause })` is a hard
arity error (TS2554) regardless of runtime support. ES2022.Error only
adds the Error.cause/ErrorOptions type declarations -- no change to
`target` or emitted JS, and no other diagnostics appear anywhere else
in the tree with it added. Confirmed the fix works at runtime today
(Node's Error.cause has been supported for years, independent of the
TS lib setting).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix: back getActivity with aggkit bridgetracker, drop client-side fan-out 30
PR CI only ran typecheck/lint/test/build, so Prettier drift went undetected until the manual release workflow's Quality Checks step caught it. Add format:check so it's gated on every PR/push to main.
src/aggkit/__fixtures__/*.json and types.ts had fallen out of sync with Prettier config (unnoticed because PR CI didn't run format:check until the previous commit). No behavioral change.
aggkit's bridgetracker (agglayer/aggkit#1823, PR #1829) adds two protocol changes to `TrackingData`, on both the REST response and the WebSocket `status` message: - A new top-level `claim_status` field (`'pending' | 'readyToClaim' | 'claimed' | 'error'`) that replaces having to derive "ready to claim" by hand from `step_index`/ `all_steps` — this SDK's own docs previously told consumers to do exactly that manual inspection. - A new `WaitingL1InfoLeafAvailable` step, inserted immediately before `WaitingClaim` on all three routes (L1->L2, L2->L1, L2->L2), with its own `{ l1_info_tree_index }` result shape. Adds `AggkitClaimStatus` and `AggkitWaitingL1InfoLeafAvailableResult`, extends `AggkitBridgeStep`/`AggkitBridgeStepResult`/ `AggkitTrackingData` accordingly, exports the new types, and updates the JSDoc/README guidance that referenced the old manual derivation. No existing fixture under `__fixtures__/tracker_*.json` covers these fields yet (all predate PR #1829) — left them untouched rather than fabricating fixture data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lable_state-aggkit1823 feat(aggkit): add claim_status and WaitingL1InfoLeafAvailable step
aggkit's bridgetracker (agglayer/aggkit#1830, PR #1831) renames `GET /tracker/v1/activity/from/{from_address}`'s per-bridge `claimed` field to `claim_status`, revaluing it from a plain tri-state (`'true'`/`'false'`/`'error'`) to the same `'pending'`/`'readyToClaim'`/ `'claimed'`/`'error'` vocabulary `TrackingData.claim_status` already used (agglayer/aggkit#1823, PR #1829) — a consumer no longer needs `includeTracking: true` plus a `tracking` inspection just to tell "still pending" apart from "ready to claim": the tracker now resolves `readyToClaim` directly against the bridge-service `/l1-info-tree-index` + `/injected-l1-info-leaf` endpoints server-side either way. - `AggkitActivityItem.claimed` -> `claim_status: AggkitClaimStatus`, reusing the type PR #1829 introduced for `AggkitTrackingData`. - `errors` may now carry a `readiness` key (in addition to the existing `claim` key) when the direct readiness probe itself fails while the bridge is still unclaimed — `claim_status` conservatively stays `'pending'` rather than surfacing as `'error'`. - Updates the module docs in `types.ts`, `aggregator.ts` and `index.ts` that told consumers to derive readiness by filtering `claimed !== 'true'`, and adds a breaking-change writeup + inline note to README.md. - Extends `activity.test.ts` with `readyToClaim` and `errors.readiness` coverage; renames existing tests/fixtures off the old field name. No fixture yet captures this field (all `__fixtures__/tracker_*.json` predate PR #1831). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aggkit1830 feat(aggkit): activity endpoint reports claim_status like the tracker
aggkit-smoke.ts still referenced the old tri-state claimed field, which was renamed to claim_status in 9b52c78. This broke the Quality Checks type-check step in CI (run 34128060002).
MaximusHaximus
left a comment
There was a problem hiding this comment.
Spotted another occurrence of the global_index where it is not being manually fixed in the SDK; checked the source code and verified it would be silently corrupted by the JSON.parse(). Weirdly, the comment in types.ts even insists that it's already a string even though it isn't as far as I can tell in RC9. My only theory is that the swagger doc was trusted instead of the code being double-checked, or I'm looking at stale code references -- so I included links back to the code where I found the issue in aggkit. 🤔
Added a few other things in-line also
…or to rc9 - getActivity now pre-quotes global_index via quoteGlobalIndex before JSON.parse: the tracker's activity endpoint embeds bridgeservicetypes.BridgeResponse unmodified, so it has the identical *big.Int bare-number defect as /bridges and /claims. - AggkitActivityBridge.global_index's doc corrected: it is parsed from a bare JSON number (like AggkitBridge.global_index), not already a JSON string as previously documented. - Raise the documented minimum supported aggkit version to v0.11.0-rc9 in README.md, src/aggkit/index.ts, and AggkitClaimStatus's doc: the tracker's activity endpoint doesn't exist before rc8, and claim_status on activity rows / AggkitTrackingData is undefined at runtime on rc8 despite being declared required, until agglayer/aggkit#1829/#1831 (rc9). - parsing.ts's quoteGlobalIndex doc extended to cover the activity endpoint's use of it, plus a full scope/safety writeup of the regex. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
…racker failover loop The bridge tracker (/tracker/v1) is a separate aggkit service from the per-network bridge services (/bridge/v1) — its own binary on its own port unless an aggkit-proxy fronts both. Address it explicitly instead of deriving it from a bridge-service URL: - AggkitBridgeClientConfig gains optional trackerBaseUrl (falls back to baseUrl, correct only behind a fronting proxy). - AggkitAggregatorConfig gains required aggkitProxyUrl, the tracker's root. AggkitBridgeAggregator now builds a dedicated trackerClient from it and routes getActivity/getBridgeTracking through that client. - getActivity no longer loops over every configured network's client trying each in turn: that was never real failover, only N guesses at where the single tracker lives (each per-network client derived its own <baseUrl>/tracker/v1). With the tracker addressed explicitly there is one endpoint and nothing to fail over to, so getActivity now issues exactly one request and an AggkitApiError from the tracker propagates unchanged instead of being replayed per network and rewrapped as a plain Error. - getBridgeTracking now prefers trackerClient over the per-network fallback (clientForNetworkOrL1): it previously always used the per-network client and could throw "no client configured for network 0" with an empty networks map even while the tracker was reachable via aggkitProxyUrl, despite its JSDoc claiming unconditional proxy routing. BREAKING CHANGE: AggkitAggregatorConfig.aggkitProxyUrl is new and required. Every AggkitBridgeAggregator construction site must supply it (the tracker's root URL — the same origin as `networks` values when an aggkit-proxy fronts everything). getActivity's per-network failover loop is removed; a tracker failure now propagates as a single, unwrapped error instead of an aggregated "all configured networks failed" Error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
includeTracking defaulted to true, so every getActivity call registered every still-unclaimed bridge in the result with the tracker's supervised list by default — a server-side write triggered by what looked like a read. Default it to false, matching the tracker's own server-side default (bridgetracker/api/activity_command.go:88-90): a plain read no longer has this side effect. claim_status already resolves the 'pending' vs. 'readyToClaim' distinction server-side even with includeTracking: false, so most callers don't need the per-row AggkitTrackingData under `tracking` at all; it remains available by passing includeTracking: true explicitly. BREAKING CHANGE: AggkitBridgeClient.getActivity / AggkitBridgeAggregator .getActivity's includeTracking now defaults to false instead of true. Callers that relied on item.tracking being present by default must pass includeTracking: true explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
…ations
- AggkitActivityClaim.proof_local_exit_root / proof_rollup_exit_root are
now optional: verified absent in a live rc9 capture, matching /claims'
AggkitClaim, whose ClaimResponse source declares these
*Proof `json:",omitempty"`.
- Corrected the getActivity from_address comment: it is a hex string
(common.Address via MarshalText), not a byte array — swagger's `type:
array, items: integer` is swag's rendering of the underlying [20]byte,
not the actual wire JSON. Notes the encoding is not uniform across the
response (bridge.from_address keeps mixed case; claim.from_address is
always "").
- Dropped the stale internal "audit finding C1-C4" / "S7" / "S16" /
"S19 scratch-note" review-round references throughout client.ts,
aggregator.ts and types.ts, replacing each with a self-contained
pointer to the actual doc/rationale it referred to (including one
citation that was line-wrapped across a JSDoc break, so it would not
have been caught by a line-based grep for the old phrasing).
- Fixed the stale `{ not-injected, detail }` shorthand in
resolveInjectedLeafState's doc to match the real
`{ kind: 'not-ready', reason, detail }` shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
…ity fixtures Update the activity/aggregator/client/tracker test suites for this round's changes: the ~34 AggkitAggregatorConfig/AggkitBridgeClientConfig construction sites now supply aggkitProxyUrl/trackerBaseUrl, getActivity assertions cover the includeTracking:false default and the quoteGlobalIndex pre-quoting, and the tracker-failover-loop tests are replaced with single-request-through-trackerClient coverage. Add two live wire-format fixtures captured from a running aggkit devnet (docs/provenance in __fixtures__/README.md): activity_rc9_live_prefix.json (rc9, the global_index bare-number defect on the activity endpoint's embedded BridgeResponse) and activity_rc9_live_postfix.json (an aggkit build with agglayer/aggkit#1835 applied, proving quoteGlobalIndex is idempotent over the fixed wire format). Both pin the same logical deposit across two deterministic devnet runs. Add .prettierignore for src/aggkit/__fixtures__/*.json: these are byte-identical captures that activity.test.ts asserts against as raw substrings, so reformatting them would break byte-fidelity with the capture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
…ertions Add AGGKIT_TRACKER_URL (defaults to AGGKIT_URL under PROXY_MODE=true; required standalone) to supply AggkitAggregatorConfig.aggkitProxyUrl, and log it alongside the existing base/proxyMode/networks line. Add live assertions against the running devnet for the two review items this round's activity work exercises: the L1-origin global_index precision workaround (agglayer/aggkit#1835) and the default includeTracking: false path (the smoke run's activity call passes only { fromAddress }, so it exercises the new default rather than the opt-in tracking-registration path). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
Add a "Bridge Tracking & Activity" README section explaining the two distinct aggkit services AggkitBridgeAggregator now talks to (the per-network bridge service in `networks` vs. the single bridge tracker at `aggkitProxyUrl`), a config example showing both, and the `includeTracking` default-false / server-side-write-on-true semantics for getActivity, with a `claim_status`-filter example for ready-to-claim bridges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
fix(aggkit)!: resolve PR #28 review round 2 — activity global_index precision, required aggkitProxyUrl, rc9 floor
CI resolves prettier@3.6.2 from bun.lock, but a prior husky pre-commit hook ran with a locally-drifted prettier@3.9.5, which reformatted src/aggkit/types.ts into a style that 3.6.2 flags as non-compliant (unbroken single-line union types). Reformat the file with the bun.lock-pinned 3.6.2 binary so `format:check` agrees locally and in CI; no semantic changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc
…, not bare JSON number (#1835) ## 🔄 Changes Summary `BridgeResponse.GlobalIndex` (`bridgeservice/types/types.go`) was declared as `*big.Int`. `math/big`'s own `MarshalJSON` writes unquoted decimal digits, so `global_index` went out on the wire as a **bare JSON number**, even though the struct tag said `swaggertype:"string"` and the generated swagger declared `type: string`. Every sibling `GlobalIndex` field in the same file (`ClaimResponse.GlobalIndex`, `UnsetClaimResponse.GlobalIndex`, `SetClaimResponse.GlobalIndex`) already used the repo's own `BigIntString` wrapper — `BridgeResponse` was the lone holdout. For an L1-origin deposit, `global_index = 2^64 + deposit_count` (`bridgesync/processor.go`), which exceeds JavaScript's `Number.MAX_SAFE_INTEGER`. A plain `JSON.parse` on the bare-number form silently rounds it to a **different** value, which can resolve to a different bridge/claim entirely. This PR switches `BridgeResponse.GlobalIndex` to `BigIntString`, matching the other three fields, and updates the two call sites: - `NewBridgeResponse` (`bridgeservice/utils.go`) now writes `globalIndex.String()` into the wrapper. - `bridgetracker/sources/activity.go`'s `ActivitySource.ClaimInfo` recovers a `*big.Int` via `.ToBigInt()` where the downstream client API still expects one. - `bridgetracker/activity.go`'s cache uses the string form directly as its map key (previously `.String()` on the `*big.Int`, now a plain string cast) — this is an equality-lookup key, never sorted, so no ordering behavior changes. - `test/e2e/bridge_utils.go` and `test/e2e/proxy_tracker_test.go` call `.ToBigInt()` where `GlobalIndex` is passed into contract-binding calls that expect `*big.Int`. ##⚠️ Breaking Changes - 🛠️ **Config**: N/A - 🔌 **API/CLI**: **Yes.** `GET /bridge/v1/bridges` and `GET /tracker/v1/activity/from/{from_address}` now emit `global_index` as a JSON **string** instead of a bare JSON **number**. Any consumer that parses `global_index` as a numeric type (e.g. `JSON.parse` mapping it straight to a JS `number`) must switch to reading/parsing it as a string. Note that the generated swagger schema for these fields is **unchanged** — it already declared `global_index` as `type: string` (via the `swaggertype:"string"` override on the old `*big.Int` field); only the runtime wire encoding changes to finally match the documented contract. - 🗑️ **Deprecated Features**: N/A ## 📋 Config Updates - N/A ## ✅ Testing - 🤖 **Automatic**: - New: `TestNewBridgeResponse_GlobalIndexMarshalsAsQuotedString` (asserts the quoted form is present and the unquoted form absent), `TestBigIntString_ToBigIntRoundTrip`, `TestActivityItemMarshalJSON_EmbeddedBridgeGlobalIndexIsQuotedString` (covers the tracker's embedded copy of the bridge in activity items). - `make lint` → `0 issues.`, exit 0. - Full non-e2e unit suite: 94 packages `ok`, 0 FAIL, including `bridgeservice`, `bridgeservice/types`, `bridgetracker`, `bridgetracker/api`, `bridgetracker/sources`. - `make generate-swagger-docs` produced a byte-identical result — zero generated-file diff, since the swagger already said `type: string`. - 🖱️ **Manual**: Live before/after capture from a 2-L2 anvil devnet (aggkit-proxy serving `/bridge/v1` and `/tracker/v1`), same deposit position (L1-origin, `deposit_count 2`, `global_index 2^64+2 = 18446744073709551618`), `claim_status: claimed`. The two captures come from two runs of a deterministic environment at the same deposit position, not one continuous chain. Before (unmodified `v0.11.0-rc9`), `GET /tracker/v1/activity/from/0x8943...`: ``` "global_index":18446744073709551618 ``` Unquoted — a plain `JSON.parse` on this yields `18446744073709552000` (`typeof number`), a different number than the one on the wire. Interestingly, that same pre-fix payload also contained the sibling `ClaimResponse` field, already on `BigIntString`, right next to it: ``` "global_index":"18446744073709551618" ``` quoted — one response demonstrating both the bug and the sibling field that proves the fix is right. After (this branch), same endpoint: ``` "global_index":"18446744073709551618" ``` Quoted, zero unquoted occurrences anywhere in the payload. `JSON.parse` now yields the exact string `"18446744073709551618"` (`typeof string`). Same result on `GET /bridge/v1/bridges?network_id=0`: all three L1-origin rows go from bare numbers to quoted strings. ## 🐞 Issues - Closes #1820 ## 🔗 Related PRs - agglayer/sdk#28 — the consumer that surfaced this defect. - #1816 — a related contract-first POC covering the same defect class (reference only; this PR does not supersede it). ## 📝 Notes - Chose `BigIntString` over a custom `MarshalJSON` on `*big.Int` because it matches all three sibling `GlobalIndex` fields already in this file, and keeps the Go *type* itself honest about the wire encoding, rather than fixing the bytes on the wire while leaving the type inconsistent with its siblings and hiding the encoding behind a method. - The generated swagger schema does not change: `global_index` was already documented as `type: string` (via `swaggertype:"string"` on the old `*big.Int`), and `BigIntString` is natively a Go string, so the docs and the runtime now finally agree — no schema diff, but a genuine wire-format change for consumers parsing the bare number. - `agglayer/sdk`'s client-side `quoteGlobalIndex` workaround (added to cope with the bare-number encoding) should stay in place until its minimum supported aggkit version contains this fix — it was verified idempotent over the new quoted form, so it keeps working unchanged after this lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Gzd3Uy5KMYZGm4q7y4hpvc --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The issue related to It has also been released in https://github.com/agglayer/aggkit/releases/tag/v0.11.0-rc10 Let us known if there is anything else left to address |
The bridge tracker's proxy backend now reports a step status of "skipped" (with an accompanying error_type 3 / "skipped") when a step no longer needs verifying, e.g. because the bridge was already claimed on the destination network. Add the missing status/error-type values to AggkitStepStatus/AggkitTrackerErrorType(String) and correct the JSDoc claiming `error` only ever appears when status is 'error'. Fixes #38 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A live capture against a real tracker (network 81) shows that a
'skipped' step does not reliably carry start_date/end_date/error: only
the step that had already started retrying before being superseded
keeps those fields (with a transient, not error_type-3, error), while
every step downstream of it is entirely bare -
{step_index, step_name, status: 'skipped'} and nothing else. Correct
the JSDoc that overclaimed 'error' always accompanies 'skipped', and
add the live fixture plus a test covering the bare-step shape.
Fixes #38
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix: support new tracker step status "skipped" 38
MaximusHaximus
left a comment
There was a problem hiding this comment.
❇️ all my prior feedback is addressed --- approving, I don't see anything blocking here.
The inline comments are all nits / non-blocking bookkeeping — take them or leave them 🤷 :)
One thing I think should be fixed before merge: the PR description is pretty out of date now. It still describes getReadyToClaimCount(), the BRIDGED/LEAF_INCLUDED/READY_TO_CLAIM/CLAIMED derivation, failedNetworks, "113/113 tests" (it's 188 now), and "NATIVE surface is untouched".
It also doesn't mention anything that landed from #32/#33/#35/#36/#37/#39 — the tracker-backed getActivity (no pagination, warnings, claim_status), the required aggkitProxyUrl, the rc9 floor, the two ! breaking commits, or the skipped step status.
Would be great to rewrite it to describe what's actually merging, so anyone (or any agent 🤣) reading the history later gets the right picture 👍
| is resolved server-side even without `includeTracking: true` — no `tracking` | ||
| snapshot required on the item to tell them apart. | ||
|
|
||
| **Caveat ([agglayer/aggkit#1786](https://github.com/agglayer/aggkit/issues/1786), OPEN)**: |
There was a problem hiding this comment.
Non-blocking: aggkit#1786 closed as completed on 09-15, so the "OPEN" and "until #1786 itself closes" wording here is stale now. Worth finding which aggkit release carries the fix, then either bumping the floor to it or rewording the caveat. It matters a bit more now that readyToClaim comes from the tracker instead of the SDK's own GER-injection check. getClaimInputs still guards against acting early, so the worst case is a claimable: false, not a revert 👍
| * of aggkit's own `BigIntString` wrapper, so it serializes as a bare number | ||
| * even though aggkit's swagger declares it a string. Tracked at | ||
| * https://github.com/agglayer/aggkit/issues/1820, fixed by | ||
| * https://github.com/agglayer/aggkit/pull/1835 — delete this workaround once |
There was a problem hiding this comment.
Non-blocking, optional: #1835 is in rc10 and rc11 now. If the floor moves for #1786 anyway, this workaround's own removal condition is met and it can go.
| * (plus the 503) threw. `/claim-proof` is the LAST call `getClaimInputs` makes | ||
| * — the source is settled and the destination has injected — so throwing there | ||
| * rendered a hard failure for a transient wait. This activates the not-ready | ||
| * arm design §2.2/§2.6 reserved for it; no aggregator change is needed because |
There was a problem hiding this comment.
nit: design §2.2/§2.6 still points at a local design doc. Same at L435 and types.ts:197. Inline the rationale or drop the pointer.
| * `:1123-1124`) — a single wire state as far as a consumer is concerned; the | ||
| * exact prose travels in `detail`. On rc4/rc5 all five answered 500 and threw. | ||
| * | ||
| * This is the member design §2.2/§2.6 reserved when `getClaimProof` was given |
There was a problem hiding this comment.
nit: same dangling design §2.2/§2.6 citation as client.ts:213.
|
|
||
| /** | ||
| * Picks the aggkit instance that can answer for `recordingNetworkId` — the | ||
| * network whose local exit tree recorded the deposit (comment 3847422009). |
There was a problem hiding this comment.
nit: there are ~45 references like comment 3847422009 / PR #33 review across src/ (plus C1/C2/C3 audit labels in the test names). They go stale the moment this merges, and nobody reading the code later will have the context. Would be nice to strip them and keep only the why.
| // unmodified, and `detail` is aggkit's own wording propagated verbatim | ||
| // rather than re-fabricated by the caller. | ||
| // | ||
| // Was `kind: 'not-injected'` with the reason hard-coded to |
There was a problem hiding this comment.
nit: history narrative ("Was kind: 'not-injected'…"). Same with "It used to loop…" in getActivity's JSDoc and the FIXED: block in getTokenMetadata. That belongs in commit messages, not in the code.
| * `claim_status === 'readyToClaim'`, exactly as this result's `bridges` | ||
| * already has to be interpreted for status display. | ||
| */ | ||
| async getActivity(params: { |
There was a problem hiding this comment.
Non-blocking bookkeeping: #30 and #31 both describe code that doesn't exist any more (client-side pagination/fan-out, getReadyToClaimCount). Close them, or rescope to what still applies. The one real leftover I see is that the tracker returns an address's entire history in one response, unpaginated. #38 is also still open even though #39 fixed it.
| // the wrong tree (comment 3847422009). | ||
| if ( | ||
| 'originNetworkId' in params && | ||
| (params as unknown as Record<string, unknown>)['originNetworkId'] !== |
There was a problem hiding this comment.
nit: as unknown as Record<string, unknown> double cast. After the in check, reading the value through a Record<string, unknown>-typed binding (or Reflect.get) avoids the cast chain.
| txHash: string, | ||
| networkId: number = this.networkId | ||
| ): Promise<AggkitTrackingData> { | ||
| const url = `${this.trackerApiUrl}/network/${networkId}/tx/${txHash}`; |
There was a problem hiding this comment.
nit: txHash goes into the path without encodeURIComponent. getActivity does encode fromAddress. Harmless for well-formed hashes, but it should be consistent.
…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
Picks up the ChainRegistry networkId-collision fix for reused default chainIds (agglayer/sdk#28, commit b9a990c): registerChain() now graduates a re-registered chainId out of defaultChainIds even when it reuses one of the SDK's own defaults (e.g. Sepolia's real chainId, 11155111). Previously testnet mode's L1 token-metadata reads (add-custom-token, ERC20 name/ symbol/decimals) resolved to the SDK's pre-seeded Ethereum mainnet entry instead of Sepolia, routing eth_call requests to eth.llamarpc.com. This snapshot is the current tip of feat/aggkit-bridge-client, so it also carries PR #32's follow-up fixes on top -- notably a breaking change to AggkitBridgeAggregator.getClaimInputs (originNetworkId -> recordingNetworkId, and a claimable result union instead of a thrown not-ready error). See the companion commit migrating app/hooks/useClaimExecution.ts to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
What & why
Adds a self-contained
src/aggkit/module: a typed client and multi-network aggregator for the aggkit bridge service REST API (/bridge/v1). This is the API surface behind the dev-ui's migration off the legacy Bridge Hub API (companion PR agglayer/agglayer-dev-ui#24): everything the UI needs — activity, status derivation, claim proofs, ready-to-claim counts, token metadata, and (later in this PR) bridge tracking — is served from aggkit through this module. The NATIVE module is untouched; the legacy bridge-hub proof path is retained (still used by the NATIVE claim flow) and marked@deprecated.14 commits, oldest layer first:
1. Bridge-service client + multi-network aggregator (
0fa7c77)AggkitBridgeClient— typed single-network client (bridges, claims, claim-proof, l1-info-tree-index, token-mappings, sync-status, health) with snake_case query serialization,page_size <= 200handling, BigInt-safeglobal_indexparsing (raw-text parse), and a typedAggkitApiError.AggkitBridgeAggregator— multi-network fan-out with partial-failure reporting (failedNetworks), tiered status derivation (BRIDGED → LEAF_INCLUDED → READY_TO_CLAIM → CLAIMED, with a targetedglobal_indexclaims probe to avoid page-1-only false positives), ready-to-claim counting, claim-proof orchestration, and token-metadata composition.ERC20.getMetadata()on-chain read helper (aggkit/token-mappingscarries no name/symbol/decimals).2. Claim correctness + proxy fault tolerance (
c472bd0)getInjectedL1InfoLeaf) gates READY_TO_CLAIM for L2-destination rows, closing a measured ≥8s premature-READY window whose claim proof would revertGlobalExitRootInvalidon-chain.getClaimInputsrebuilt on the destination-injected leaf index rather than the deposit's own L1-info-tree index (previously wrong for every L2-destination claim, incl. L1→L2 manual claims); additivesourceL1InfoTreeIndexfield.failedNetworksinstead of rejecting the whole call;getSyncStatussendsnetwork_idexplicitly (unqualified requests 400 through aggkit-proxy).3. Live smoke script + docs (
fef61f2,b706110,2ec0d5e)scripts/aggkit-smoke.tsextended to run against aggkit-proxy across networks {0,1,2} (full fan-out activity, real L2→L2 and L2→L1 claim-input checks, opt-in proxy-502 partial-failure section); multi-network proxy configuration and the status lifecycle documented.4. Bridge tracker API (
a8a44a0,c254ec2,3c6d475,5ad31b0)getBridgeTracking— tracker types + client method + aggregator wiring, reconciled against the live-captured wire format (not the docs, which were wrong pre-rc5 — see #1781); terminal-state, step-error, and transient-resolve semantics plus polling guidance documented. This is what the dev-ui tracker UI (companion PR) polls.5. rc5 phase (
fea252e,76fe6a4,8094f92,9a5a5b9,5bbda2b)fix(native): consumer-registered chains now win networkId collisions in the chain registry (see breaking-change note below) — previously the SDK's pre-seeded Ethereum mainnet (networkId 0) shadowed a consumer-registered devnet L1.Upstream issue context
expected_durationnever emitted). Fixed upstream in rc5 (fix: bridgetracker API.md drift, fail-fast on unset L1GlobalExitRootAddress, unlimited default rate limit (#1781, #1782, #1783) aggkit#1784). Our tracker types were never derived from the buggy docs — they're fixture-derived from live-captured rc4/rc5 wire payloads (30+ payloads checked against 0 violations during review) — so no functional fix was needed here, only a JSDoc pass (76fe6a4) to stop citing the pre-fix API.md text.WaitingClaimprecedes real claimability by a measured ~16s gap. No SDK-side change; the client surfaces tracker status as-is, and dev-ui handles the UX mitigation ("Finalizing claim data").Testing evidence
npm run typecheck,npm run lint: clean.npm run test:run: 113/113 unit tests passing across 5 files, including new registry-precedence tests (registry.test.ts, 6 tests) and the full tracker/aggregator/client suites (33 + 13 + 31 tests). The aggregator suite includes fixture-backed regressions for the L2→L2 premature-window/post-injection transition, the L2→L1 skip path, the 404 message-matching hazard, and the proxy-502 partial-failure path.npm run build: green (ESM + CJS +.d.tsall build successfully).scripts/aggkit-smoke.ts) exercises the client + aggregator against a real kurtosis devnet, both direct and through aggkit-proxy across networks {0,1,2} (migration-phase baseline run: 117 checks PASS / 0 FAIL).76fe6a4and re-verified (typecheck/lintclean, 113/113 tests). Full attack-list writeup lives in a local plans workspace, not linkable — summarized here.Breaking-change notes
None for existing consumers — the entire aggkit module is additive (new type/value exports only), and the NATIVE surface is untouched. The chain-registry fix (
8094f92) only changes behavior when a consumer explicitly registers a chain whosenetworkIdcollides with one of the SDK's built-in defaults — previously the built-in default silently won that collision; now the consumer's registration wins, which is the documented/expected precedence. No default-registry behavior changes for consumers who don't register overrides. LegacyfetchMerkleProofis retained (the NATIVE claim flow still uses it) and marked@deprecated.Reviewer notes
workflow_dispatchworkflow, not triggered on merge tomain.file:../sdklocal snapshot dependency during development, so it should be reviewed/merged in this order: sdk → dev-ui. kurtosis-cdk's companion change is independent of both (deploys the enclave-side backend + tracker).Citation sweep (agglayer/aggkit#1791)
Contributes to agglayer/aggkit#1791.
Local design-doc citations in this branch were replaced with self-contained comments in commits
3879690(sweep) +5680d83(review fix), with no behavior change: dist proven byte-identical after comment-stripping, 115/115 tests passing.Merging this PR does not complete agglayer/aggkit#1793 — the sdk release is a separate manual
workflow_dispatchstep (.github/workflows/release.yml), and the dev-ui dependency swap to the published version must follow it. agglayer/aggkit#1793 is tracked independently and gets closed manually once both of those steps have happened.Other issues to close
Closes agglayer/aggkit#1800.