@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/extensions-BskkhEHk.d.cts +101 -0
- package/dist/extensions-BskkhEHk.d.ts +101 -0
- package/dist/index.cjs +1071 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +489 -0
- package/dist/index.d.ts +489 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +1 -0
- package/dist/prizeAggregation-CHwIJzXr.d.cts +325 -0
- package/dist/prizeAggregation-CHwIJzXr.d.ts +325 -0
- package/dist/react.cjs +918 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +300 -0
- package/dist/react.d.ts +300 -0
- package/dist/react.js +906 -0
- package/dist/react.js.map +1 -0
- package/dist/rpc.cjs +161 -0
- package/dist/rpc.cjs.map +1 -0
- package/dist/rpc.d.cts +48 -0
- package/dist/rpc.d.ts +48 -0
- package/dist/rpc.js +146 -0
- package/dist/rpc.js.map +1 -0
- package/package.json +10 -10
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
|