@provable-games/budokan-sdk 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { RpcProvider } from 'starknet';
2
- import { EntryFee, EntryRequirement, QualificationProof } from '@provable-games/metagame-sdk';
2
+ import { EntryFee, EntryRequirement } from '@provable-games/metagame-sdk';
3
3
 
4
4
  type DataSource = "api" | "rpc";
5
5
  interface BudokanClientConfig {
@@ -111,9 +111,20 @@ interface TournamentListParams {
111
111
  whitelistedExtensions?: string[];
112
112
  includePrizeSummary?: "summary" | boolean;
113
113
  }
114
+ /**
115
+ * Discriminator for `QualificationEntry.qualificationKind`. Picks one of
116
+ * the two terminal variants of the on-chain `QualificationProof` enum
117
+ * (NFT, Extension). `nftTokenId` is populated only for `nft`;
118
+ * `extensionConfig` only for `extension`.
119
+ */
120
+ type QualificationKind = "nft" | "extension";
114
121
  interface QualificationEntry {
115
122
  tournamentId: string;
116
- qualificationProof: QualificationProof | null;
123
+ qualificationKind: QualificationKind;
124
+ /** Populated when `qualificationKind === "nft"`. u256 token id, decimal string. */
125
+ nftTokenId: string | null;
126
+ /** Populated when `qualificationKind === "extension"`. List of felt252 hex strings. */
127
+ extensionConfig: string[] | null;
117
128
  entryCount: number;
118
129
  }
119
130
 
@@ -141,23 +152,6 @@ interface Registration {
141
152
  isBanned: boolean;
142
153
  }
143
154
 
144
- type RewardType = {
145
- Prize: {
146
- tokenAddress: string;
147
- tokenType: string;
148
- };
149
- } | {
150
- EntryFee: EntryFeeRewardType;
151
- };
152
- type EntryFeeRewardType = {
153
- Position: number;
154
- } | {
155
- TournamentCreator: Record<string, never>;
156
- } | {
157
- GameCreator: Record<string, never>;
158
- } | {
159
- Refund: string;
160
- };
161
155
  interface Prize {
162
156
  prizeId: string;
163
157
  tournamentId: string;
@@ -174,9 +168,25 @@ interface Prize {
174
168
  distributionCount: number | null;
175
169
  sponsorAddress: string;
176
170
  }
171
+ /**
172
+ * Discriminator for `RewardClaim.claimKind`. Picks one of the six terminal
173
+ * variants of the on-chain `RewardType` enum (Prize::Single, Prize::Distributed,
174
+ * EntryFee::Position / TournamentCreator / GameCreator / Refund). The
175
+ * variant-specific fields below are populated only for the kinds that carry
176
+ * them; the two pure-marker creator kinds leave all four nullable fields null.
177
+ */
178
+ type RewardClaimKind = "prize_single" | "prize_distributed" | "entry_fee_position" | "entry_fee_tournament_creator" | "entry_fee_game_creator" | "entry_fee_refund";
177
179
  interface RewardClaim {
178
180
  tournamentId: string;
179
- rewardType: RewardType;
181
+ claimKind: RewardClaimKind;
182
+ /** Populated for `prize_single` and `prize_distributed`. Stringified u64. */
183
+ prizeId: string | null;
184
+ /** Populated for `prize_distributed`. */
185
+ payoutIndex: number | null;
186
+ /** Populated for `entry_fee_position`. */
187
+ position: number | null;
188
+ /** Populated for `entry_fee_refund`. felt252 hex string of the game token. */
189
+ refundTokenId: string | null;
180
190
  claimed: boolean;
181
191
  }
182
192
  interface PrizeAggregation {
@@ -472,4 +482,39 @@ declare class BudokanClient {
472
482
  */
473
483
  declare function createBudokanClient(config: BudokanClientConfig): BudokanClient;
474
484
 
475
- export { BudokanClient as B, type ConnectionMode as C, type DataSource as D, type GameConfig as G, type LeaderboardConfig as L, type PrizeAggregation as P, type QualificationEntry as Q, type Registration as R, type Schedule as S, type Tournament as T, type WSSubscribeOptions as W, type Prize as a, type PaginatedResult as b, type RewardClaim as c, type RewardClaimSummary as d, type TournamentListParams as e, type PlatformStats as f, type PrizeStats as g, type WSEventHandler as h, type BudokanClientConfig as i, ConnectionStatus as j, type ConnectionStatusState as k, type LeaderboardEntry as l, type Phase as m, type WSChannel as n, type WSEventMessage as o, type WSMessage as p, type WSSubscribeMessage as q, type WSUnsubscribeMessage as r, createBudokanClient as s };
485
+ /**
486
+ * One row per (player token, tournament) where the token's final rank fell
487
+ * inside a paid position. Multiple placements per tournament are possible
488
+ * when the player owns several tokens entered into the same tournament.
489
+ */
490
+ interface PlayerPlacement {
491
+ tournamentId: string;
492
+ tokenId: string;
493
+ /** 1-indexed rank in the tournament's final leaderboard. */
494
+ position: number;
495
+ /** Token's final score as a decimal string (preserves felt252 precision). */
496
+ score: string;
497
+ }
498
+ /**
499
+ * Aggregate rewards summary for a player address. Computed against current
500
+ * NFT ownership (denshokan), not historical attribution — see PR #243.
501
+ *
502
+ * `tournaments`, `prizes`, and `rewardClaims` are restricted to tournaments
503
+ * where the player has at least one placement. Consumers compute USD values
504
+ * by walking placements + prize/entry-fee data + token prices client-side.
505
+ */
506
+ interface PlayerRewards {
507
+ /** Count of placements that landed on a paid position. */
508
+ wins: number;
509
+ /** Lowest position number across all placements; null when no wins. */
510
+ bestPlacement: number | null;
511
+ placements: PlayerPlacement[];
512
+ /** Tournaments where the player placed (subset of currently-held entries). */
513
+ tournaments: Tournament[];
514
+ /** All sponsored prizes for those tournaments. */
515
+ prizes: Prize[];
516
+ /** All reward claims for those tournaments. */
517
+ rewardClaims: RewardClaim[];
518
+ }
519
+
520
+ export { BudokanClient as B, type ConnectionMode as C, type DataSource as D, type GameConfig as G, type LeaderboardConfig as L, type PrizeAggregation as P, type QualificationEntry as Q, type Registration as R, type Schedule as S, type Tournament as T, type WSSubscribeOptions as W, type Prize as a, type PaginatedResult as b, type RewardClaim as c, type RewardClaimSummary as d, type TournamentListParams as e, type PlatformStats as f, type PrizeStats as g, type WSEventHandler as h, type BudokanClientConfig as i, ConnectionStatus as j, type ConnectionStatusState as k, type LeaderboardEntry as l, type Phase as m, type PlayerPlacement as n, type PlayerRewards as o, type WSChannel as p, type WSEventMessage as q, type WSMessage as r, type WSSubscribeMessage as s, type WSUnsubscribeMessage as t, createBudokanClient as u };
@@ -1,5 +1,5 @@
1
1
  import { RpcProvider } from 'starknet';
2
- import { EntryFee, EntryRequirement, QualificationProof } from '@provable-games/metagame-sdk';
2
+ import { EntryFee, EntryRequirement } from '@provable-games/metagame-sdk';
3
3
 
4
4
  type DataSource = "api" | "rpc";
5
5
  interface BudokanClientConfig {
@@ -111,9 +111,20 @@ interface TournamentListParams {
111
111
  whitelistedExtensions?: string[];
112
112
  includePrizeSummary?: "summary" | boolean;
113
113
  }
114
+ /**
115
+ * Discriminator for `QualificationEntry.qualificationKind`. Picks one of
116
+ * the two terminal variants of the on-chain `QualificationProof` enum
117
+ * (NFT, Extension). `nftTokenId` is populated only for `nft`;
118
+ * `extensionConfig` only for `extension`.
119
+ */
120
+ type QualificationKind = "nft" | "extension";
114
121
  interface QualificationEntry {
115
122
  tournamentId: string;
116
- qualificationProof: QualificationProof | null;
123
+ qualificationKind: QualificationKind;
124
+ /** Populated when `qualificationKind === "nft"`. u256 token id, decimal string. */
125
+ nftTokenId: string | null;
126
+ /** Populated when `qualificationKind === "extension"`. List of felt252 hex strings. */
127
+ extensionConfig: string[] | null;
117
128
  entryCount: number;
118
129
  }
119
130
 
@@ -141,23 +152,6 @@ interface Registration {
141
152
  isBanned: boolean;
142
153
  }
143
154
 
144
- type RewardType = {
145
- Prize: {
146
- tokenAddress: string;
147
- tokenType: string;
148
- };
149
- } | {
150
- EntryFee: EntryFeeRewardType;
151
- };
152
- type EntryFeeRewardType = {
153
- Position: number;
154
- } | {
155
- TournamentCreator: Record<string, never>;
156
- } | {
157
- GameCreator: Record<string, never>;
158
- } | {
159
- Refund: string;
160
- };
161
155
  interface Prize {
162
156
  prizeId: string;
163
157
  tournamentId: string;
@@ -174,9 +168,25 @@ interface Prize {
174
168
  distributionCount: number | null;
175
169
  sponsorAddress: string;
176
170
  }
171
+ /**
172
+ * Discriminator for `RewardClaim.claimKind`. Picks one of the six terminal
173
+ * variants of the on-chain `RewardType` enum (Prize::Single, Prize::Distributed,
174
+ * EntryFee::Position / TournamentCreator / GameCreator / Refund). The
175
+ * variant-specific fields below are populated only for the kinds that carry
176
+ * them; the two pure-marker creator kinds leave all four nullable fields null.
177
+ */
178
+ type RewardClaimKind = "prize_single" | "prize_distributed" | "entry_fee_position" | "entry_fee_tournament_creator" | "entry_fee_game_creator" | "entry_fee_refund";
177
179
  interface RewardClaim {
178
180
  tournamentId: string;
179
- rewardType: RewardType;
181
+ claimKind: RewardClaimKind;
182
+ /** Populated for `prize_single` and `prize_distributed`. Stringified u64. */
183
+ prizeId: string | null;
184
+ /** Populated for `prize_distributed`. */
185
+ payoutIndex: number | null;
186
+ /** Populated for `entry_fee_position`. */
187
+ position: number | null;
188
+ /** Populated for `entry_fee_refund`. felt252 hex string of the game token. */
189
+ refundTokenId: string | null;
180
190
  claimed: boolean;
181
191
  }
182
192
  interface PrizeAggregation {
@@ -472,4 +482,39 @@ declare class BudokanClient {
472
482
  */
473
483
  declare function createBudokanClient(config: BudokanClientConfig): BudokanClient;
474
484
 
475
- export { BudokanClient as B, type ConnectionMode as C, type DataSource as D, type GameConfig as G, type LeaderboardConfig as L, type PrizeAggregation as P, type QualificationEntry as Q, type Registration as R, type Schedule as S, type Tournament as T, type WSSubscribeOptions as W, type Prize as a, type PaginatedResult as b, type RewardClaim as c, type RewardClaimSummary as d, type TournamentListParams as e, type PlatformStats as f, type PrizeStats as g, type WSEventHandler as h, type BudokanClientConfig as i, ConnectionStatus as j, type ConnectionStatusState as k, type LeaderboardEntry as l, type Phase as m, type WSChannel as n, type WSEventMessage as o, type WSMessage as p, type WSSubscribeMessage as q, type WSUnsubscribeMessage as r, createBudokanClient as s };
485
+ /**
486
+ * One row per (player token, tournament) where the token's final rank fell
487
+ * inside a paid position. Multiple placements per tournament are possible
488
+ * when the player owns several tokens entered into the same tournament.
489
+ */
490
+ interface PlayerPlacement {
491
+ tournamentId: string;
492
+ tokenId: string;
493
+ /** 1-indexed rank in the tournament's final leaderboard. */
494
+ position: number;
495
+ /** Token's final score as a decimal string (preserves felt252 precision). */
496
+ score: string;
497
+ }
498
+ /**
499
+ * Aggregate rewards summary for a player address. Computed against current
500
+ * NFT ownership (denshokan), not historical attribution — see PR #243.
501
+ *
502
+ * `tournaments`, `prizes`, and `rewardClaims` are restricted to tournaments
503
+ * where the player has at least one placement. Consumers compute USD values
504
+ * by walking placements + prize/entry-fee data + token prices client-side.
505
+ */
506
+ interface PlayerRewards {
507
+ /** Count of placements that landed on a paid position. */
508
+ wins: number;
509
+ /** Lowest position number across all placements; null when no wins. */
510
+ bestPlacement: number | null;
511
+ placements: PlayerPlacement[];
512
+ /** Tournaments where the player placed (subset of currently-held entries). */
513
+ tournaments: Tournament[];
514
+ /** All sponsored prizes for those tournaments. */
515
+ prizes: Prize[];
516
+ /** All reward claims for those tournaments. */
517
+ rewardClaims: RewardClaim[];
518
+ }
519
+
520
+ export { BudokanClient as B, type ConnectionMode as C, type DataSource as D, type GameConfig as G, type LeaderboardConfig as L, type PrizeAggregation as P, type QualificationEntry as Q, type Registration as R, type Schedule as S, type Tournament as T, type WSSubscribeOptions as W, type Prize as a, type PaginatedResult as b, type RewardClaim as c, type RewardClaimSummary as d, type TournamentListParams as e, type PlatformStats as f, type PrizeStats as g, type WSEventHandler as h, type BudokanClientConfig as i, ConnectionStatus as j, type ConnectionStatusState as k, type LeaderboardEntry as l, type Phase as m, type PlayerPlacement as n, type PlayerRewards as o, type WSChannel as p, type WSEventMessage as q, type WSMessage as r, type WSSubscribeMessage as s, type WSUnsubscribeMessage as t, createBudokanClient as u };
package/dist/react.cjs CHANGED
@@ -1114,6 +1114,84 @@ async function viewerPrizes(contract, tournamentId) {
1114
1114
  return result.map(parsePrize);
1115
1115
  }, contract.address);
1116
1116
  }
1117
+ function translateCairoRewardType(rewardType) {
1118
+ if (!rewardType || typeof rewardType !== "object") {
1119
+ throw new Error(`Unexpected RewardType payload: ${JSON.stringify(rewardType)}`);
1120
+ }
1121
+ const rt = rewardType;
1122
+ const outer = typeof rt.activeVariant === "function" ? rt.activeVariant() : null;
1123
+ const innerBag = typeof rt.activeVariant === "function" ? rt.variant : rt;
1124
+ if (outer === "Prize" || innerBag.Prize !== void 0) {
1125
+ const prize = innerBag.Prize;
1126
+ const subVariant = typeof prize?.activeVariant === "function" ? prize.activeVariant() : null;
1127
+ const subBag = typeof prize?.activeVariant === "function" ? prize.variant : prize;
1128
+ if (subVariant === "Single" || subBag?.Single !== void 0) {
1129
+ return {
1130
+ claimKind: "prize_single",
1131
+ prizeId: BigInt(subBag.Single).toString(),
1132
+ payoutIndex: null,
1133
+ position: null,
1134
+ refundTokenId: null
1135
+ };
1136
+ }
1137
+ if (subVariant === "Distributed" || subBag?.Distributed !== void 0) {
1138
+ const distributed = subBag.Distributed;
1139
+ const prizeId = distributed?.["0"] ?? distributed?.[0];
1140
+ const payoutIndex = distributed?.["1"] ?? distributed?.[1];
1141
+ return {
1142
+ claimKind: "prize_distributed",
1143
+ prizeId: BigInt(prizeId).toString(),
1144
+ payoutIndex: Number(payoutIndex),
1145
+ position: null,
1146
+ refundTokenId: null
1147
+ };
1148
+ }
1149
+ }
1150
+ if (outer === "EntryFee" || innerBag.EntryFee !== void 0) {
1151
+ const entryFee = innerBag.EntryFee;
1152
+ const subVariant = typeof entryFee?.activeVariant === "function" ? entryFee.activeVariant() : null;
1153
+ const subBag = typeof entryFee?.activeVariant === "function" ? entryFee.variant : entryFee;
1154
+ if (subVariant === "Position" || subBag?.Position !== void 0) {
1155
+ return {
1156
+ claimKind: "entry_fee_position",
1157
+ prizeId: null,
1158
+ payoutIndex: null,
1159
+ position: Number(subBag.Position),
1160
+ refundTokenId: null
1161
+ };
1162
+ }
1163
+ if (subVariant === "TournamentCreator" || subBag?.TournamentCreator !== void 0) {
1164
+ return {
1165
+ claimKind: "entry_fee_tournament_creator",
1166
+ prizeId: null,
1167
+ payoutIndex: null,
1168
+ position: null,
1169
+ refundTokenId: null
1170
+ };
1171
+ }
1172
+ if (subVariant === "GameCreator" || subBag?.GameCreator !== void 0) {
1173
+ return {
1174
+ claimKind: "entry_fee_game_creator",
1175
+ prizeId: null,
1176
+ payoutIndex: null,
1177
+ position: null,
1178
+ refundTokenId: null
1179
+ };
1180
+ }
1181
+ if (subVariant === "Refund" || subBag?.Refund !== void 0) {
1182
+ return {
1183
+ claimKind: "entry_fee_refund",
1184
+ prizeId: null,
1185
+ payoutIndex: null,
1186
+ position: null,
1187
+ refundTokenId: `0x${BigInt(subBag.Refund).toString(16)}`
1188
+ };
1189
+ }
1190
+ }
1191
+ throw new Error(
1192
+ `Unrecognised on-chain RewardType variant: ${JSON.stringify(rewardType)}`
1193
+ );
1194
+ }
1117
1195
  async function viewerRewardClaims(contract, tournamentId, offset, limit) {
1118
1196
  return wrapRpcCall(async () => {
1119
1197
  const result = await contract.call("tournament_reward_claims", [tournamentId, offset, limit]);
@@ -1121,7 +1199,7 @@ async function viewerRewardClaims(contract, tournamentId, offset, limit) {
1121
1199
  const claims = obj.claims?.map((raw) => {
1122
1200
  const claim = raw;
1123
1201
  return {
1124
- rewardType: claim.reward_type,
1202
+ ...translateCairoRewardType(claim.reward_type),
1125
1203
  claimed: Boolean(claim.claimed)
1126
1204
  };
1127
1205
  }) ?? [];
@@ -4518,8 +4596,7 @@ var BudokanClient = class {
4518
4596
  const result = await viewerRewardClaims(contract, tournamentId, offset, limit);
4519
4597
  const data = result.claims.map((c) => ({
4520
4598
  tournamentId,
4521
- rewardType: c.rewardType,
4522
- claimed: c.claimed
4599
+ ...c
4523
4600
  }));
4524
4601
  return { data, total: result.total, limit, offset };
4525
4602
  };
@@ -4794,7 +4871,6 @@ function useOwnedTournamentIds(owner, contextId) {
4794
4871
  enabled ? {
4795
4872
  owner,
4796
4873
  minterAddress: budokanAddress,
4797
- hasContext: true,
4798
4874
  ...{},
4799
4875
  limit: MAX_OWNED_TOKENS
4800
4876
  } : void 0
@@ -4910,7 +4986,11 @@ function useRegistrationsByOwner(tournamentId, owner, params) {
4910
4986
  }, [enabled, tokensResult]);
4911
4987
  const inner = useRegistrations(
4912
4988
  ownedGameTokenIds && ownedGameTokenIds.length > 0 ? tournamentId : void 0,
4913
- ownedGameTokenIds && ownedGameTokenIds.length > 0 ? { ...params, gameTokenIds: ownedGameTokenIds } : void 0
4989
+ ownedGameTokenIds && ownedGameTokenIds.length > 0 ? {
4990
+ limit: Math.min(ownedGameTokenIds.length, MAX_OWNED_TOKENS),
4991
+ ...params,
4992
+ gameTokenIds: ownedGameTokenIds
4993
+ } : void 0
4914
4994
  );
4915
4995
  if (ownedGameTokenIds !== null && ownedGameTokenIds.length === 0) {
4916
4996
  return {
@@ -5099,6 +5179,200 @@ function useActivityStats() {
5099
5179
  }, [fetch2]);
5100
5180
  return { stats, loading, error, refetch: fetch2 };
5101
5181
  }
5182
+ function usePlayerRewards(address) {
5183
+ const budokan = useBudokanClient();
5184
+ const denshokan = react$1.useDenshokanClient();
5185
+ const budokanAddress = budokan.clientConfig.budokanAddress;
5186
+ const [rewards, setRewards] = react.useState(null);
5187
+ const [aggregating, setAggregating] = react.useState(false);
5188
+ const [error, setError] = react.useState(null);
5189
+ useResetOnClient(budokan, setRewards, setError);
5190
+ const tokensEnabled = !!address && !!budokanAddress;
5191
+ const { data: tokensResult, isLoading: tokensLoading } = react$1.useTokens(
5192
+ tokensEnabled ? {
5193
+ owner: address,
5194
+ minterAddress: budokanAddress,
5195
+ limit: 1e3
5196
+ } : void 0
5197
+ );
5198
+ const tokensByTournament = react.useMemo(() => {
5199
+ if (!tokensResult?.data) return null;
5200
+ const map = /* @__PURE__ */ new Map();
5201
+ for (const t of tokensResult.data) {
5202
+ if (t.contextId == null || !t.tokenId) continue;
5203
+ const tid = String(t.contextId);
5204
+ let list = map.get(tid);
5205
+ if (!list) {
5206
+ list = [];
5207
+ map.set(tid, list);
5208
+ }
5209
+ list.push(t.tokenId);
5210
+ }
5211
+ return map;
5212
+ }, [tokensResult]);
5213
+ const tournamentIds = react.useMemo(
5214
+ () => tokensByTournament ? Array.from(tokensByTournament.keys()) : [],
5215
+ [tokensByTournament]
5216
+ );
5217
+ const { tournaments: tournamentsPage, loading: tournamentsLoading } = useTournaments(
5218
+ tournamentIds.length > 0 ? { tournamentIds, limit: 1e3 } : void 0
5219
+ );
5220
+ const tournamentIdsKey = tournamentIds.join(",");
5221
+ const fetch2 = react.useCallback(async () => {
5222
+ if (!tokensEnabled) {
5223
+ setRewards(null);
5224
+ return;
5225
+ }
5226
+ if (!tokensByTournament) return;
5227
+ if (tokensByTournament.size === 0) {
5228
+ setRewards(emptyRewards());
5229
+ return;
5230
+ }
5231
+ if (!tournamentsPage?.data) return;
5232
+ const now = Math.floor(Date.now() / 1e3);
5233
+ const finalized = tournamentsPage.data.filter((t) => {
5234
+ const sub = Number(t.submissionEndTime ?? 0);
5235
+ return sub > 0 && sub <= now;
5236
+ });
5237
+ if (finalized.length === 0) {
5238
+ setRewards(emptyRewards());
5239
+ return;
5240
+ }
5241
+ setAggregating(true);
5242
+ setError(null);
5243
+ try {
5244
+ const settled = await Promise.allSettled(
5245
+ finalized.map(async (t) => {
5246
+ const tid = t.id;
5247
+ const tokenIds = tokensByTournament.get(tid) ?? [];
5248
+ if (tokenIds.length === 0) return null;
5249
+ const [prizes, allClaims, ranksResult] = await Promise.all([
5250
+ budokan.getTournamentPrizes(tid),
5251
+ fetchAllRewardClaims(budokan, tid),
5252
+ denshokan.getTokenRanks(tokenIds, {
5253
+ contextId: Number(tid),
5254
+ minterAddress: budokanAddress
5255
+ })
5256
+ ]);
5257
+ let maxPaid = 0;
5258
+ for (const p of prizes) {
5259
+ if ((p.payoutPosition ?? 0) > 0) {
5260
+ maxPaid = Math.max(maxPaid, p.payoutPosition);
5261
+ }
5262
+ if ((p.distributionCount ?? 0) > 0) {
5263
+ maxPaid = Math.max(maxPaid, p.distributionCount);
5264
+ }
5265
+ }
5266
+ const efDistCount = Number(t.entryFee?.distributionCount ?? 0);
5267
+ if (efDistCount > 0) maxPaid = Math.max(maxPaid, efDistCount);
5268
+ if (maxPaid === 0) return null;
5269
+ const placements = ranksResult.data.filter((r) => r.rank > 0 && r.rank <= maxPaid).map((r) => ({
5270
+ tournamentId: tid,
5271
+ tokenId: r.tokenId,
5272
+ position: r.rank,
5273
+ score: String(r.score ?? "0")
5274
+ }));
5275
+ if (placements.length === 0) return null;
5276
+ return {
5277
+ tournament: t,
5278
+ prizes,
5279
+ rewardClaims: allClaims,
5280
+ placements
5281
+ };
5282
+ })
5283
+ );
5284
+ const valid = settled.map((s, i) => {
5285
+ if (s.status === "fulfilled") return s.value;
5286
+ console.warn(
5287
+ `usePlayerRewards: tournament ${finalized[i].id} fetch failed; skipping`,
5288
+ s.reason
5289
+ );
5290
+ return null;
5291
+ }).filter((r) => r !== null);
5292
+ const allPlacements = valid.flatMap(
5293
+ (r) => r.placements
5294
+ );
5295
+ const wins = allPlacements.length;
5296
+ const bestPlacement = wins > 0 ? Math.min(...allPlacements.map((p) => p.position)) : null;
5297
+ const tournamentsList = valid.map((r) => r.tournament);
5298
+ const prizesList = valid.flatMap((r) => r.prizes);
5299
+ const rewardClaimsList = valid.flatMap(
5300
+ (r) => r.rewardClaims
5301
+ );
5302
+ setRewards({
5303
+ wins,
5304
+ bestPlacement,
5305
+ placements: allPlacements,
5306
+ tournaments: tournamentsList,
5307
+ prizes: prizesList,
5308
+ rewardClaims: rewardClaimsList
5309
+ });
5310
+ } catch (e) {
5311
+ setError(e);
5312
+ } finally {
5313
+ setAggregating(false);
5314
+ }
5315
+ }, [
5316
+ tokensEnabled,
5317
+ tokensByTournament,
5318
+ tournamentsPage,
5319
+ tournamentIdsKey,
5320
+ budokan,
5321
+ denshokan,
5322
+ budokanAddress
5323
+ ]);
5324
+ const lastRunRef = react.useRef("");
5325
+ react.useEffect(() => {
5326
+ if (!tokensEnabled) {
5327
+ setRewards(null);
5328
+ return;
5329
+ }
5330
+ if (!tokensByTournament || !tournamentsPage?.data) return;
5331
+ const fingerprint = `${address}|${tournamentIdsKey}|${tournamentsPage.data.length}`;
5332
+ if (fingerprint === lastRunRef.current) return;
5333
+ lastRunRef.current = fingerprint;
5334
+ fetch2();
5335
+ }, [
5336
+ address,
5337
+ tokensEnabled,
5338
+ tokensByTournament,
5339
+ tournamentsPage,
5340
+ tournamentIdsKey,
5341
+ fetch2
5342
+ ]);
5343
+ const loading = tokensEnabled && (tokensLoading || tournamentsLoading) || aggregating;
5344
+ return { rewards, loading, error, refetch: fetch2 };
5345
+ }
5346
+ function emptyRewards() {
5347
+ return {
5348
+ wins: 0,
5349
+ bestPlacement: null,
5350
+ placements: [],
5351
+ tournaments: [],
5352
+ prizes: [],
5353
+ rewardClaims: []
5354
+ };
5355
+ }
5356
+ async function fetchAllRewardClaims(client, tournamentId) {
5357
+ const first = await client.getTournamentRewardClaims(tournamentId, {
5358
+ limit: 100
5359
+ });
5360
+ const accumulated = [...first.data];
5361
+ const total = first.total ?? accumulated.length;
5362
+ if (accumulated.length >= total) return accumulated;
5363
+ const pageSize = Math.max(first.data.length, 1);
5364
+ let offset = accumulated.length;
5365
+ while (offset < total) {
5366
+ const next = await client.getTournamentRewardClaims(tournamentId, {
5367
+ limit: pageSize,
5368
+ offset
5369
+ });
5370
+ if (next.data.length === 0) break;
5371
+ accumulated.push(...next.data);
5372
+ offset += next.data.length;
5373
+ }
5374
+ return accumulated;
5375
+ }
5102
5376
  function useSubscription(channels, tournamentIds) {
5103
5377
  const client = useBudokanClient();
5104
5378
  const [lastMessage, setLastMessage] = react.useState(null);
@@ -5149,6 +5423,7 @@ exports.useActivityStats = useActivityStats;
5149
5423
  exports.useBudokanClient = useBudokanClient;
5150
5424
  exports.useConnectionStatus = useConnectionStatus;
5151
5425
  exports.useLeaderboard = useLeaderboard;
5426
+ exports.usePlayerRewards = usePlayerRewards;
5152
5427
  exports.usePrizeAggregation = usePrizeAggregation;
5153
5428
  exports.usePrizeStats = usePrizeStats;
5154
5429
  exports.usePrizes = usePrizes;