1+ import { useMemo } from "react"
12import type { Address } from "viem"
2- import { useAttesterView } from "./useAttesterView "
3- import { SequencerStatus } from "./sequencerStatus "
4- import { contracts } from "@/contracts "
3+ import { useReadContracts } from "wagmi "
4+ import { contracts , getRollupVersions } from "@/contracts "
5+ import { attesterRollupCandidates , selectAttesterRollup } from "./attesterRollupSelection "
56
67/**
78 * Optional indexer-supplied hint about which rollup currently holds the
89 * live record. The indexer captures `moveWithRollup` from the originating
910 * tx's calldata and bulk-updates `effectiveRollup` whenever the canonical
10- * rollup rotates. If the caller has these handy, this hook uses them as
11- * the **preferred** probe candidate — saving an RPC roundtrip in the
12- * common case. The on-chain probe still runs as a safety net: if it sees a
13- * live record on a different rollup than the hint suggested, the probe's
14- * answer wins (the chain is authoritative).
11+ * rollup rotates. It's used to break ties when the chain shows records on
12+ * more than one rollup; it never overrides the chain (see
13+ * `selectAttesterRollup`).
1514 */
1615export interface AttesterViewHint {
1716 effectiveRollup : Address
1817 moveWithRollup : boolean | null
1918}
2019
2120/**
22- * Looks up an attester via `getAttesterView` against the candidate rollups
23- * (canonical, legacy/deposit-time, and an optional indexer hint) and returns
24- * whichever view recognises the attester (non-NONE status). Covers:
21+ * Looks up an attester via `getAttesterView` on every rollup version the
22+ * Registry has made canonical (plus the deposit-time rollup and indexer hint,
23+ * if not already among them), in one multicall, and returns whichever view
24+ * recognises the attester (non-NONE status). Covers:
2525 *
26- * - Active sequencer on canonical rollup with old delegation record — the
27- * legacy view returns NONE; we fall through to canonical.
28- * This is the `moveWithRollup = true` (auto-migrating) case : the stake
29- * follows the canonical rollup as it upgrades .
30- * - Legacy stake mid-withdrawal — canonical returns NONE; we fall through
31- * to legacy so the exit data is still visible.
32- * This is the `moveWithRollup = false` case: the stake stays on the
33- * rollup it was originally deposited on .
34- * - Genuinely unregistered — both NONE; we return the canonical view so
35- * callers still get a well-defined result .
26+ * - Auto-migrating stakes (`moveWithRollup = true`): the record follows
27+ * canonical, so the deposit-time rollup returns NONE and canonical has it .
28+ * - Pinned stakes ( `moveWithRollup = false`) : the record stays on the
29+ * deposit-time rollup.
30+ * - Exits on an intermediate rollup: an exit (voluntary, or a ZOMBIE from
31+ * slashing) stays locked on the rollup it started on, which may be
32+ * neither the deposit-time rollup nor canonical, and the indexer hint may
33+ * have moved past it. Probing every version finds it anyway .
34+ * - Genuinely unregistered: every read is NONE; we return a well-defined
35+ * NONE result (see `selectAttesterRollup` for which rollup it's from) .
3636 *
3737 * Returns the **effective rollup** the live record was found on, alongside
3838 * the view data. Callers that need to send writes (unstake, etc.) should
@@ -49,107 +49,60 @@ export function useAttesterViewBestEffort(
4949 hint ?: AttesterViewHint | null ,
5050) {
5151 const canonicalRollup = contracts . rollup . address
52- const isLegacyDifferent =
53- ! ! rollupAddress && rollupAddress . toLowerCase ( ) !== canonicalRollup . toLowerCase ( )
54-
55- // The indexer hint may match canonical or legacy or a third address. Only
56- // probe the hint separately when it doesn't coincide with one we'd probe
57- // anyway. This keeps the common case to exactly two reads (canonical +
58- // legacy) while still supporting unusual hints.
5952 const hintRollup = hint ?. effectiveRollup
60- const hintIsCanonical =
61- ! ! hintRollup && hintRollup . toLowerCase ( ) === canonicalRollup . toLowerCase ( )
62- const hintIsLegacy =
63- ! ! hintRollup && ! ! rollupAddress && hintRollup . toLowerCase ( ) === rollupAddress . toLowerCase ( )
64- const hintNeedsOwnProbe = ! ! hintRollup && ! hintIsCanonical && ! hintIsLegacy
6553
66- const canonicalView = useAttesterView ( attesterAddress , canonicalRollup )
67- const legacyView = useAttesterView (
68- attesterAddress ,
69- isLegacyDifferent ? rollupAddress : undefined ,
70- )
71- const hintView = useAttesterView (
72- attesterAddress ,
73- hintNeedsOwnProbe ? hintRollup : undefined ,
54+ const candidates = useMemo (
55+ ( ) =>
56+ attesterRollupCandidates (
57+ canonicalRollup ,
58+ getRollupVersions ( ) . map ( ( v ) => v . address ) ,
59+ rollupAddress ,
60+ hintRollup ,
61+ ) ,
62+ [ canonicalRollup , rollupAddress , hintRollup ] ,
7463 )
7564
76- const canonicalHasRecord =
77- canonicalView . status !== undefined && canonicalView . status !== SequencerStatus . NONE
78- const legacyHasRecord =
79- isLegacyDifferent &&
80- legacyView . status !== undefined &&
81- legacyView . status !== SequencerStatus . NONE
82- // `hintHasRecord` is gated on `hintNeedsOwnProbe` — by construction it's
83- // only true when the hint address differs from both canonical and legacy
84- // (i.e. a third probe was actually issued). When the hint coincides with
85- // canonical or legacy, the corresponding `*HasRecord` flag already
86- // covers it; this variable would be redundant there. Don't widen this
87- // without also reworking the resolution order, or you'll double-count
88- // the hint coverage.
89- const hintHasRecord =
90- hintNeedsOwnProbe &&
91- hintView . status !== undefined &&
92- hintView . status !== SequencerStatus . NONE
65+ const { data, isLoading, error, refetch } = useReadContracts ( {
66+ contracts : candidates . map ( ( address ) => ( {
67+ address,
68+ abi : contracts . rollup . abi ,
69+ functionName : "getAttesterView" as const ,
70+ args : attesterAddress ? ( [ attesterAddress ] as const ) : undefined ,
71+ } ) ) ,
72+ query : {
73+ enabled : ! ! attesterAddress ,
74+ } ,
75+ } )
76+
77+ const views = candidates . map ( ( _ , i ) => {
78+ const entry = data ?. [ i ]
79+ return entry ?. status === "success" ? entry . result : undefined
80+ } )
81+ const selected = selectAttesterRollup (
82+ candidates ,
83+ views . map ( ( v ) => v ?. status ) ,
84+ hintRollup ,
85+ )
86+ const attesterView = views [ selected ]
9387
94- // Resolution preference, from highest to lowest priority:
95- // 1. Indexer hint, if its own probe found a record (it's the most
96- // specific signal — captured from on-chain calldata + migration
97- // tracking).
98- // 2. Canonical wins when it has a record (covers moveWithRollup=true).
99- // 3. Otherwise, legacy if it has a record (covers moveWithRollup=false
100- // or mid-withdrawal on a legacy rollup).
101- // 4. Otherwise, fall back to canonical for a well-defined NONE result.
102- //
103- // The chain is always authoritative: the hint never overrides a probe
104- // that found the validator on a different rollup. This keeps the design
105- // future-proof against indexer regressions.
106- let preferred = canonicalView
107- let effectiveRollup : Address = canonicalRollup
108- if ( hintHasRecord && hintRollup ) {
109- preferred = hintView
110- effectiveRollup = hintRollup
111- } else if ( canonicalHasRecord ) {
112- preferred = canonicalView
113- effectiveRollup = canonicalRollup
114- } else if ( legacyHasRecord && rollupAddress ) {
115- preferred = legacyView
116- effectiveRollup = rollupAddress
117- } else if ( hint && hintRollup && ( hintIsCanonical || hintIsLegacy ) ) {
118- // No probe found a record, but the indexer hint coincided with one
119- // we already probed. Honour the hint as the "where this stake last
120- // lived" answer rather than forcing canonical, so the operator at
121- // least sees a stable target. The probe NONE will still gate the
122- // unstake button via `useSequencerStatus`.
123- //
124- // Also align `preferred` with the chosen rollup. Otherwise the
125- // returned status/exit/balance would come from the canonicalView
126- // probe while `effectiveRollup` points at legacy — a self-
127- // inconsistent result. Both views returned NONE in this branch, so
128- // the swap is purely cosmetic (loading/error states stay correct),
129- // but it keeps the contract "preferred describes effectiveRollup".
130- effectiveRollup = hintRollup
131- if ( hintIsLegacy ) {
132- preferred = legacyView
133- }
134- }
88+ // A failed read could be the one rollup holding the record, so surface it
89+ // rather than silently presenting a NONE result.
90+ const readError =
91+ error ?? data ?. find ( ( entry ) => entry . status === "failure" ) ?. error ?? null
13592
13693 return {
137- ...preferred ,
94+ attesterView,
95+ status : attesterView ?. status ,
96+ effectiveBalance : attesterView ?. effectiveBalance ,
97+ exit : attesterView ?. exit ,
98+ config : attesterView ?. config ,
13899 /** The rollup the live record was found on. Use this as the target for
139100 * any unstake / withdraw write — see hook docstring. */
140- effectiveRollup,
141- isLoading :
142- canonicalView . isLoading ||
143- ( isLegacyDifferent && legacyView . isLoading ) ||
144- ( hintNeedsOwnProbe && hintView . isLoading ) ,
145- error :
146- preferred . error ||
147- ( isLegacyDifferent ? legacyView . error : undefined ) ||
148- ( hintNeedsOwnProbe ? hintView . error : undefined ) ,
101+ effectiveRollup : candidates [ selected ] ,
102+ isLoading,
103+ error : readError ,
149104 refetch : ( ) => {
150- canonicalView . refetch ( )
151- if ( isLegacyDifferent ) legacyView . refetch ( )
152- if ( hintNeedsOwnProbe ) hintView . refetch ( )
105+ refetch ( )
153106 } ,
154107 }
155108}
0 commit comments