Summary
AggkitBridgeAggregator.getReadyToClaimCount and AggkitBridgeAggregator.toTransaction derive claimability from the same two source signals (an L1-info-tree-index probe on the recording network, then a targeted claim check) but disagree on a third: whether the destination network has actually injected the corresponding GER. toTransaction checks it via resolveInjectedLeafIndex; getReadyToClaimCount does not call that method at all. The result is that the "ready to claim" count can include a deposit that the activity list (built by toTransaction) correctly reports as LEAF_INCLUDED — i.e. not yet claimable — and a user who acts on the badge builds a claim proof against an index the destination hasn't ingested yet, which reverts on-chain with GlobalExitRootInvalid.
This is pre-existing behavior from #28, not a regression from the recent review-response work.
Where (verified against fix/aggkit-pr28-review, commit 2959a83, working tree as of this filing — a concurrent edit to unrelated helpers in the same file was in progress, so exact line numbers may drift further; anchor on the function/method names if so)
src/aggkit/aggregator.ts, getReadyToClaimCount (method starts at line 515). Its per-row predicate, in the readyFlags block (lines 585-622), does exactly two things per unclaimed row: call getL1InfoTreeIndex on the recording network, then (if the source probe is ready) call confirmClaimed. A row counts as ready iff confirmedClaim === null. It never calls resolveInjectedLeafIndex.
src/aggkit/aggregator.ts, toTransaction (method starts at line 1083). For the same two prior checks (source probe ready, confirmClaimed returns null) and a non-zero destination_network, it additionally calls this.resolveInjectedLeafIndex({ destinationNetworkId, sourceL1InfoTreeIndex }). If that resolves to kind === 'not-ready' (the destination hasn't injected the leaf yet), status is set to LEAF_INCLUDED, explicitly not READY_TO_CLAIM.
resolveInjectedLeafIndex (same file) is the single place that queries the destination's /injected-l1-info-leaf endpoint; getReadyToClaimCount has no equivalent call anywhere in its predicate.
src/aggkit/types.ts:389-398 documents the state machine: LEAF_INCLUDED is explicitly the case where "the source leaf exists but the destination's GER injection lags," and READY_TO_CLAIM for an L2 destination requires "both source included AND destination injected." getReadyToClaimCount only ever checks the first half of that conjunction.
Reproduction shape
Two configured networks {1, 2}. One unclaimed bridge recorded on network 1, destination_network: 2, deposit_count: 5:
GET /l1-info-tree-index?network_id=1&deposit_count=5 → 200, ready, some index N (source settled on the L1 info tree).
GET /injected-l1-info-leaf?network_id=2&leaf_index=N → 404 "not injected yet" (destination hasn't ingested the GER).
With this data:
getActivity (via toTransaction) returns this row with status: 'LEAF_INCLUDED' and no claim proof — correct, matches the documented state machine.
getReadyToClaimCount counts this same row as ready (its predicate returns true once the source probe is ready and confirmClaimed returns null, having never queried /injected-l1-info-leaf) — incorrect.
The badge (backed by getReadyToClaimCount, consumed via app/hooks/useReadyToClaimCount.ts in the dev-ui) therefore advertises a claimable deposit that the activity list, on the same data, correctly shows as not yet claimable. Building and submitting a claim proof for that deposit reverts on-chain with GlobalExitRootInvalid.
Relationship to #30
#30 tracks a redesign of this same code path — the readyFlags/fan-out logic in getReadyToClaimCount is explicitly in scope there (its own Background section names getReadyToClaimCount directly, and its Scope item 3 already covers the ready-probe concurrency/error-surfacing behavior in this exact predicate). This issue does not propose an independent fix; it's the correctness gap in the same predicate that #30's rework should close, so whoever implements #30 should fold in the missing resolveInjectedLeafIndex (or equivalent injected-GER) check when the per-row readiness logic is rewritten, rather than carrying it forward unaddressed.
Suggested direction (for whoever picks this up)
Mirror toTransaction: after the source probe succeeds and before/alongside confirmClaimed, call resolveInjectedLeafIndex({ destinationNetworkId: row.bridge.destination_network, sourceL1InfoTreeIndex: probe.value }) and treat kind === 'not-ready' as not-ready-to-claim, the same way toTransaction derives LEAF_INCLUDED. This adds one probe per remaining candidate row; if that per-request cost is unacceptable for the badge, the alternative is to explicitly document the count as an upper bound rather than leave the two paths silently disagreeing.
Summary
AggkitBridgeAggregator.getReadyToClaimCountandAggkitBridgeAggregator.toTransactionderive claimability from the same two source signals (an L1-info-tree-index probe on the recording network, then a targeted claim check) but disagree on a third: whether the destination network has actually injected the corresponding GER.toTransactionchecks it viaresolveInjectedLeafIndex;getReadyToClaimCountdoes not call that method at all. The result is that the "ready to claim" count can include a deposit that the activity list (built bytoTransaction) correctly reports asLEAF_INCLUDED— i.e. not yet claimable — and a user who acts on the badge builds a claim proof against an index the destination hasn't ingested yet, which reverts on-chain withGlobalExitRootInvalid.This is pre-existing behavior from #28, not a regression from the recent review-response work.
Where (verified against
fix/aggkit-pr28-review, commit2959a83, working tree as of this filing — a concurrent edit to unrelated helpers in the same file was in progress, so exact line numbers may drift further; anchor on the function/method names if so)src/aggkit/aggregator.ts,getReadyToClaimCount(method starts at line 515). Its per-row predicate, in thereadyFlagsblock (lines 585-622), does exactly two things per unclaimed row: callgetL1InfoTreeIndexon the recording network, then (if the source probe isready) callconfirmClaimed. A row counts as ready iffconfirmedClaim === null. It never callsresolveInjectedLeafIndex.src/aggkit/aggregator.ts,toTransaction(method starts at line 1083). For the same two prior checks (source probe ready,confirmClaimedreturns null) and a non-zerodestination_network, it additionally callsthis.resolveInjectedLeafIndex({ destinationNetworkId, sourceL1InfoTreeIndex }). If that resolves tokind === 'not-ready'(the destination hasn't injected the leaf yet), status is set toLEAF_INCLUDED, explicitly notREADY_TO_CLAIM.resolveInjectedLeafIndex(same file) is the single place that queries the destination's/injected-l1-info-leafendpoint;getReadyToClaimCounthas no equivalent call anywhere in its predicate.src/aggkit/types.ts:389-398documents the state machine:LEAF_INCLUDEDis explicitly the case where "the source leaf exists but the destination's GER injection lags," andREADY_TO_CLAIMfor an L2 destination requires "both source included AND destination injected."getReadyToClaimCountonly ever checks the first half of that conjunction.Reproduction shape
Two configured networks
{1, 2}. One unclaimed bridge recorded on network 1,destination_network: 2,deposit_count: 5:GET /l1-info-tree-index?network_id=1&deposit_count=5→200, ready, some indexN(source settled on the L1 info tree).GET /injected-l1-info-leaf?network_id=2&leaf_index=N→404"not injected yet" (destination hasn't ingested the GER).With this data:
getActivity(viatoTransaction) returns this row withstatus: 'LEAF_INCLUDED'and no claim proof — correct, matches the documented state machine.getReadyToClaimCountcounts this same row as ready (its predicate returnstrueonce the source probe is ready andconfirmClaimedreturns null, having never queried/injected-l1-info-leaf) — incorrect.The badge (backed by
getReadyToClaimCount, consumed viaapp/hooks/useReadyToClaimCount.tsin the dev-ui) therefore advertises a claimable deposit that the activity list, on the same data, correctly shows as not yet claimable. Building and submitting a claim proof for that deposit reverts on-chain withGlobalExitRootInvalid.Relationship to #30
#30 tracks a redesign of this same code path — the
readyFlags/fan-out logic ingetReadyToClaimCountis explicitly in scope there (its own Background section namesgetReadyToClaimCountdirectly, and its Scope item 3 already covers the ready-probe concurrency/error-surfacing behavior in this exact predicate). This issue does not propose an independent fix; it's the correctness gap in the same predicate that #30's rework should close, so whoever implements #30 should fold in the missingresolveInjectedLeafIndex(or equivalent injected-GER) check when the per-row readiness logic is rewritten, rather than carrying it forward unaddressed.Suggested direction (for whoever picks this up)
Mirror
toTransaction: after the source probe succeeds and before/alongsideconfirmClaimed, callresolveInjectedLeafIndex({ destinationNetworkId: row.bridge.destination_network, sourceL1InfoTreeIndex: probe.value })and treatkind === 'not-ready'as not-ready-to-claim, the same waytoTransactionderivesLEAF_INCLUDED. This adds one probe per remaining candidate row; if that per-request cost is unacceptable for the badge, the alternative is to explicitly document the count as an upper bound rather than leave the two paths silently disagreeing.