@@ -56,16 +56,8 @@ pub fn claim_milestone(
5656
5757 let is_crowdfunding = matches ! ( event. pillar, Pillar :: Crowdfunding ) ;
5858
59- let anchor_idx =
60- storage:: get_grant_recipient_idx ( env, event_id, & recipient) . ok_or ( Error :: NoSubmissions ) ?;
61- let anchor = storage:: winner_at ( env, event_id, anchor_idx) . ok_or ( Error :: NoSubmissions ) ?;
62- if anchor. recipient != recipient || anchor. milestone . is_some ( ) {
63- return Err ( Error :: NoSubmissions ) ;
64- }
65- let position = anchor. position ;
66- let reputation_bump = anchor. reputation_bump . unwrap_or ( 0 ) ;
67- let already_claimed_for_recipient =
68- storage:: get_grant_recipient_claim_count ( env, event_id, & recipient) ;
59+ let ( position, reputation_bump, already_claimed_for_recipient) =
60+ resolve_recipient ( env, event_id, & recipient) ?;
6961 let already_paid_to_recipient = if is_crowdfunding {
7062 0
7163 } else {
@@ -167,3 +159,62 @@ pub fn claim_milestone(
167159 idempotency:: mark_seen ( env, & op_id) ;
168160 Ok ( ( ) )
169161}
162+
163+ /// Resolve recipient anchor (position + reputation_bump) and milestone claim count.
164+ ///
165+ /// Tries the O(1) index path first. If the indexes don't exist (pre-upgrade events),
166+ /// falls back to a linear scan over winner rows and persists the indexes so future
167+ /// calls use the fast path.
168+ fn resolve_recipient (
169+ env : & Env ,
170+ event_id : u64 ,
171+ recipient : & Address ,
172+ ) -> Result < ( u32 , u32 , u32 ) , Error > {
173+ // Fast path: indexes already exist (post-upgrade events).
174+ if let Some ( anchor_idx) = storage:: get_grant_recipient_idx ( env, event_id, recipient) {
175+ let anchor = storage:: winner_at ( env, event_id, anchor_idx) . ok_or ( Error :: NoSubmissions ) ?;
176+ if anchor. recipient != * recipient || anchor. milestone . is_some ( ) {
177+ return Err ( Error :: NoSubmissions ) ;
178+ }
179+ let claim_count = storage:: get_grant_recipient_claim_count ( env, event_id, recipient) ;
180+ return Ok ( (
181+ anchor. position ,
182+ anchor. reputation_bump . unwrap_or ( 0 ) ,
183+ claim_count,
184+ ) ) ;
185+ }
186+
187+ // Legacy fallback: scan winner rows, which works before the indexes existed.
188+ let count = storage:: winner_count ( env, event_id) ;
189+ let mut anchor_idx = None ;
190+ let mut position = None ;
191+ let mut reputation_bump = 0 ;
192+ let mut claim_count = 0u32 ;
193+ for idx in 0 ..count {
194+ let w = match storage:: winner_at ( env, event_id, idx) {
195+ Some ( w) => w,
196+ None => continue ,
197+ } ;
198+ if w. recipient != * recipient {
199+ continue ;
200+ }
201+ match w. milestone {
202+ None => {
203+ anchor_idx = Some ( idx) ;
204+ position = Some ( w. position ) ;
205+ reputation_bump = w. reputation_bump . unwrap_or ( 0 ) ;
206+ }
207+ Some ( _) => {
208+ claim_count = claim_count. saturating_add ( 1 ) ;
209+ }
210+ }
211+ }
212+ let idx = anchor_idx. ok_or ( Error :: NoSubmissions ) ?;
213+ let pos = position. ok_or ( Error :: NoSubmissions ) ?;
214+
215+ // Persist indexes for future O(1) lookups.
216+ storage:: set_grant_recipient_idx ( env, event_id, recipient, idx) ;
217+ storage:: set_grant_recipient_claim_count ( env, event_id, recipient, claim_count) ;
218+
219+ Ok ( ( pos, reputation_bump, claim_count) )
220+ }
0 commit comments