@provable-games/metagame-sdk 0.1.0 → 0.1.2

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.
package/dist/react.cjs ADDED
@@ -0,0 +1,918 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var starknet = require('starknet');
5
+
6
+ // src/react/useDebounce.ts
7
+ function useDebounce(value, delay) {
8
+ const [debouncedValue, setDebouncedValue] = react.useState(value);
9
+ react.useEffect(() => {
10
+ const handler = setTimeout(() => {
11
+ setDebouncedValue(value);
12
+ }, delay);
13
+ return () => {
14
+ clearTimeout(handler);
15
+ };
16
+ }, [value, delay]);
17
+ return debouncedValue;
18
+ }
19
+ function usePagination({
20
+ totalItems,
21
+ pageSize = 10
22
+ }) {
23
+ const [currentPage, setCurrentPage] = react.useState(0);
24
+ const totalPages = react.useMemo(
25
+ () => Math.max(1, Math.ceil(totalItems / pageSize)),
26
+ [totalItems, pageSize]
27
+ );
28
+ const hasNext = currentPage < totalPages - 1;
29
+ const hasPrev = currentPage > 0;
30
+ const next = react.useCallback(() => {
31
+ setCurrentPage((p) => Math.min(p + 1, totalPages - 1));
32
+ }, [totalPages]);
33
+ const prev = react.useCallback(() => {
34
+ setCurrentPage((p) => Math.max(p - 1, 0));
35
+ }, []);
36
+ const reset = react.useCallback(() => {
37
+ setCurrentPage(0);
38
+ }, []);
39
+ const startIndex = currentPage * pageSize;
40
+ const endIndex = Math.min(startIndex + pageSize, totalItems);
41
+ return {
42
+ currentPage,
43
+ totalPages,
44
+ hasNext,
45
+ hasPrev,
46
+ next,
47
+ prev,
48
+ reset,
49
+ startIndex,
50
+ endIndex
51
+ };
52
+ }
53
+ function useSearchFilter({
54
+ items,
55
+ searchFields,
56
+ debounceMs = 300
57
+ }) {
58
+ const [search, setSearch] = react.useState("");
59
+ const debouncedSearch = useDebounce(search, debounceMs);
60
+ const filteredItems = react.useMemo(() => {
61
+ if (!debouncedSearch) return items;
62
+ const lower = debouncedSearch.toLowerCase();
63
+ return items.filter(
64
+ (item) => searchFields.some((field) => {
65
+ const value = item[field];
66
+ if (typeof value === "string") {
67
+ return value.toLowerCase().includes(lower);
68
+ }
69
+ if (typeof value === "number") {
70
+ return String(value).includes(lower);
71
+ }
72
+ return false;
73
+ })
74
+ );
75
+ }, [items, debouncedSearch, searchFields]);
76
+ return {
77
+ search,
78
+ setSearch,
79
+ filteredItems,
80
+ resultCount: filteredItems.length
81
+ };
82
+ }
83
+ function useTokenSelector({
84
+ tokens,
85
+ tokenType,
86
+ pageSize = 10,
87
+ debounceMs = 300
88
+ }) {
89
+ const [search, setSearchRaw] = react.useState("");
90
+ const [selectedToken, setSelectedToken] = react.useState(null);
91
+ const debouncedSearch = useDebounce(search, debounceMs);
92
+ const allFiltered = react.useMemo(() => {
93
+ let result = tokens;
94
+ if (tokenType) {
95
+ result = result.filter((t) => t.tokenType === tokenType);
96
+ }
97
+ if (debouncedSearch) {
98
+ const lower = debouncedSearch.toLowerCase();
99
+ result = result.filter(
100
+ (t) => t.name.toLowerCase().includes(lower) || t.symbol.toLowerCase().includes(lower) || t.address.toLowerCase().includes(lower)
101
+ );
102
+ }
103
+ return result;
104
+ }, [tokens, tokenType, debouncedSearch]);
105
+ const pagination = usePagination({
106
+ totalItems: allFiltered.length,
107
+ pageSize
108
+ });
109
+ const filteredTokens = react.useMemo(
110
+ () => allFiltered.slice(pagination.startIndex, pagination.endIndex),
111
+ [allFiltered, pagination.startIndex, pagination.endIndex]
112
+ );
113
+ const setSearch = react.useCallback(
114
+ (q) => {
115
+ setSearchRaw(q);
116
+ pagination.reset();
117
+ },
118
+ [pagination]
119
+ );
120
+ const select = react.useCallback((token) => {
121
+ setSelectedToken(token);
122
+ }, []);
123
+ const clear = react.useCallback(() => {
124
+ setSelectedToken(null);
125
+ }, []);
126
+ const isSelected = react.useCallback(
127
+ (token) => selectedToken?.address === token.address,
128
+ [selectedToken]
129
+ );
130
+ const getTokenProps = react.useCallback(
131
+ (token) => ({
132
+ onClick: () => select(token),
133
+ "data-selected": isSelected(token),
134
+ role: "option",
135
+ "aria-selected": isSelected(token)
136
+ }),
137
+ [select, isSelected]
138
+ );
139
+ return {
140
+ search,
141
+ setSearch,
142
+ filteredTokens,
143
+ selectedToken,
144
+ select,
145
+ clear,
146
+ isSelected,
147
+ pagination,
148
+ getTokenProps
149
+ };
150
+ }
151
+
152
+ // src/utils/status.ts
153
+ function computeStatus(timestamps, now) {
154
+ const currentTimestamp = now ?? Math.floor(Date.now() / 1e3);
155
+ if (timestamps.unlocked === false) {
156
+ return {
157
+ label: "Locked",
158
+ variant: "locked",
159
+ isActive: false,
160
+ countdown: null
161
+ };
162
+ }
163
+ if (timestamps.completed) {
164
+ return {
165
+ label: "Completed",
166
+ variant: "completed",
167
+ isActive: false,
168
+ countdown: null
169
+ };
170
+ }
171
+ const { registrationStart, registrationEnd, start, end, submissionEnd } = timestamps;
172
+ if (registrationStart && registrationEnd) {
173
+ if (currentTimestamp < registrationStart) {
174
+ return {
175
+ label: "Upcoming",
176
+ variant: "upcoming",
177
+ isActive: false,
178
+ countdown: {
179
+ targetTimestamp: registrationStart,
180
+ label: "Registration"
181
+ }
182
+ };
183
+ }
184
+ if (currentTimestamp >= registrationStart && currentTimestamp < registrationEnd) {
185
+ return {
186
+ label: "Registration",
187
+ variant: "registration",
188
+ isActive: true,
189
+ countdown: { targetTimestamp: registrationEnd, label: "Closes" }
190
+ };
191
+ }
192
+ }
193
+ if (currentTimestamp < start) {
194
+ return {
195
+ label: "Upcoming",
196
+ variant: "upcoming",
197
+ isActive: false,
198
+ countdown: { targetTimestamp: start, label: "Starts" }
199
+ };
200
+ }
201
+ if (currentTimestamp >= start && currentTimestamp < end) {
202
+ return {
203
+ label: "Live",
204
+ variant: "active",
205
+ isActive: true,
206
+ countdown: { targetTimestamp: end, label: "Ends" }
207
+ };
208
+ }
209
+ if (submissionEnd && currentTimestamp >= end && currentTimestamp < submissionEnd) {
210
+ return {
211
+ label: "Submission",
212
+ variant: "submission",
213
+ isActive: true,
214
+ countdown: { targetTimestamp: submissionEnd, label: "Submit" }
215
+ };
216
+ }
217
+ return {
218
+ label: "Ended",
219
+ variant: "ended",
220
+ isActive: false,
221
+ countdown: null
222
+ };
223
+ }
224
+
225
+ // src/react/useStatusIndicator.ts
226
+ function useStatusIndicator(timestamps, refreshMs = 1e3) {
227
+ const [now, setNow] = react.useState(() => Math.floor(Date.now() / 1e3));
228
+ react.useEffect(() => {
229
+ const interval = setInterval(() => {
230
+ setNow(Math.floor(Date.now() / 1e3));
231
+ }, refreshMs);
232
+ return () => clearInterval(interval);
233
+ }, [refreshMs]);
234
+ return computeStatus(timestamps, now);
235
+ }
236
+ function useScoreTable({
237
+ participants,
238
+ pageSize = 10,
239
+ sortField = "rank",
240
+ sortDirection = "asc"
241
+ }) {
242
+ const [sort, setSortState] = react.useState({ field: sortField, direction: sortDirection });
243
+ const [search, setSearchRaw] = react.useState("");
244
+ const debouncedSearch = useDebounce(search, 300);
245
+ const filtered = react.useMemo(() => {
246
+ if (!debouncedSearch) return participants;
247
+ const lower = debouncedSearch.toLowerCase();
248
+ return participants.filter(
249
+ (p) => p.address.toLowerCase().includes(lower) || p.name && p.name.toLowerCase().includes(lower)
250
+ );
251
+ }, [participants, debouncedSearch]);
252
+ const sorted = react.useMemo(() => {
253
+ const arr = [...filtered];
254
+ const dir = sort.direction === "asc" ? 1 : -1;
255
+ arr.sort((a, b) => {
256
+ const aVal = a[sort.field] ?? 0;
257
+ const bVal = b[sort.field] ?? 0;
258
+ if (typeof aVal === "string" && typeof bVal === "string") {
259
+ return dir * aVal.localeCompare(bVal);
260
+ }
261
+ return dir * (Number(aVal) - Number(bVal));
262
+ });
263
+ return arr;
264
+ }, [filtered, sort]);
265
+ const pagination = usePagination({
266
+ totalItems: sorted.length,
267
+ pageSize
268
+ });
269
+ const rows = react.useMemo(
270
+ () => sorted.slice(pagination.startIndex, pagination.endIndex),
271
+ [sorted, pagination.startIndex, pagination.endIndex]
272
+ );
273
+ const setSort = react.useCallback(
274
+ (field) => {
275
+ const validField = field;
276
+ setSortState((prev) => {
277
+ if (prev.field === validField) {
278
+ return {
279
+ field: validField,
280
+ direction: prev.direction === "asc" ? "desc" : "asc"
281
+ };
282
+ }
283
+ return { field: validField, direction: "asc" };
284
+ });
285
+ },
286
+ []
287
+ );
288
+ const setSearch = react.useCallback(
289
+ (q) => {
290
+ setSearchRaw(q);
291
+ pagination.reset();
292
+ },
293
+ [pagination]
294
+ );
295
+ return {
296
+ rows,
297
+ pagination,
298
+ sort,
299
+ setSort,
300
+ search,
301
+ setSearch
302
+ };
303
+ }
304
+
305
+ // src/utils/prizeAggregation.ts
306
+ function aggregatePrizesByPosition(prizes) {
307
+ const positionMap = /* @__PURE__ */ new Map();
308
+ for (const prize of prizes) {
309
+ if (!positionMap.has(prize.position)) {
310
+ positionMap.set(prize.position, {
311
+ erc20Map: /* @__PURE__ */ new Map(),
312
+ erc721Map: /* @__PURE__ */ new Map()
313
+ });
314
+ }
315
+ const group = positionMap.get(prize.position);
316
+ if (prize.tokenType === "erc20") {
317
+ const current = group.erc20Map.get(prize.tokenAddress) ?? 0n;
318
+ group.erc20Map.set(prize.tokenAddress, current + BigInt(prize.amount));
319
+ } else {
320
+ const ids = group.erc721Map.get(prize.tokenAddress) ?? [];
321
+ ids.push(prize.id);
322
+ group.erc721Map.set(prize.tokenAddress, ids);
323
+ }
324
+ }
325
+ return Array.from(positionMap.entries()).sort(([a], [b]) => a - b).map(([position, { erc20Map, erc721Map }]) => ({
326
+ position,
327
+ erc20: Array.from(erc20Map.entries()).map(([tokenAddress, totalAmount]) => ({
328
+ tokenAddress,
329
+ totalAmount
330
+ })),
331
+ erc721: Array.from(erc721Map.entries()).map(([tokenAddress, tokenIds]) => ({
332
+ tokenAddress,
333
+ tokenIds
334
+ }))
335
+ }));
336
+ }
337
+
338
+ // src/react/usePrizeTable.ts
339
+ function usePrizeTable({
340
+ prizes,
341
+ positionsPerPage = 5
342
+ }) {
343
+ const grouped = react.useMemo(() => aggregatePrizesByPosition(prizes), [prizes]);
344
+ const pagination = usePagination({
345
+ totalItems: grouped.length,
346
+ pageSize: positionsPerPage
347
+ });
348
+ const rows = react.useMemo(
349
+ () => grouped.slice(pagination.startIndex, pagination.endIndex),
350
+ [grouped, pagination.startIndex, pagination.endIndex]
351
+ );
352
+ return {
353
+ rows,
354
+ totalPositions: grouped.length,
355
+ pagination
356
+ };
357
+ }
358
+
359
+ // src/utils/entryFee.ts
360
+ function calculateEntryFeeBreakdown(feePerEntry, entryCount, shares) {
361
+ const totalCollected = feePerEntry * BigInt(entryCount);
362
+ if (totalCollected === 0n) {
363
+ return {
364
+ totalCollected: 0n,
365
+ creatorAmount: 0n,
366
+ gameCreatorAmount: 0n,
367
+ refundAmount: 0n,
368
+ prizePoolAmount: 0n
369
+ };
370
+ }
371
+ const creatorAmount = shares.creatorShare > 0 ? totalCollected * BigInt(shares.creatorShare) / 10000n : 0n;
372
+ const gameCreatorAmount = shares.gameCreatorShare > 0 ? totalCollected * BigInt(shares.gameCreatorShare) / 10000n : 0n;
373
+ const refundAmount = shares.refundShare > 0 ? totalCollected * BigInt(shares.refundShare) / 10000n : 0n;
374
+ const prizePoolBps = 1e4 - shares.creatorShare - shares.gameCreatorShare - shares.refundShare;
375
+ const prizePoolAmount = prizePoolBps > 0 ? totalCollected * BigInt(prizePoolBps) / 10000n : 0n;
376
+ return {
377
+ totalCollected,
378
+ creatorAmount,
379
+ gameCreatorAmount,
380
+ refundAmount,
381
+ prizePoolAmount
382
+ };
383
+ }
384
+ function distributePool(prizePoolAmount, percentages) {
385
+ return percentages.map((pct, index) => ({
386
+ position: index + 1,
387
+ amount: prizePoolAmount * BigInt(Math.floor(pct * 100)) / 10000n
388
+ })).filter((entry) => entry.amount > 0n);
389
+ }
390
+
391
+ // src/utils/formatting.ts
392
+ function calculateDistribution(positions, weight, creatorFee, gameFee, refundShare, distributionType = "exponential") {
393
+ if (positions <= 0) {
394
+ return [];
395
+ }
396
+ let rawDistributions = [];
397
+ if (distributionType === "uniform") {
398
+ rawDistributions = Array(positions).fill(1);
399
+ } else if (distributionType === "linear") {
400
+ for (let i = 0; i < positions; i++) {
401
+ const positionValue = positions - i;
402
+ const share = 1 + (positionValue - 1) * (weight / 10);
403
+ rawDistributions.push(share);
404
+ }
405
+ } else {
406
+ for (let i = 0; i < positions; i++) {
407
+ const share = Math.pow(1 - i / positions, weight);
408
+ rawDistributions.push(share);
409
+ }
410
+ }
411
+ const total = rawDistributions.reduce((a, b) => a + b, 0);
412
+ if (total === 0) {
413
+ return Array(positions).fill(0);
414
+ }
415
+ const basisPointShares = rawDistributions.map((d) => {
416
+ const ratio = d / total;
417
+ const basisPoints = ratio * 1e4;
418
+ return Math.floor(basisPoints);
419
+ });
420
+ const totalBasisPoints = basisPointShares.reduce((a, b) => a + b, 0);
421
+ const remainingBasisPoints = 1e4 - totalBasisPoints;
422
+ if (remainingBasisPoints !== 0 && positions > 0) {
423
+ basisPointShares[0] = basisPointShares[0] + remainingBasisPoints;
424
+ }
425
+ return basisPointShares.map((bp) => bp / 100);
426
+ }
427
+
428
+ // src/react/useEntryFeePreview.ts
429
+ function useEntryFeePreview({
430
+ feePerEntry,
431
+ entryCount,
432
+ shares,
433
+ distributionPositions,
434
+ distributionType,
435
+ distributionWeight
436
+ }) {
437
+ const breakdown = react.useMemo(
438
+ () => calculateEntryFeeBreakdown(feePerEntry, entryCount, shares),
439
+ [feePerEntry, entryCount, shares.creatorShare, shares.gameCreatorShare, shares.refundShare]
440
+ );
441
+ const percentages = react.useMemo(
442
+ () => distributionPositions > 0 ? calculateDistribution(
443
+ distributionPositions,
444
+ distributionWeight,
445
+ 0,
446
+ 0,
447
+ 0,
448
+ distributionType
449
+ ) : [],
450
+ [distributionPositions, distributionWeight, distributionType]
451
+ );
452
+ const positionPrizes = react.useMemo(
453
+ () => distributePool(breakdown.prizePoolAmount, percentages),
454
+ [breakdown.prizePoolAmount, percentages]
455
+ );
456
+ return { breakdown, positionPrizes, percentages };
457
+ }
458
+
459
+ // src/utils/qualification.ts
460
+ function evaluateTokenQualification(input) {
461
+ if (input.ownedTokenIds.length === 0) {
462
+ return {
463
+ meetsRequirements: false,
464
+ bestProof: null,
465
+ qualifications: [],
466
+ totalEntriesLeft: 0
467
+ };
468
+ }
469
+ const qualifications = [];
470
+ for (const tokenId of input.ownedTokenIds) {
471
+ const usedCount = input.usedEntryCounts[tokenId] ?? 0;
472
+ const remaining = input.entryLimit > 0 ? input.entryLimit - usedCount : Infinity;
473
+ if (remaining > 0) {
474
+ qualifications.push({
475
+ id: `token-${tokenId}`,
476
+ entriesLeft: remaining,
477
+ proof: { type: "token", tokenId },
478
+ label: `Token #${tokenId}`,
479
+ metadata: { tokenId }
480
+ });
481
+ }
482
+ }
483
+ const totalEntriesLeft = qualifications.reduce(
484
+ (sum, q) => sum + (q.entriesLeft === Infinity ? 1 : q.entriesLeft),
485
+ 0
486
+ );
487
+ const best = qualifications.length > 0 ? qualifications.reduce(
488
+ (a, b) => b.entriesLeft > a.entriesLeft ? b : a
489
+ ) : null;
490
+ return {
491
+ meetsRequirements: qualifications.length > 0,
492
+ bestProof: best?.proof ?? null,
493
+ qualifications,
494
+ totalEntriesLeft
495
+ };
496
+ }
497
+ function evaluateExtensionQualification(input) {
498
+ const qualifications = input.checkedQualifications.filter((q) => q.entriesLeft > 0).map((q) => ({
499
+ id: q.id,
500
+ entriesLeft: q.entriesLeft,
501
+ proof: {
502
+ type: "extension",
503
+ extensionProof: q.proof
504
+ },
505
+ label: q.label,
506
+ metadata: q.metadata
507
+ }));
508
+ const totalEntriesLeft = qualifications.reduce(
509
+ (sum, q) => sum + q.entriesLeft,
510
+ 0
511
+ );
512
+ const best = qualifications.length > 0 ? qualifications.reduce(
513
+ (a, b) => b.entriesLeft > a.entriesLeft ? b : a
514
+ ) : null;
515
+ return {
516
+ meetsRequirements: qualifications.length > 0,
517
+ bestProof: best?.proof ?? null,
518
+ qualifications,
519
+ totalEntriesLeft
520
+ };
521
+ }
522
+
523
+ // src/react/useEntryQualification.ts
524
+ function useEntryQualification(options) {
525
+ const {
526
+ variant,
527
+ ownedTokenIds = [],
528
+ tokenEntryCounts = {},
529
+ playerAddress,
530
+ extensionQualifications = [],
531
+ tournamentValidatorConfig,
532
+ extensionValidEntry = false,
533
+ extensionEntriesLeft = null,
534
+ entryLimit = 0
535
+ } = options;
536
+ return react.useMemo(() => {
537
+ if (variant === "none") {
538
+ return {
539
+ meetsRequirements: true,
540
+ bestProof: null,
541
+ qualifications: [],
542
+ totalEntriesLeft: Infinity,
543
+ entriesLeftBySource: [{ entriesLeft: Infinity }]
544
+ };
545
+ }
546
+ if (variant === "token") {
547
+ const result = evaluateTokenQualification({
548
+ ownedTokenIds,
549
+ usedEntryCounts: tokenEntryCounts,
550
+ entryLimit
551
+ });
552
+ return {
553
+ meetsRequirements: result.meetsRequirements,
554
+ bestProof: result.bestProof,
555
+ qualifications: result.qualifications,
556
+ totalEntriesLeft: result.totalEntriesLeft,
557
+ entriesLeftBySource: result.meetsRequirements ? [{ sourceType: "token", entriesLeft: result.totalEntriesLeft }] : []
558
+ };
559
+ }
560
+ if (variant === "extension") {
561
+ if (!playerAddress) {
562
+ return empty();
563
+ }
564
+ if (tournamentValidatorConfig && extensionQualifications.length > 0) {
565
+ const result = evaluateExtensionQualification({
566
+ checkedQualifications: extensionQualifications
567
+ });
568
+ const entriesBySource = /* @__PURE__ */ new Map();
569
+ for (const qual of extensionQualifications) {
570
+ if (qual.entriesLeft <= 0) continue;
571
+ const sourceId = qual.metadata?.tournamentId ?? qual.id;
572
+ const current = entriesBySource.get(sourceId) ?? 0;
573
+ entriesBySource.set(sourceId, current + qual.entriesLeft);
574
+ }
575
+ const qualifyingMode = tournamentValidatorConfig.qualifyingMode;
576
+ let meetsRequirements;
577
+ if (qualifyingMode === 0 || qualifyingMode === 1 || qualifyingMode === 3) {
578
+ meetsRequirements = result.meetsRequirements;
579
+ } else {
580
+ meetsRequirements = entriesBySource.size >= tournamentValidatorConfig.tournamentIds.length;
581
+ }
582
+ return {
583
+ meetsRequirements,
584
+ bestProof: result.bestProof,
585
+ qualifications: result.qualifications,
586
+ totalEntriesLeft: result.totalEntriesLeft,
587
+ entriesLeftBySource: Array.from(entriesBySource.entries()).map(
588
+ ([sourceId, entriesLeft]) => ({
589
+ sourceId,
590
+ sourceType: "tournament",
591
+ entriesLeft
592
+ })
593
+ )
594
+ };
595
+ }
596
+ const remaining = extensionEntriesLeft !== null ? extensionEntriesLeft : Infinity;
597
+ if (extensionValidEntry && remaining > 0) {
598
+ return {
599
+ meetsRequirements: true,
600
+ bestProof: { type: "extension", extensionProof: [] },
601
+ qualifications: [],
602
+ totalEntriesLeft: remaining,
603
+ entriesLeftBySource: [
604
+ { sourceType: "extension", entriesLeft: remaining }
605
+ ]
606
+ };
607
+ }
608
+ return empty();
609
+ }
610
+ return empty();
611
+ }, [
612
+ variant,
613
+ ownedTokenIds,
614
+ tokenEntryCounts,
615
+ playerAddress,
616
+ extensionQualifications,
617
+ tournamentValidatorConfig,
618
+ extensionValidEntry,
619
+ extensionEntriesLeft,
620
+ entryLimit
621
+ ]);
622
+ }
623
+ function empty() {
624
+ return {
625
+ meetsRequirements: false,
626
+ bestProof: null,
627
+ qualifications: [],
628
+ totalEntriesLeft: 0,
629
+ entriesLeftBySource: []
630
+ };
631
+ }
632
+ async function getExtensionEntriesLeft(provider, extensionAddress, contextId, playerAddress, qualification) {
633
+ try {
634
+ const result = await provider.callContract({
635
+ contractAddress: extensionAddress,
636
+ entrypoint: "entries_left",
637
+ calldata: starknet.CallData.compile([contextId, playerAddress, qualification])
638
+ });
639
+ if (result[0] === "0x1" || BigInt(result[0]) === 1n) {
640
+ return null;
641
+ }
642
+ return Number(result[1]);
643
+ } catch (error) {
644
+ console.error("Error checking extension entries_left:", error);
645
+ return null;
646
+ }
647
+ }
648
+ var OPUS_ABBOT_ADDRESS = "0x04d0bb0a4c40012384e7c419e6eb3c637b28e8363fb66958b60d90505b9c072f";
649
+ var OPUS_SHRINE_ADDRESS = "0x0498edfaf50ca5855666a700c25dd629d577eb9afccdf3b5977aec79aee55ada";
650
+ async function getUserTroveIds(provider, userAddress, abbotAddress = OPUS_ABBOT_ADDRESS) {
651
+ try {
652
+ const result = await provider.callContract({
653
+ contractAddress: abbotAddress,
654
+ entrypoint: "get_user_trove_ids",
655
+ calldata: starknet.CallData.compile([userAddress])
656
+ });
657
+ const arrayLength = Number(result[0] || 0);
658
+ const troveIds = [];
659
+ for (let i = 1; i <= arrayLength; i++) {
660
+ troveIds.push(BigInt(result[i] || 0));
661
+ }
662
+ return troveIds;
663
+ } catch (error) {
664
+ console.error("Error getting user trove IDs:", error);
665
+ return [];
666
+ }
667
+ }
668
+ async function getTroveDebt(provider, troveId, shrineAddress = OPUS_SHRINE_ADDRESS) {
669
+ try {
670
+ const result = await provider.callContract({
671
+ contractAddress: shrineAddress,
672
+ entrypoint: "get_trove_health",
673
+ calldata: starknet.CallData.compile([troveId])
674
+ });
675
+ return BigInt(result[3] || 0);
676
+ } catch (error) {
677
+ console.error("Error getting trove health:", error);
678
+ return null;
679
+ }
680
+ }
681
+ async function getUserTotalTroveDebt(provider, userAddress, abbotAddress, shrineAddress) {
682
+ const troveIds = await getUserTroveIds(provider, userAddress, abbotAddress);
683
+ if (troveIds.length === 0) return 0n;
684
+ let totalDebt = 0n;
685
+ for (const troveId of troveIds) {
686
+ const debt = await getTroveDebt(provider, troveId, shrineAddress);
687
+ if (debt !== null) totalDebt += debt;
688
+ }
689
+ return totalDebt;
690
+ }
691
+
692
+ // src/react/useExtensionQualification.ts
693
+ var useExtensionQualification = (provider, extensionAddress, contextId, playerAddress, qualificationInputs, enabled = true) => {
694
+ const [qualifications, setQualifications] = react.useState([]);
695
+ const [loading, setLoading] = react.useState(false);
696
+ const [error, setError] = react.useState(null);
697
+ const inputsKey = react.useMemo(() => {
698
+ if (!enabled || !provider || !extensionAddress || !contextId || !playerAddress) {
699
+ return "";
700
+ }
701
+ return JSON.stringify({
702
+ extensionAddress,
703
+ contextId,
704
+ playerAddress,
705
+ inputs: qualificationInputs.map((i) => ({
706
+ tid: i.tournamentId,
707
+ token: i.tokenId,
708
+ pos: i.position
709
+ }))
710
+ });
711
+ }, [enabled, provider, extensionAddress, contextId, playerAddress, qualificationInputs]);
712
+ const lastFetchedKey = react.useRef("");
713
+ react.useEffect(() => {
714
+ if (!enabled) {
715
+ lastFetchedKey.current = "";
716
+ setQualifications([]);
717
+ setLoading(false);
718
+ setError(null);
719
+ }
720
+ }, [enabled]);
721
+ react.useEffect(() => {
722
+ if (!inputsKey) return;
723
+ if (inputsKey === lastFetchedKey.current) return;
724
+ const checkQualifications = async () => {
725
+ lastFetchedKey.current = inputsKey;
726
+ setLoading(true);
727
+ setError(null);
728
+ try {
729
+ const results = await Promise.all(
730
+ qualificationInputs.map(async (input) => {
731
+ try {
732
+ const proof = [
733
+ input.tournamentId,
734
+ input.tokenId,
735
+ input.position.toString()
736
+ ];
737
+ const entriesLeft = await getExtensionEntriesLeft(
738
+ provider,
739
+ extensionAddress,
740
+ contextId,
741
+ playerAddress,
742
+ proof
743
+ );
744
+ if (entriesLeft !== null && entriesLeft > 0) {
745
+ return {
746
+ id: `${input.tournamentId}-${input.tokenId}-${input.position}`,
747
+ proof,
748
+ entriesLeft,
749
+ label: input.tournamentName,
750
+ metadata: {
751
+ tournamentId: input.tournamentId,
752
+ tournamentName: input.tournamentName,
753
+ tokenId: input.tokenId,
754
+ position: input.position
755
+ }
756
+ };
757
+ }
758
+ return null;
759
+ } catch (err) {
760
+ console.error(
761
+ `Error checking qualification for ${input.tournamentId}:`,
762
+ err
763
+ );
764
+ return null;
765
+ }
766
+ })
767
+ );
768
+ const validQualifications = results.filter(
769
+ (q) => q !== null
770
+ );
771
+ setQualifications(validQualifications);
772
+ } catch (err) {
773
+ console.error("Error checking extension qualifications:", err);
774
+ setError(err instanceof Error ? err : new Error(String(err)));
775
+ setQualifications([]);
776
+ } finally {
777
+ setLoading(false);
778
+ }
779
+ };
780
+ checkQualifications();
781
+ }, [inputsKey, provider, extensionAddress, contextId, playerAddress, qualificationInputs]);
782
+ const totalEntriesLeft = qualifications.reduce(
783
+ (sum, q) => sum + q.entriesLeft,
784
+ 0
785
+ );
786
+ const bestQualification = qualifications.length > 0 ? qualifications.reduce(
787
+ (best, current) => current.entriesLeft > best.entriesLeft ? current : best
788
+ ) : null;
789
+ return {
790
+ qualifications,
791
+ totalEntriesLeft,
792
+ bestQualification,
793
+ loading,
794
+ error
795
+ };
796
+ };
797
+
798
+ // src/utils/opusTroves.ts
799
+ function calculateOpusTrovesEntries(debt, config) {
800
+ let allowed = 0;
801
+ if (config.valuePerEntry > 0n) {
802
+ if (debt > config.threshold) {
803
+ allowed = Number((debt - config.threshold) / config.valuePerEntry);
804
+ }
805
+ } else {
806
+ if (debt >= config.threshold && config.maxEntries > 0) {
807
+ allowed = config.maxEntries;
808
+ }
809
+ }
810
+ if (config.maxEntries > 0) {
811
+ allowed = Math.min(allowed, config.maxEntries);
812
+ }
813
+ return allowed;
814
+ }
815
+ function findBannableEntries(registeredTokenIds, entriesAllowed) {
816
+ const bannable = /* @__PURE__ */ new Set();
817
+ const bannableCount = Math.max(0, registeredTokenIds.length - entriesAllowed);
818
+ if (bannableCount > 0) {
819
+ const sorted = [...registeredTokenIds].sort((a, b) => {
820
+ return Number(BigInt(a)) - Number(BigInt(b));
821
+ });
822
+ for (let i = 0; i < bannableCount; i++) {
823
+ bannable.add(sorted[i]);
824
+ }
825
+ }
826
+ return bannable;
827
+ }
828
+ function findAllBannableEntries(players, config) {
829
+ const allBannable = /* @__PURE__ */ new Set();
830
+ for (const [, { debt, registeredTokenIds }] of players) {
831
+ const allowed = calculateOpusTrovesEntries(debt, config);
832
+ const bannable = findBannableEntries(registeredTokenIds, allowed);
833
+ for (const id of bannable) {
834
+ allBannable.add(id);
835
+ }
836
+ }
837
+ return allBannable;
838
+ }
839
+
840
+ // src/react/useOpusTrovesBannableEntries.ts
841
+ var useOpusTrovesBannableEntries = (provider, games, config, enabled) => {
842
+ const [troveDebts, setTroveDebts] = react.useState(/* @__PURE__ */ new Map());
843
+ const [isLoading, setIsLoading] = react.useState(false);
844
+ const playerGroups = react.useMemo(() => {
845
+ const groups = /* @__PURE__ */ new Map();
846
+ games.forEach((game) => {
847
+ const owner = game?.owner;
848
+ if (!owner) return;
849
+ if (!groups.has(owner)) groups.set(owner, []);
850
+ groups.get(owner).push(game);
851
+ });
852
+ return groups;
853
+ }, [games]);
854
+ const playerAddresses = react.useMemo(
855
+ () => Array.from(playerGroups.keys()),
856
+ [playerGroups]
857
+ );
858
+ react.useEffect(() => {
859
+ if (!enabled || !provider || !config || playerAddresses.length === 0) {
860
+ setTroveDebts(/* @__PURE__ */ new Map());
861
+ return;
862
+ }
863
+ let cancelled = false;
864
+ setIsLoading(true);
865
+ const fetchDebts = async () => {
866
+ const debts = /* @__PURE__ */ new Map();
867
+ await Promise.all(
868
+ playerAddresses.map(async (addr) => {
869
+ try {
870
+ const debt = await getUserTotalTroveDebt(provider, addr);
871
+ debts.set(addr, debt);
872
+ } catch {
873
+ debts.set(addr, 0n);
874
+ }
875
+ })
876
+ );
877
+ if (!cancelled) {
878
+ setTroveDebts(debts);
879
+ setIsLoading(false);
880
+ }
881
+ };
882
+ fetchDebts();
883
+ return () => {
884
+ cancelled = true;
885
+ };
886
+ }, [enabled, provider, config, playerAddresses]);
887
+ const bannableEntries = react.useMemo(() => {
888
+ if (!enabled || isLoading || !config) return /* @__PURE__ */ new Set();
889
+ const players = /* @__PURE__ */ new Map();
890
+ for (const [owner, ownerGames] of playerGroups.entries()) {
891
+ players.set(owner, {
892
+ debt: troveDebts.get(owner) ?? 0n,
893
+ registeredTokenIds: ownerGames.map((g) => g.tokenId.toString())
894
+ });
895
+ }
896
+ return findAllBannableEntries(players, config);
897
+ }, [enabled, isLoading, playerGroups, troveDebts, config]);
898
+ return {
899
+ bannableEntries,
900
+ troveDebts,
901
+ isLoading,
902
+ playerGroups
903
+ };
904
+ };
905
+
906
+ exports.useDebounce = useDebounce;
907
+ exports.useEntryFeePreview = useEntryFeePreview;
908
+ exports.useEntryQualification = useEntryQualification;
909
+ exports.useExtensionQualification = useExtensionQualification;
910
+ exports.useOpusTrovesBannableEntries = useOpusTrovesBannableEntries;
911
+ exports.usePagination = usePagination;
912
+ exports.usePrizeTable = usePrizeTable;
913
+ exports.useScoreTable = useScoreTable;
914
+ exports.useSearchFilter = useSearchFilter;
915
+ exports.useStatusIndicator = useStatusIndicator;
916
+ exports.useTokenSelector = useTokenSelector;
917
+ //# sourceMappingURL=react.cjs.map
918
+ //# sourceMappingURL=react.cjs.map