@@ -457,29 +457,88 @@ pub async fn dispute_bounty(
457457}
458458
459459/// GET /api/v1/bounties/stats
460- pub async fn bounty_stats ( State ( state) : State < AppState > ) -> Result < Json < BountyStatsResponse > > {
461- let open = state. db . count_bounties_by_status ( "open" ) . await . unwrap_or ( 0 ) ;
462- let claimed = state
463- . db
464- . count_bounties_by_status ( "claimed" )
465- . await
466- . unwrap_or ( 0 ) ;
467- let completed = state
468- . db
469- . count_bounties_by_status ( "completed" )
470- . await
471- . unwrap_or ( 0 ) ;
460+ pub async fn bounty_stats (
461+ State ( state) : State < AppState > ,
462+ auth : Option < Extension < AuthenticatedDid > > ,
463+ ) -> Result < Json < BountyStatsResponse > > {
464+ let caller = auth. as_ref ( ) . map ( |e| e. 0 . 0 . as_str ( ) ) ;
465+ let mut memo = std:: collections:: HashMap :: new ( ) ;
466+ let mut open = 0i64 ;
467+ let mut claimed = 0i64 ;
468+ let mut completed = 0i64 ;
469+ let mut agent_earnings: std:: collections:: HashMap < String , ( i64 , i64 ) > = std:: collections:: HashMap :: new ( ) ;
470+
471+ let page_size = 200i64 ;
472+ let mut cursor: Option < ( String , String ) > = None ;
473+
474+ loop {
475+ let after = cursor. as_ref ( ) . map ( |( ts, id) | ( ts. as_str ( ) , id. as_str ( ) ) ) ;
476+ let bounties = state
477+ . db
478+ . list_bounties (
479+ None ,
480+ None ,
481+ None ,
482+ page_size,
483+ after. map ( |( ts, _) | ts) ,
484+ after. map ( |( _, id) | id) ,
485+ )
486+ . await ?;
487+ if bounties. is_empty ( ) {
488+ break ;
489+ }
472490
473- let leaders = state. db . bounty_leaderboard ( 10 ) . await . unwrap_or_default ( ) ;
474- let leaderboard = leaders
491+ let last = bounties. last ( ) . unwrap ( ) ;
492+ for b in & bounties {
493+ let key = format ! ( "{}/{}" , b. repo_owner, b. repo_name) ;
494+ let admitted = if let Some ( & result) = memo. get ( & key) {
495+ result
496+ } else {
497+ let result = crate :: api:: authorize_repo_read (
498+ & state,
499+ & b. repo_owner ,
500+ & b. repo_name ,
501+ caller,
502+ "/" ,
503+ )
504+ . await
505+ . is_ok ( ) ;
506+ memo. insert ( key, result) ;
507+ result
508+ } ;
509+
510+ if admitted {
511+ match b. status . as_str ( ) {
512+ "open" => open += 1 ,
513+ "claimed" => claimed += 1 ,
514+ "completed" => {
515+ completed += 1 ;
516+ if let Some ( ref claimant) = b. claimant_did {
517+ let entry = agent_earnings. entry ( claimant. clone ( ) ) . or_insert ( ( 0 , 0 ) ) ;
518+ entry. 0 += 1 ;
519+ entry. 1 += b. amount ;
520+ }
521+ }
522+ _ => { }
523+ }
524+ }
525+ }
526+
527+ cursor = Some ( ( last. created_at . clone ( ) , last. id . clone ( ) ) ) ;
528+ }
529+
530+ let mut leaderboard: Vec < AgentBountyEntry > = agent_earnings
475531 . into_iter ( )
476- . map ( |( did, cnt, total) | AgentBountyEntry {
532+ . map ( |( did, ( cnt, total) ) | AgentBountyEntry {
477533 did,
478534 completed : cnt,
479535 total_earned : total,
480536 } )
481537 . collect ( ) ;
482538
539+ leaderboard. sort_by ( |a, b| b. total_earned . cmp ( & a. total_earned ) . then_with ( || b. completed . cmp ( & a. completed ) ) ) ;
540+ leaderboard. truncate ( 10 ) ;
541+
483542 Ok ( Json ( BountyStatsResponse {
484543 open,
485544 claimed,
@@ -492,11 +551,66 @@ pub async fn bounty_stats(State(state): State<AppState>) -> Result<Json<BountySt
492551pub async fn agent_bounty_stats (
493552 State ( state) : State < AppState > ,
494553 Path ( did) : Path < String > ,
554+ auth : Option < Extension < AuthenticatedDid > > ,
495555) -> Result < Json < serde_json:: Value > > {
496- let ( count, total) = state. db . agent_bounty_stats ( & did) . await . unwrap_or ( ( 0 , 0 ) ) ;
556+ let caller = auth. as_ref ( ) . map ( |e| e. 0 . 0 . as_str ( ) ) ;
557+ let mut memo = std:: collections:: HashMap :: new ( ) ;
558+ let mut count = 0i64 ;
559+ let mut total = 0i64 ;
560+
561+ let page_size = 200i64 ;
562+ let mut cursor: Option < ( String , String ) > = None ;
563+
564+ loop {
565+ let after = cursor. as_ref ( ) . map ( |( ts, id) | ( ts. as_str ( ) , id. as_str ( ) ) ) ;
566+ let bounties = state
567+ . db
568+ . list_bounties (
569+ None ,
570+ None ,
571+ Some ( "completed" ) ,
572+ page_size,
573+ after. map ( |( ts, _) | ts) ,
574+ after. map ( |( _, id) | id) ,
575+ )
576+ . await ?;
577+ if bounties. is_empty ( ) {
578+ break ;
579+ }
580+
581+ let last = bounties. last ( ) . unwrap ( ) ;
582+ for b in & bounties {
583+ if b. claimant_did . as_deref ( ) . is_some_and ( |c| crate :: api:: did_matches ( & did, c) ) {
584+ let key = format ! ( "{}/{}" , b. repo_owner, b. repo_name) ;
585+ let admitted = if let Some ( & result) = memo. get ( & key) {
586+ result
587+ } else {
588+ let result = crate :: api:: authorize_repo_read (
589+ & state,
590+ & b. repo_owner ,
591+ & b. repo_name ,
592+ caller,
593+ "/" ,
594+ )
595+ . await
596+ . is_ok ( ) ;
597+ memo. insert ( key, result) ;
598+ result
599+ } ;
600+
601+ if admitted {
602+ count += 1 ;
603+ total += b. amount ;
604+ }
605+ }
606+ }
607+
608+ cursor = Some ( ( last. created_at . clone ( ) , last. id . clone ( ) ) ) ;
609+ }
610+
497611 Ok ( Json ( serde_json:: json!( {
498612 "did" : did,
499613 "completed_bounties" : count,
500614 "total_earned" : total,
501615 } ) ) )
502- }
616+ }
0 commit comments