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