@stackedapp/utils 1.23.2 → 1.23.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1746 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.meetsCompletionConditionsBeforeExpiry = exports.meetsCompletionConditions = exports.offerMeetsCompletionConditions = exports.meetsLinkedEntityOffersCondition = exports.hasCompletionConditions = exports.meetsBaseConditions = exports.DEFAULT_ENTITY_KIND = void 0;
4
- exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
5
- exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
6
- exports.meetsDynamicConditions = meetsDynamicConditions;
7
- exports.meetsClaimableConditions = meetsClaimableConditions;
8
- exports.aggregateTokenBalances = aggregateTokenBalances;
9
- const template_1 = require("./template");
10
- const dynamic_1 = require("./dynamic");
11
- const blockchain_utils_1 = require("./blockchain_utils");
12
- exports.DEFAULT_ENTITY_KIND = '_default';
13
- const calculatePercent = (amount, goal, isMet) => {
14
- if (goal && goal > 0 && amount !== undefined) {
15
- return Math.min(100, (amount / goal) * 100);
16
- }
17
- return isMet ? 100 : 0;
18
- };
19
- const formatList = (items) => items.length === 2 ? items.join(' and ') : `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`;
20
- const calculateDynamicConditionPercent = (dynamicObj, cond) => {
21
- if (!dynamicObj)
22
- return 0;
23
- if (cond.operator === 'is_truthy')
24
- return dynamicObj[cond.key] ? 100 : 0;
25
- if (cond.operator === 'is_falsy')
26
- return !dynamicObj[cond.key] ? 100 : 0;
27
- if (cond.operator === 'is_defined')
28
- return dynamicObj[cond.key] != null ? 100 : 0;
29
- const val = dynamicObj[cond.key];
30
- if (typeof val !== 'number')
31
- return 0;
32
- const compareTo = typeof cond.compareTo === 'number' ? cond.compareTo : 0;
33
- switch (cond.operator) {
34
- case '>=':
35
- case '>':
36
- return compareTo > 0 ? Math.min(100, (val / compareTo) * 100) : val >= compareTo ? 100 : 0;
37
- case '==':
38
- return val === compareTo ? 100 : 0;
39
- case '!=':
40
- return val !== compareTo ? 100 : 0;
41
- case '<=':
42
- case '<':
43
- return val <= compareTo ? 100 : 0; // Binary for max-style
44
- default:
45
- return 0;
46
- }
47
- };
48
- const calculateDynamicGroupPercent = (dynamicObj, group) => {
49
- if (!group?.conditions?.length)
50
- return 100;
51
- const percentages = group.conditions.map((c) => calculateDynamicConditionPercent(dynamicObj, c));
52
- if (!group.links?.length) {
53
- return percentages.reduce((sum, p) => sum + p, 0) / percentages.length;
54
- }
55
- let result = percentages[0];
56
- for (let i = 0; i < group.links.length; i++) {
57
- const nextPct = percentages[i + 1];
58
- switch (group.links[i]) {
59
- case 'AND':
60
- result = (result + nextPct) / 2;
61
- break;
62
- case 'OR':
63
- result = Math.max(result, nextPct);
64
- break;
65
- case 'AND NOT':
66
- result = (result + (100 - nextPct)) / 2;
67
- break;
68
- }
69
- }
70
- return result;
71
- };
72
- const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
73
- /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
74
- * check doesn't use this since we don't have a playerOffer at surfacing time
75
- */
76
- playerOffer,
77
- /** Additional data like fetched token balances that isn't part of playerSnap */
78
- additionalData, }) => {
79
- const conditionData = [];
80
- let isValid = true;
81
- if (conditions?.minDaysInGame) {
82
- const trackerAmount = playerSnap.daysInGame || 0;
83
- const trackerGoal = conditions.minDaysInGame;
84
- const isDisqualify = trackerAmount < trackerGoal;
85
- if (addDetails) {
86
- conditionData.push({
87
- isMet: !isDisqualify,
88
- kind: 'minDaysInGame',
89
- trackerAmount,
90
- trackerGoal,
91
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
92
- text: `More than ${trackerGoal} Days in Game`,
93
- });
94
- if (isDisqualify)
95
- isValid = false;
96
- }
97
- else {
98
- if (isDisqualify)
99
- return { isValid: false, isComplete: false, percentCompleted: 0 };
100
- }
101
- }
102
- if (conditions?.minTrustScore) {
103
- const trackerAmount = playerSnap.trustScore || 0;
104
- const trackerGoal = conditions.minTrustScore;
105
- const isDisqualify = trackerAmount < trackerGoal;
106
- if (addDetails) {
107
- conditionData.push({
108
- isMet: !isDisqualify,
109
- kind: 'minTrustScore',
110
- trackerAmount,
111
- trackerGoal,
112
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
113
- text: `More than ${trackerGoal} Rep`,
114
- });
115
- if (isDisqualify)
116
- isValid = false;
117
- }
118
- else {
119
- if (isDisqualify)
120
- return { isValid: false, isComplete: false, percentCompleted: 0 };
121
- }
122
- }
123
- if (conditions?.maxTrustScore) {
124
- const trackerAmount = playerSnap.trustScore || 0;
125
- const isDisqualify = trackerAmount > conditions.maxTrustScore;
126
- if (addDetails) {
127
- conditionData.push({
128
- isMet: !isDisqualify,
129
- kind: 'maxTrustScore',
130
- trackerAmount,
131
- trackerGoal: conditions.maxTrustScore,
132
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
133
- text: `Less than ${conditions.maxTrustScore} Rep`,
134
- });
135
- if (isDisqualify)
136
- isValid = false;
137
- }
138
- else {
139
- if (isDisqualify)
140
- return { isValid: false, isComplete: false, percentCompleted: 0 };
141
- }
142
- }
143
- for (const key in conditions?.achievements) {
144
- const a = conditions.achievements[key];
145
- const playerAchData = playerSnap.achievements?.[key];
146
- if (!playerAchData) {
147
- const isDisqualify = true;
148
- if (addDetails) {
149
- conditionData.push({
150
- isMet: !isDisqualify,
151
- kind: 'achievements',
152
- trackerAmount: 0,
153
- trackerGoal: 1,
154
- percentCompleted: 0,
155
- text: `Have the achievement ${a.name}`,
156
- });
157
- if (isDisqualify)
158
- isValid = false;
159
- }
160
- else {
161
- if (isDisqualify)
162
- return { isValid: false, isComplete: false, percentCompleted: 0 };
163
- }
164
- }
165
- if (a.minCount) {
166
- const trackerAmount = playerAchData?.count || 0;
167
- const trackerGoal = a.minCount;
168
- const isDisqualify = trackerAmount < trackerGoal;
169
- if (addDetails) {
170
- conditionData.push({
171
- isMet: !isDisqualify,
172
- kind: 'achievements',
173
- trackerAmount,
174
- trackerGoal,
175
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
176
- text: `Have the achievement ${a.name} more than ${trackerGoal} times`,
177
- });
178
- if (isDisqualify)
179
- isValid = false;
180
- }
181
- else {
182
- if (isDisqualify)
183
- return { isValid: false, isComplete: false, percentCompleted: 0 };
184
- }
185
- }
186
- }
187
- for (const key in conditions?.currencies) {
188
- const c = conditions.currencies[key];
189
- const playerCurrencyData = playerSnap.currencies?.[key];
190
- if (c.min) {
191
- const trackerAmount = playerCurrencyData?.balance || 0;
192
- const trackerGoal = c.min;
193
- const isDisqualify = trackerAmount < trackerGoal;
194
- if (addDetails) {
195
- conditionData.push({
196
- isMet: !isDisqualify,
197
- kind: 'currencies',
198
- trackerAmount,
199
- trackerGoal,
200
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
201
- text: `Have more than ${trackerGoal} ${c.name}`,
202
- });
203
- if (isDisqualify)
204
- isValid = false;
205
- }
206
- else {
207
- if (isDisqualify)
208
- return { isValid: false, isComplete: false, percentCompleted: 0 };
209
- }
210
- }
211
- if (c.max) {
212
- const trackerAmount = playerCurrencyData?.balance || 0;
213
- const isDisqualify = trackerAmount > c.max;
214
- if (addDetails) {
215
- conditionData.push({
216
- isMet: !isDisqualify,
217
- kind: 'currencies',
218
- trackerAmount,
219
- trackerGoal: c.max,
220
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
221
- text: `Have less than ${c.max} ${c.name}`,
222
- });
223
- if (isDisqualify)
224
- isValid = false;
225
- }
226
- else {
227
- if (isDisqualify)
228
- return { isValid: false, isComplete: false, percentCompleted: 0 };
229
- }
230
- }
231
- if (c.in) {
232
- const trackerAmount = playerCurrencyData?.in || 0;
233
- const trackerGoal = c.in;
234
- const isDisqualify = trackerAmount < trackerGoal;
235
- if (addDetails) {
236
- conditionData.push({
237
- isMet: !isDisqualify,
238
- kind: 'currencies',
239
- trackerAmount,
240
- trackerGoal,
241
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
242
- text: `Deposit at least ${trackerGoal} ${c.name}`,
243
- });
244
- if (isDisqualify)
245
- isValid = false;
246
- }
247
- else {
248
- if (isDisqualify)
249
- return { isValid: false, isComplete: false, percentCompleted: 0 };
250
- }
251
- }
252
- if (c.out) {
253
- const trackerAmount = playerCurrencyData?.out || 0;
254
- const trackerGoal = c.out;
255
- const isDisqualify = trackerAmount < trackerGoal;
256
- if (addDetails) {
257
- conditionData.push({
258
- isMet: !isDisqualify,
259
- kind: 'currencies',
260
- trackerAmount,
261
- trackerGoal,
262
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
263
- text: `Withdraw at least ${trackerGoal} ${c.name}`,
264
- });
265
- if (isDisqualify)
266
- isValid = false;
267
- }
268
- else {
269
- if (isDisqualify)
270
- return { isValid: false, isComplete: false, percentCompleted: 0 };
271
- }
272
- }
273
- }
274
- for (const key in conditions?.levels) {
275
- const l = conditions.levels[key];
276
- const playerLevelData = playerSnap.levels?.[key];
277
- if (l.min) {
278
- const trackerAmount = playerLevelData?.level || 0;
279
- const trackerGoal = l.min;
280
- const isDisqualify = trackerAmount < trackerGoal;
281
- if (addDetails) {
282
- conditionData.push({
283
- isMet: !isDisqualify,
284
- kind: 'levels',
285
- trackerAmount,
286
- trackerGoal,
287
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
288
- text: `Be above level ${trackerGoal} ${l.name}`,
289
- });
290
- if (isDisqualify)
291
- isValid = false;
292
- }
293
- else {
294
- if (isDisqualify)
295
- return { isValid: false, isComplete: false, percentCompleted: 0 };
296
- }
297
- }
298
- if (l.max) {
299
- const trackerAmount = playerLevelData?.level || 0;
300
- const isDisqualify = trackerAmount > l.max;
301
- if (addDetails) {
302
- conditionData.push({
303
- isMet: !isDisqualify,
304
- kind: 'levels',
305
- trackerAmount,
306
- trackerGoal: l.max,
307
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
308
- text: `Be under level ${l.max} ${l.name}`,
309
- });
310
- if (isDisqualify)
311
- isValid = false;
312
- }
313
- else {
314
- if (isDisqualify)
315
- return { isValid: false, isComplete: false, percentCompleted: 0 };
316
- }
317
- }
318
- }
319
- if (conditions?.quests) {
320
- for (const questId in conditions.quests) {
321
- const quest = conditions.quests[questId];
322
- const playerQuestData = playerSnap.quests?.[questId];
323
- const trackerAmount = playerQuestData?.completions || 0;
324
- const trackerGoal = quest.completions || 1;
325
- const isDisqualify = playerQuestData ? trackerAmount < (quest.completions || 0) : true;
326
- if (addDetails) {
327
- conditionData.push({
328
- isMet: !isDisqualify,
329
- kind: 'quests',
330
- trackerAmount,
331
- trackerGoal,
332
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
333
- text: quest.completions === 1
334
- ? `Complete the quest ${quest.name}`
335
- : (quest.completions || 0) < 1
336
- ? `Start the quest ${quest.name}`
337
- : `Complete the quest ${quest.name} ${quest.completions} times`,
338
- });
339
- if (isDisqualify)
340
- isValid = false;
341
- }
342
- else {
343
- if (isDisqualify)
344
- return { isValid: false, isComplete: false, percentCompleted: 0 };
345
- }
346
- }
347
- }
348
- for (const key in conditions?.memberships) {
349
- const m = conditions.memberships[key];
350
- const playerMembershipsData = playerSnap.memberships?.[key];
351
- if (m.minCount) {
352
- const trackerAmount = playerMembershipsData?.count || 0;
353
- const trackerGoal = m.minCount;
354
- const isDisqualify = trackerAmount < trackerGoal;
355
- if (addDetails) {
356
- conditionData.push({
357
- isMet: !isDisqualify,
358
- kind: 'memberships',
359
- trackerAmount,
360
- trackerGoal,
361
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
362
- text: trackerGoal > 1 ? `Have at least ${trackerGoal} ${m.name} memberships` : `Have a ${m.name} membership`,
363
- });
364
- if (isDisqualify)
365
- isValid = false;
366
- }
367
- else {
368
- if (isDisqualify)
369
- return { isValid: false, isComplete: false, percentCompleted: 0 };
370
- }
371
- }
372
- if (m.maxCount) {
373
- const trackerAmount = playerMembershipsData?.count || 0;
374
- const isDisqualify = trackerAmount > m.maxCount;
375
- if (addDetails) {
376
- conditionData.push({
377
- isMet: !isDisqualify,
378
- kind: 'memberships',
379
- trackerAmount,
380
- trackerGoal: m.maxCount,
381
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
382
- text: `Have less than ${m.maxCount} ${m.name} memberships`,
383
- });
384
- if (isDisqualify)
385
- isValid = false;
386
- }
387
- else {
388
- if (isDisqualify)
389
- return { isValid: false, isComplete: false, percentCompleted: 0 };
390
- }
391
- }
392
- const timeOwned = (playerMembershipsData?.expiresAt || 0) - Date.now();
393
- if (m.minMs) {
394
- const trackerAmount = Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1));
395
- const trackerGoal = Number((m.minMs / (1000 * 60 * 60 * 24)).toFixed(1));
396
- const isDisqualify = timeOwned < m.minMs;
397
- if (addDetails) {
398
- conditionData.push({
399
- isMet: !isDisqualify,
400
- kind: 'memberships',
401
- trackerAmount,
402
- trackerGoal,
403
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
404
- text: `Own ${m.name} membership for at least ${trackerGoal} days`,
405
- });
406
- if (isDisqualify)
407
- isValid = false;
408
- }
409
- else {
410
- if (isDisqualify)
411
- return { isValid: false, isComplete: false, percentCompleted: 0 };
412
- }
413
- }
414
- if (m.maxMs) {
415
- const trackerAmount = Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1));
416
- const isDisqualify = timeOwned > m.maxMs;
417
- if (addDetails) {
418
- conditionData.push({
419
- isMet: !isDisqualify,
420
- kind: 'memberships',
421
- trackerAmount,
422
- trackerGoal: Number((m.maxMs / (1000 * 60 * 60 * 24)).toFixed(1)),
423
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
424
- text: `Own ${m.name} membership for less than ${(m.maxMs / (1000 * 60 * 60 * 24)).toFixed(1)} days`,
425
- });
426
- if (isDisqualify)
427
- isValid = false;
428
- }
429
- else {
430
- if (isDisqualify)
431
- return { isValid: false, isComplete: false, percentCompleted: 0 };
432
- }
433
- }
434
- }
435
- for (const key in conditions?.stakedTokens) {
436
- const s = conditions.stakedTokens[key];
437
- const playerStakedData = playerSnap.stakedTokens?.[key];
438
- if (s.min) {
439
- const trackerAmount = playerStakedData?.balance || 0;
440
- const trackerGoal = s.min;
441
- const isDisqualify = trackerAmount < trackerGoal;
442
- if (addDetails) {
443
- conditionData.push({
444
- isMet: !isDisqualify,
445
- kind: 'stakedTokens',
446
- trackerAmount,
447
- trackerGoal,
448
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
449
- text: `Have at least ${trackerGoal} ${s.name} staked`,
450
- });
451
- if (isDisqualify)
452
- isValid = false;
453
- }
454
- else {
455
- if (isDisqualify)
456
- return { isValid: false, isComplete: false, percentCompleted: 0 };
457
- }
458
- }
459
- }
460
- // Validate link count conditions
461
- if (conditions?.links) {
462
- for (const [linkType, constraint] of Object.entries(conditions.links)) {
463
- // linkType should always exist. and be default is none was specified
464
- const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || exports.DEFAULT_ENTITY_KIND) === linkType).length || 0;
465
- if (constraint.min !== undefined) {
466
- const trackerAmount = linkCount;
467
- const trackerGoal = constraint.min;
468
- const isDisqualify = trackerAmount < trackerGoal;
469
- if (addDetails) {
470
- conditionData.push({
471
- isMet: !isDisqualify,
472
- kind: 'links',
473
- trackerAmount,
474
- trackerGoal,
475
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
476
- text: constraint.template
477
- ? (0, template_1.renderTemplate)(constraint.template, {
478
- current: linkCount,
479
- min: constraint.min ?? 0,
480
- max: constraint.max ?? 0,
481
- type: linkType,
482
- })
483
- : `At least ${trackerGoal} ${linkType} link(s)`,
484
- });
485
- if (isDisqualify)
486
- isValid = false;
487
- }
488
- else {
489
- if (isDisqualify)
490
- return { isValid: false, isComplete: false, percentCompleted: 0 };
491
- }
492
- }
493
- if (constraint.max !== undefined) {
494
- const trackerAmount = linkCount;
495
- const isDisqualify = trackerAmount > constraint.max;
496
- if (addDetails) {
497
- conditionData.push({
498
- isMet: !isDisqualify,
499
- kind: 'links',
500
- trackerAmount,
501
- trackerGoal: constraint.max,
502
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
503
- text: constraint.template
504
- ? (0, template_1.renderTemplate)(constraint.template, {
505
- current: linkCount,
506
- min: constraint.min ?? 0,
507
- max: constraint.max ?? 0,
508
- type: linkType,
509
- })
510
- : `At most ${constraint.max} ${linkType} link(s)`,
511
- });
512
- if (isDisqualify)
513
- isValid = false;
514
- }
515
- else {
516
- if (isDisqualify)
517
- return { isValid: false, isComplete: false, percentCompleted: 0 };
518
- }
519
- }
520
- }
521
- }
522
- // Evaluate dynamic conditions
523
- if (conditions?.dynamic?.conditions?.length) {
524
- const resolvedConditions = (0, template_1.replaceDynamicConditionKeys)(conditions.dynamic.conditions, playerOffer?.trackers || {});
525
- const dynamicObj = playerSnap.dynamic || {};
526
- const dynamicResult = meetsDynamicConditions(dynamicObj, {
527
- ...conditions.dynamic,
528
- conditions: resolvedConditions,
529
- });
530
- if (addDetails) {
531
- // Extract tracker amount and goal from the first condition for display
532
- let trackerAmount;
533
- let trackerGoal;
534
- if (resolvedConditions.length > 0) {
535
- const firstCond = resolvedConditions[0];
536
- if (typeof firstCond.compareTo === 'number') {
537
- trackerGoal = firstCond.compareTo;
538
- }
539
- const val = dynamicObj[firstCond.key];
540
- trackerAmount = typeof val === 'number' ? val : 0;
541
- }
542
- // Calculate percentage using the dynamic group helper for AND/OR logic
543
- const percentCompleted = calculateDynamicGroupPercent(dynamicObj, {
544
- ...conditions.dynamic,
545
- conditions: resolvedConditions,
546
- });
547
- conditionData.push({
548
- isMet: dynamicResult,
549
- kind: 'dynamic',
550
- trackerAmount,
551
- trackerGoal,
552
- percentCompleted,
553
- text: (0, template_1.renderTemplate)((0, template_1.replaceDynamicConditionKey)(conditions.dynamic.template || '', playerOffer?.trackers || {}), dynamicObj) || 'Dynamic conditions',
554
- });
555
- if (!dynamicResult)
556
- isValid = false;
557
- }
558
- else {
559
- if (!dynamicResult)
560
- return { isValid: false, isComplete: false, percentCompleted: 0 };
561
- }
562
- }
563
- if (conditions?.identifiers?.platforms?.length) {
564
- const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
565
- const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
566
- const platformsToCheck = conditions.identifiers.platforms;
567
- let isMet;
568
- let displayText;
569
- const platformsMap = {
570
- sms: 'SMS',
571
- email: 'Email',
572
- apple: 'Apple',
573
- google: 'Google',
574
- tiktok: 'TikTok',
575
- };
576
- const metAndPlatforms = platformsToCheck.filter((platform) => playerPlatforms.has(platform.toLowerCase()));
577
- if (isAndBehaviour) {
578
- isMet = metAndPlatforms.length === platformsToCheck.length;
579
- displayText =
580
- platformsToCheck.length > 1
581
- ? `Link your ${formatList(platformsToCheck.map((p) => platformsMap[p.toLowerCase()] ?? p))} accounts`
582
- : `Link your ${platformsMap[platformsToCheck[0].toLowerCase()] ?? platformsToCheck[0]} account`;
583
- }
584
- else {
585
- isMet = platformsToCheck.some((platform) => playerPlatforms.has(platform.toLowerCase()));
586
- displayText = `Link any of your ${formatList(platformsToCheck.map((p) => platformsMap[p.toLowerCase()] ?? p))} accounts`;
587
- }
588
- if (addDetails) {
589
- conditionData.push({
590
- isMet,
591
- kind: 'identifiers',
592
- trackerAmount: isMet ? 1 : 0,
593
- trackerGoal: 1,
594
- percentCompleted: isAndBehaviour
595
- ? calculatePercent(metAndPlatforms.length, platformsToCheck.length, isMet)
596
- : isMet
597
- ? 100
598
- : 0,
599
- text: displayText,
600
- });
601
- if (!isMet)
602
- isValid = false;
603
- }
604
- else {
605
- if (!isMet)
606
- return { isValid: false, isComplete: false, percentCompleted: 0 };
607
- }
608
- }
609
- // Evaluate token balance conditions
610
- for (const tokenCond of conditions?.tokenBalances || []) {
611
- const contracts = tokenCond.contracts || [];
612
- let totalBalance = 0;
613
- const fetchedBalances = aggregateTokenBalances(additionalData);
614
- for (const contract of contracts) {
615
- const balanceKey = (0, blockchain_utils_1.addressNetworkId)(contract.contractAddress, contract.network);
616
- totalBalance += fetchedBalances[balanceKey] || 0;
617
- }
618
- if (tokenCond.min !== undefined) {
619
- const trackerAmount = totalBalance;
620
- const trackerGoal = tokenCond.min;
621
- const isDisqualify = trackerAmount < trackerGoal;
622
- if (addDetails) {
623
- conditionData.push({
624
- isMet: !isDisqualify,
625
- kind: 'tokenBalances',
626
- trackerAmount,
627
- trackerGoal,
628
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
629
- text: `Have at least ${trackerGoal} ${tokenCond.name || 'tokens'}`,
630
- });
631
- if (isDisqualify)
632
- isValid = false;
633
- }
634
- else {
635
- if (isDisqualify)
636
- return { isValid: false, isComplete: false, percentCompleted: 0 };
637
- }
638
- }
639
- if (tokenCond.max !== undefined) {
640
- const trackerAmount = totalBalance;
641
- const isDisqualify = trackerAmount > tokenCond.max;
642
- if (addDetails) {
643
- conditionData.push({
644
- isMet: !isDisqualify,
645
- kind: 'tokenBalances',
646
- trackerAmount,
647
- trackerGoal: tokenCond.max,
648
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
649
- text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
650
- });
651
- if (isDisqualify)
652
- isValid = false;
653
- }
654
- else {
655
- if (isDisqualify)
656
- return { isValid: false, isComplete: false, percentCompleted: 0 };
657
- }
658
- }
659
- }
660
- // Evaluate stackedAccount conditions
661
- if (conditions?.stackedAccount) {
662
- const { hasLinkedAccount, cryptoWallets } = conditions.stackedAccount;
663
- // Check if player has linked stacked account
664
- if (hasLinkedAccount !== undefined) {
665
- const playerHasAccount = additionalData?.hasStackedAccount ?? false;
666
- const isDisqualify = hasLinkedAccount !== playerHasAccount;
667
- if (addDetails) {
668
- conditionData.push({
669
- isMet: !isDisqualify,
670
- kind: 'stackedAccount.hasLinkedAccount',
671
- trackerAmount: playerHasAccount ? 1 : 0,
672
- trackerGoal: 1,
673
- percentCompleted: !isDisqualify ? 100 : 0,
674
- text: hasLinkedAccount ? 'Link a Stacked account' : 'Must not have Stacked account linked',
675
- });
676
- if (isDisqualify)
677
- isValid = false;
678
- }
679
- else {
680
- if (isDisqualify)
681
- return { isValid: false, isComplete: false, percentCompleted: 0 };
682
- }
683
- }
684
- // Check crypto wallet count
685
- if (cryptoWallets) {
686
- const walletCount = additionalData?.cryptoWallets?.length || 0;
687
- if (cryptoWallets.min !== undefined) {
688
- const trackerAmount = walletCount;
689
- const trackerGoal = cryptoWallets.min;
690
- const isDisqualify = trackerAmount < trackerGoal;
691
- if (addDetails) {
692
- conditionData.push({
693
- isMet: !isDisqualify,
694
- kind: 'stackedAccount.cryptoWallets',
695
- trackerAmount,
696
- trackerGoal,
697
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
698
- text: `Link at least ${trackerGoal} crypto wallet${trackerGoal === 1 ? '' : 's'}`,
699
- });
700
- if (isDisqualify)
701
- isValid = false;
702
- }
703
- else {
704
- if (isDisqualify)
705
- return { isValid: false, isComplete: false, percentCompleted: 0 };
706
- }
707
- }
708
- if (cryptoWallets.max !== undefined) {
709
- const trackerAmount = walletCount;
710
- const trackerGoal = cryptoWallets.max;
711
- const isDisqualify = trackerAmount > trackerGoal;
712
- if (addDetails) {
713
- conditionData.push({
714
- isMet: !isDisqualify,
715
- kind: 'stackedAccount.cryptoWallets',
716
- trackerAmount,
717
- trackerGoal,
718
- percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
719
- text: `Have at most ${trackerGoal} crypto wallet${trackerGoal === 1 ? '' : 's'}`,
720
- });
721
- if (isDisqualify)
722
- isValid = false;
723
- }
724
- else {
725
- if (isDisqualify)
726
- return { isValid: false, isComplete: false, percentCompleted: 0 };
727
- }
728
- }
729
- }
730
- }
731
- // Evaluate userSettings conditions
732
- if (conditions?.userSettings) {
733
- const { emailNewsletter } = conditions.userSettings;
734
- if (emailNewsletter !== undefined) {
735
- const playerHasNewsletter = additionalData?.emailNewsletter ?? false;
736
- const isDisqualify = emailNewsletter !== playerHasNewsletter;
737
- if (addDetails) {
738
- conditionData.push({
739
- isMet: !isDisqualify,
740
- kind: 'userSettings.emailNewsletter',
741
- trackerAmount: playerHasNewsletter ? 1 : 0,
742
- trackerGoal: 1,
743
- percentCompleted: !isDisqualify ? 100 : 0,
744
- text: emailNewsletter ? 'Subscribe to email newsletter' : 'Must not be subscribed to email newsletter',
745
- });
746
- if (isDisqualify)
747
- isValid = false;
748
- }
749
- else {
750
- if (isDisqualify)
751
- return { isValid: false, isComplete: false, percentCompleted: 0 };
752
- }
753
- }
754
- }
755
- // Calculate top-level percentCompleted as average of all condition percentages
756
- const percentCompleted = conditionData.length > 0
757
- ? conditionData.reduce((sum, c) => sum + c.percentCompleted, 0) / conditionData.length
758
- : isValid
759
- ? 100
760
- : 0;
761
- return {
762
- /** @deprecated Use isComplete instead */
763
- isValid,
764
- isComplete: isValid,
765
- percentCompleted,
766
- conditionData: addDetails ? conditionData : undefined,
767
- };
768
- };
769
- exports.meetsBaseConditions = meetsBaseConditions;
770
- const hasCompletionConditions = (conditions) => {
771
- if (!conditions)
772
- return false;
773
- if (Object.keys(conditions.currencies || {}).length > 0)
774
- return true;
775
- if (Object.keys(conditions.levels || {}).length > 0)
776
- return true;
777
- if (Object.keys(conditions.stakedTokens || {}).length > 0)
778
- return true;
779
- if (Object.keys(conditions.memberships || {}).length > 0)
780
- return true;
781
- if (Object.keys(conditions.quests || {}).length > 0)
782
- return true;
783
- if (conditions.minTrustScore)
784
- return true;
785
- if (conditions.maxTrustScore)
786
- return true;
787
- if (conditions.achievements)
788
- return true;
789
- if (conditions.minDaysInGame)
790
- return true;
791
- if (conditions.dynamic?.conditions?.length)
792
- return true;
793
- if (conditions.identifiers?.platforms?.length)
794
- return true;
795
- const compCond = conditions;
796
- if (compCond.context)
797
- return true;
798
- if (compCond.buyItem)
799
- return true;
800
- if (compCond.spendCurrency)
801
- return true;
802
- if (compCond.depositCurrency)
803
- return true;
804
- if (compCond.social)
805
- return true;
806
- if (compCond.login)
807
- return true;
808
- if (compCond.loginStreak)
809
- return true;
810
- if (compCond.linkedCompletions)
811
- return true;
812
- if (compCond.dynamicTracker?.conditions?.length)
813
- return true;
814
- if (conditions.tokenBalances?.length)
815
- return true;
816
- if (Object.keys(compCond.contractInteractions || {}).length > 0)
817
- return true;
818
- if (conditions.stackedAccount)
819
- return true;
820
- if (conditions.userSettings)
821
- return true;
822
- return false;
823
- };
824
- exports.hasCompletionConditions = hasCompletionConditions;
825
- const meetsLinkedEntityOffersCondition = ({ linkedEntityOffers, matchingLinks, linkedPOfferMap, }) => {
826
- if (!linkedPOfferMap)
827
- return { isValid: false };
828
- const linkedPlayerOffer_ids = [];
829
- for (const link of matchingLinks) {
830
- const key = `${link.playerId}:${linkedEntityOffers.offer_id}`;
831
- const po = linkedPOfferMap.get(key);
832
- if (po) {
833
- linkedPlayerOffer_ids.push(po.instanceId.toString());
834
- }
835
- }
836
- if (linkedPlayerOffer_ids.length > 0) {
837
- return { isValid: true, linkedPlayerOffer_ids };
838
- }
839
- return { isValid: false };
840
- };
841
- exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
842
- const offerMeetsCompletionConditions = (offer, snapshot, additionalData) => {
843
- return (0, exports.meetsCompletionConditions)({
844
- completionConditions: offer.completionConditions || {},
845
- completionTrackers: offer.completionTrackers,
846
- playerSnap: snapshot,
847
- playerOffer: offer,
848
- addDetails: true,
849
- maxClaimCount: offer.maxClaimCount,
850
- additionalData,
851
- });
852
- };
853
- exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;
854
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
855
- if (completionConditions) {
856
- const conditions = completionConditions;
857
- // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
858
- const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
859
- const claimMultiplier = shouldScale ? (playerOffer.trackers?.claimedCount || 0) + 1 : 1;
860
- const conditionData = [];
861
- let isValid = true;
862
- let maxTotalClaimsFromScaling = Infinity;
863
- const updateMax = (limit) => (maxTotalClaimsFromScaling = Math.min(maxTotalClaimsFromScaling, limit));
864
- if (completionConditions?.context?.id) {
865
- const hasTrackedContext = completionTrackers?.context && completionConditions.context.id === completionTrackers.context;
866
- const isDisqualify = !hasTrackedContext;
867
- if (addDetails) {
868
- conditionData.push({
869
- isMet: !isDisqualify,
870
- kind: 'context',
871
- trackerAmount: hasTrackedContext ? 1 : 0,
872
- trackerGoal: 1,
873
- percentCompleted: !isDisqualify ? 100 : 0, // Binary
874
- text: completionConditions.context.name,
875
- });
876
- if (isDisqualify)
877
- isValid = false;
878
- }
879
- else {
880
- if (isDisqualify)
881
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
882
- }
883
- }
884
- if (conditions?.buyItem) {
885
- const baseAmount = conditions.buyItem.amount || 1;
886
- const scaledAmount = baseAmount * claimMultiplier;
887
- const trackerValue = completionTrackers?.buyItem || 0;
888
- const isDisqualify = trackerValue < scaledAmount;
889
- if (shouldScale && baseAmount > 0) {
890
- updateMax(Math.floor(trackerValue / baseAmount));
891
- }
892
- if (addDetails) {
893
- conditionData.push({
894
- isMet: !isDisqualify,
895
- kind: 'buyItem',
896
- trackerAmount: trackerValue,
897
- trackerGoal: scaledAmount,
898
- percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
899
- text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
900
- });
901
- if (isDisqualify)
902
- isValid = false;
903
- }
904
- else {
905
- if (isDisqualify)
906
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
907
- }
908
- }
909
- if (conditions?.spendCurrency) {
910
- const baseAmount = conditions.spendCurrency.amount || 1;
911
- const scaledAmount = baseAmount * claimMultiplier;
912
- const trackerValue = completionTrackers?.spendCurrency || 0;
913
- const isDisqualify = trackerValue < scaledAmount;
914
- if (shouldScale && baseAmount > 0) {
915
- updateMax(Math.floor(trackerValue / baseAmount));
916
- }
917
- if (addDetails) {
918
- conditionData.push({
919
- isMet: !isDisqualify,
920
- kind: 'spendCurrency',
921
- trackerAmount: trackerValue,
922
- trackerGoal: scaledAmount,
923
- percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
924
- text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
925
- });
926
- if (isDisqualify)
927
- isValid = false;
928
- }
929
- else {
930
- if (isDisqualify)
931
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
932
- }
933
- }
934
- if (conditions?.depositCurrency) {
935
- const baseAmount = conditions.depositCurrency.amount || 1;
936
- const scaledAmount = baseAmount * claimMultiplier;
937
- const trackerValue = completionTrackers?.depositCurrency || 0;
938
- const isDisqualify = trackerValue < scaledAmount;
939
- if (shouldScale && baseAmount > 0) {
940
- updateMax(Math.floor(trackerValue / baseAmount));
941
- }
942
- if (addDetails) {
943
- conditionData.push({
944
- isMet: !isDisqualify,
945
- kind: 'depositCurrency',
946
- trackerAmount: trackerValue,
947
- trackerGoal: scaledAmount,
948
- percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
949
- text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
950
- });
951
- if (isDisqualify)
952
- isValid = false;
953
- }
954
- else {
955
- if (isDisqualify)
956
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
957
- }
958
- }
959
- if (conditions?.login) {
960
- const isMet = new Date(playerSnap.snapshotLastUpdated || 0).getTime() > new Date(playerOffer.createdAt || 0).getTime();
961
- if (addDetails) {
962
- conditionData.push({
963
- isMet,
964
- kind: 'login',
965
- trackerAmount: isMet ? 1 : 0,
966
- trackerGoal: 1,
967
- percentCompleted: isMet ? 100 : 0, // Binary
968
- text: `Login to the game`,
969
- });
970
- if (!isMet)
971
- isValid = false;
972
- }
973
- else {
974
- if (!isMet)
975
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
976
- }
977
- }
978
- if (conditions?.loginStreak) {
979
- // player's login streak snapshot right now - their login streak when offer was surfaced = their login streak since the offer was surfaced
980
- // if their login streak since the offer was surfaced is less than the required login streak, then they are not yet eligible for the offer
981
- const trackerAmount = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0) + 1;
982
- const trackerGoal = conditions.loginStreak;
983
- const isDisqualify = trackerAmount < trackerGoal;
984
- if (addDetails) {
985
- conditionData.push({
986
- isMet: !isDisqualify,
987
- kind: 'loginStreak',
988
- trackerAmount,
989
- trackerGoal,
990
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
991
- text: `Login streak of ${trackerGoal || 0} days`,
992
- });
993
- if (isDisqualify)
994
- isValid = false;
995
- }
996
- else {
997
- if (isDisqualify)
998
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
999
- }
1000
- }
1001
- if (conditions?.social) {
1002
- const tSocialAccumulate = completionTrackers?.social;
1003
- const tSocialAttach = completionTrackers?.social;
1004
- const cSocial = completionConditions.social;
1005
- const mode = cSocial?.mode || 'attach';
1006
- const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
1007
- const hasContent = Boolean(mode === 'accumulate' ? tSocialAccumulate?.matchCount > 0 : tSocialAttach?.videoId);
1008
- // Only scale social metrics in accumulate mode (attach mode is single content)
1009
- const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
1010
- const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
1011
- const minViews = (cSocial?.minViews || 0) * socialMultiplier;
1012
- const minComments = (cSocial?.minComments || 0) * socialMultiplier;
1013
- const likes = tSocial?.likes || 0;
1014
- const views = tSocial?.views || 0;
1015
- const comments = tSocial?.comments || 0;
1016
- let isDisqualify = !hasContent;
1017
- if (likes < minLikes || views < minViews || comments < minComments) {
1018
- isDisqualify = true;
1019
- }
1020
- if (shouldScale && mode === 'accumulate' && hasContent) {
1021
- const baseLikes = cSocial?.minLikes || 0;
1022
- const baseViews = cSocial?.minViews || 0;
1023
- const baseComments = cSocial?.minComments || 0;
1024
- if (baseLikes > 0)
1025
- updateMax(Math.floor(likes / baseLikes));
1026
- if (baseViews > 0)
1027
- updateMax(Math.floor(views / baseViews));
1028
- if (baseComments > 0)
1029
- updateMax(Math.floor(comments / baseComments));
1030
- }
1031
- // Calculate social percentage - average of all applicable metrics
1032
- const socialPercentages = [];
1033
- if (minLikes > 0)
1034
- socialPercentages.push(calculatePercent(likes, minLikes, likes >= minLikes));
1035
- if (minViews > 0)
1036
- socialPercentages.push(calculatePercent(views, minViews, views >= minViews));
1037
- if (minComments > 0)
1038
- socialPercentages.push(calculatePercent(comments, minComments, comments >= minComments));
1039
- if (!hasContent)
1040
- socialPercentages.push(0);
1041
- const socialPercent = socialPercentages.length > 0
1042
- ? socialPercentages.reduce((a, b) => a + b, 0) / socialPercentages.length
1043
- : hasContent
1044
- ? 100
1045
- : 0;
1046
- if (addDetails) {
1047
- const platformMap = {
1048
- tiktok: 'TikTok',
1049
- instagram: 'Instagram',
1050
- youtube: 'YouTube',
1051
- };
1052
- const platformText = conditions.social.platforms.map((platform) => platformMap[platform]).join(' | ');
1053
- const requiredWords = cSocial?.requiredWords ?? [];
1054
- if (mode === 'accumulate') {
1055
- const matchCount = tSocialAccumulate?.matchCount || 0;
1056
- conditionData.push({
1057
- isMet: hasContent && !isDisqualify,
1058
- kind: 'social',
1059
- trackerAmount: matchCount,
1060
- trackerGoal: 1,
1061
- percentCompleted: socialPercent,
1062
- text: hasContent
1063
- ? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
1064
- : requiredWords.length > 0
1065
- ? `Post ${platformText} content with ${requiredWords.map((w) => `"${w}"`).join(', ')}`
1066
- : `Post ${platformText} content`,
1067
- });
1068
- }
1069
- else {
1070
- const title = tSocialAttach?.title;
1071
- conditionData.push({
1072
- isMet: hasContent && !isDisqualify,
1073
- kind: 'social',
1074
- trackerAmount: hasContent ? 1 : 0,
1075
- trackerGoal: 1,
1076
- percentCompleted: socialPercent,
1077
- text: !hasContent
1078
- ? requiredWords.length > 0
1079
- ? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
1080
- : `Attach a ${platformText} post`
1081
- : `Attached: ${title}`,
1082
- });
1083
- }
1084
- if (minLikes > 0) {
1085
- conditionData.push({
1086
- isMet: hasContent && likes >= minLikes,
1087
- kind: 'social',
1088
- trackerAmount: likes,
1089
- trackerGoal: minLikes,
1090
- percentCompleted: calculatePercent(likes, minLikes, hasContent && likes >= minLikes),
1091
- text: mode === 'accumulate' ? `Combined ${minLikes} Likes` : `Reach ${minLikes} Likes`,
1092
- });
1093
- }
1094
- if (minViews > 0) {
1095
- conditionData.push({
1096
- isMet: hasContent && views >= minViews,
1097
- kind: 'social',
1098
- trackerAmount: views,
1099
- trackerGoal: minViews,
1100
- percentCompleted: calculatePercent(views, minViews, hasContent && views >= minViews),
1101
- text: mode === 'accumulate' ? `Combined ${minViews} Views` : `Reach ${minViews} Views`,
1102
- });
1103
- }
1104
- if (minComments > 0) {
1105
- conditionData.push({
1106
- isMet: hasContent && comments >= minComments,
1107
- kind: 'social',
1108
- trackerAmount: comments,
1109
- trackerGoal: minComments,
1110
- percentCompleted: calculatePercent(comments, minComments, hasContent && comments >= minComments),
1111
- text: mode === 'accumulate' ? `Combined ${minComments} Comments` : `Reach ${minComments} Comments`,
1112
- });
1113
- }
1114
- if (isDisqualify)
1115
- isValid = false;
1116
- }
1117
- else {
1118
- if (isDisqualify)
1119
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
1120
- }
1121
- }
1122
- // Linked completions - wait for N linked entities to complete
1123
- if (conditions?.linkedCompletions?.min) {
1124
- const baseMin = conditions.linkedCompletions.min;
1125
- const trackerAmount = completionTrackers?.linkedCompletions || 0;
1126
- const trackerGoal = baseMin * claimMultiplier;
1127
- const isDisqualify = trackerAmount < trackerGoal;
1128
- if (shouldScale && baseMin > 0) {
1129
- updateMax(Math.floor(trackerAmount / baseMin));
1130
- }
1131
- if (addDetails) {
1132
- conditionData.push({
1133
- isMet: !isDisqualify,
1134
- kind: 'linkedCompletions',
1135
- trackerAmount,
1136
- trackerGoal,
1137
- percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
1138
- text: conditions.linkedCompletions.template
1139
- ? (0, template_1.renderTemplate)(conditions.linkedCompletions.template, {
1140
- current: trackerAmount,
1141
- required: trackerGoal,
1142
- })
1143
- : `Wait for ${trackerGoal} linked ${trackerGoal === 1 ? 'entity' : 'entities'} to complete`,
1144
- });
1145
- if (isDisqualify)
1146
- isValid = false;
1147
- }
1148
- else {
1149
- if (isDisqualify)
1150
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
1151
- }
1152
- }
1153
- if (conditions?.dynamicTracker?.conditions?.length) {
1154
- const resolvedConditions = (0, template_1.replaceDynamicConditionKeys)(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
1155
- // Convert { key: { value: X } } to { key: X } format
1156
- const primitiveTrackers = (0, dynamic_1.dynamicTrackerToPrimitive)(completionTrackers?.dynamicTracker || {});
1157
- const dynamicResult = meetsDynamicConditions(primitiveTrackers, {
1158
- ...conditions.dynamicTracker,
1159
- conditions: resolvedConditions,
1160
- }, claimMultiplier);
1161
- if (shouldScale) {
1162
- const dynamicMax = getMaxClaimsForDynamicGroup(primitiveTrackers, {
1163
- ...conditions.dynamicTracker,
1164
- conditions: resolvedConditions,
1165
- }, playerOffer?.trackers?.claimedCount || 0);
1166
- updateMax(dynamicMax);
1167
- }
1168
- if (addDetails) {
1169
- // Extract tracker amount and goal from the first condition for display
1170
- let trackerAmount;
1171
- let trackerGoal;
1172
- if (resolvedConditions.length > 0) {
1173
- const firstCond = resolvedConditions[0];
1174
- if (typeof firstCond.compareTo === 'number') {
1175
- trackerGoal = firstCond.compareTo;
1176
- }
1177
- const val = primitiveTrackers[firstCond.key];
1178
- trackerAmount = typeof val === 'number' ? val : 0;
1179
- }
1180
- // Calculate percentage using the dynamic group helper for AND/OR logic
1181
- const percentCompleted = calculateDynamicGroupPercent(primitiveTrackers, {
1182
- ...conditions.dynamicTracker,
1183
- conditions: resolvedConditions,
1184
- });
1185
- conditionData.push({
1186
- isMet: dynamicResult,
1187
- kind: 'dynamicTracker',
1188
- trackerAmount,
1189
- trackerGoal,
1190
- percentCompleted,
1191
- text: (0, template_1.renderTemplate)(
1192
- // First resolve {{}} placeholders (e.g., {{surfacerPlayerId}}) using offer trackers
1193
- (0, template_1.replaceDynamicConditionKey)(conditions.dynamicTracker.template || '', playerOffer?.trackers || {}), primitiveTrackers) || 'Dynamic conditions',
1194
- });
1195
- if (!dynamicResult)
1196
- isValid = false;
1197
- }
1198
- else {
1199
- if (!dynamicResult)
1200
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
1201
- }
1202
- }
1203
- // Evaluate contractInteractions completion trackers
1204
- if (conditions?.contractInteractions) {
1205
- const interactions = conditions.contractInteractions;
1206
- const entries = Array.isArray(interactions)
1207
- ? interactions.map((c) => [c.id || '', c])
1208
- : Object.entries(interactions);
1209
- for (const [conditionId, condition] of entries) {
1210
- // Tracker now stores { amount: number, count: number }
1211
- // Default to { amount: 0, count: 0 } if missing
1212
- const rawTracker = completionTrackers?.contractInteractions?.[conditionId];
1213
- const trackerData = rawTracker || { amount: 0, count: 0 };
1214
- const trackerAmount = trackerData.amount || 0;
1215
- const trackerCount = trackerData.count || 0;
1216
- // Goals
1217
- const minAmount = condition.minAmount ? condition.minAmount * claimMultiplier : 0;
1218
- const maxAmount = condition.maxAmount ? condition.maxAmount * claimMultiplier : undefined;
1219
- const minCount = condition.minCount ? condition.minCount * claimMultiplier : 0;
1220
- const maxCount = condition.maxCount ? condition.maxCount * claimMultiplier : undefined;
1221
- let isDisqualify = false;
1222
- // Check Amounts
1223
- if (shouldScale) {
1224
- // If we have scaling (claimMultiplier), we handle max claims calculation differently
1225
- // For now, assume standard validation
1226
- }
1227
- if (minAmount > 0 && trackerAmount < minAmount)
1228
- isDisqualify = true;
1229
- if (maxAmount !== undefined && trackerAmount > maxAmount)
1230
- isDisqualify = true;
1231
- // Check Counts
1232
- if (minCount > 0 && trackerCount < minCount)
1233
- isDisqualify = true;
1234
- if (maxCount !== undefined && trackerCount > maxCount)
1235
- isDisqualify = true;
1236
- if (shouldScale) {
1237
- // Update max claims based on whatever allows the FEWEST claims
1238
- // e.g. if I have enough Amount for 3 claims but enough Count for 1 claim, I can only make 1 claim.
1239
- const possibleClaimsByAmount = condition.minAmount
1240
- ? Math.floor(trackerAmount / condition.minAmount)
1241
- : Number.MAX_SAFE_INTEGER;
1242
- const possibleClaimsByCount = condition.minCount
1243
- ? Math.floor(trackerCount / condition.minCount)
1244
- : Number.MAX_SAFE_INTEGER;
1245
- const possibleClaims = Math.min(possibleClaimsByAmount, possibleClaimsByCount);
1246
- // Only update if we have at least ANY requirement (Amount or Count)
1247
- // If both minAmount and minCount are 0/undefined, this logic might be weird, but usually one is set.
1248
- if (condition.minAmount || condition.minCount) {
1249
- updateMax(possibleClaims);
1250
- }
1251
- }
1252
- if (addDetails) {
1253
- // Determine primary metric for progress display
1254
- // If minAmount is set, use amount. Else use count.
1255
- let current = trackerAmount;
1256
- let goal = minAmount;
1257
- let percent = 0;
1258
- // If both are set, display percent based on the average of the two
1259
- const hasMin = minAmount > 0 || minCount > 0;
1260
- const hasMax = (maxAmount !== undefined && maxAmount > 0) || (maxCount !== undefined && maxCount > 0);
1261
- if (!hasMin && hasMax) {
1262
- // If only max is set: > 0 progress = 100%, else 0%
1263
- const maxAmountSet = maxAmount !== undefined && maxAmount > 0;
1264
- const maxCountSet = maxCount !== undefined && maxCount > 0;
1265
- let pcts = [];
1266
- if (maxAmountSet) {
1267
- pcts.push(trackerAmount > 0 ? 100 : 0);
1268
- }
1269
- if (maxCountSet) {
1270
- pcts.push(trackerCount > 0 ? 100 : 0);
1271
- }
1272
- if (pcts.length > 0) {
1273
- percent = pcts.reduce((a, b) => a + b, 0) / pcts.length;
1274
- }
1275
- else {
1276
- percent = 0;
1277
- }
1278
- }
1279
- else if (minAmount > 0 && minCount > 0) {
1280
- const pctAmount = calculatePercent(trackerAmount, minAmount, true); // clamp to 100
1281
- const pctCount = calculatePercent(trackerCount, minCount, true);
1282
- percent = (pctAmount + pctCount) / 2;
1283
- }
1284
- else if (minCount > 0) {
1285
- current = trackerCount;
1286
- goal = minCount;
1287
- percent = calculatePercent(current, goal, !isDisqualify);
1288
- }
1289
- else {
1290
- percent = calculatePercent(trackerAmount, minAmount || 0, !isDisqualify);
1291
- }
1292
- const displayText = (0, template_1.renderTemplate)(condition.template, {
1293
- currentAmount: trackerAmount || 0,
1294
- currentCount: trackerCount || 0,
1295
- minAmount: minAmount || 0,
1296
- maxAmount: maxAmount || 0,
1297
- minCount: minCount || 0,
1298
- maxCount: maxCount || 0,
1299
- });
1300
- conditionData.push({
1301
- isMet: !isDisqualify,
1302
- kind: 'contractInteractions',
1303
- trackerAmount,
1304
- percentCompleted: percent,
1305
- text: displayText,
1306
- });
1307
- if (isDisqualify)
1308
- isValid = false;
1309
- }
1310
- else {
1311
- if (isDisqualify)
1312
- return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
1313
- }
1314
- }
1315
- }
1316
- const r = (0, exports.meetsBaseConditions)({
1317
- conditions,
1318
- playerSnap,
1319
- addDetails: true,
1320
- playerOffer,
1321
- additionalData,
1322
- });
1323
- isValid = isValid && r.isValid;
1324
- conditionData.push(...(r.conditionData || []));
1325
- if (maxClaimCount && maxClaimCount > 0) {
1326
- updateMax(maxClaimCount);
1327
- }
1328
- const claimedCount = playerOffer?.trackers?.claimedCount || 0;
1329
- const availableClaimsNow = !isValid
1330
- ? 0
1331
- : maxTotalClaimsFromScaling === Infinity
1332
- ? -1
1333
- : Math.max(0, maxTotalClaimsFromScaling - claimedCount);
1334
- // Calculate top-level percentCompleted as average of all condition percentages
1335
- const percentCompleted = conditionData.length > 0
1336
- ? conditionData.reduce((sum, c) => sum + c.percentCompleted, 0) / conditionData.length
1337
- : isValid
1338
- ? 100
1339
- : 0;
1340
- return {
1341
- /** @deprecated Use isComplete instead */
1342
- isValid,
1343
- isComplete: isValid,
1344
- percentCompleted,
1345
- conditionData,
1346
- availableClaimsNow,
1347
- };
1348
- }
1349
- return { isValid: true, isComplete: true, percentCompleted: 100, conditionData: [], availableClaimsNow: -1 };
1350
- };
1351
- exports.meetsCompletionConditions = meetsCompletionConditions;
1352
- /**
1353
- * Checks if completion conditions were met before a specific expiry time.
1354
- * Returns true if all relevant condition fields were updated before expiryTime.
1355
- *
1356
- * @param completionConditions - The completion conditions to check
1357
- * @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
1358
- * @param playerSnap - The player snapshot with field timestamps
1359
- * @returns true if all conditions were met before expiry, false otherwise
1360
- */
1361
- const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, playerOffer, maxClaimCount, }) => {
1362
- if (!completionConditions)
1363
- return false;
1364
- // Check if there are actually any conditions to evaluate
1365
- if (!(0, exports.hasCompletionConditions)(completionConditions))
1366
- return false;
1367
- // First check if conditions are actually met
1368
- const conditionsMet = (0, exports.meetsCompletionConditions)({
1369
- completionConditions,
1370
- completionTrackers,
1371
- playerOffer,
1372
- playerSnap,
1373
- maxClaimCount,
1374
- });
1375
- if (!conditionsMet.isValid)
1376
- return false;
1377
- if (!playerOffer.expiresAt)
1378
- return true;
1379
- const expiryTime = new Date(playerOffer.expiresAt).getTime();
1380
- const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated ?? new Date()).getTime();
1381
- /**
1382
- * Checks if a field was updated after the expiry time.
1383
- * Returns true if updated AFTER or AT expiry (violates grace period).
1384
- * Returns false if updated BEFORE expiry (allows grace period).
1385
- */
1386
- function wasUpdatedAfterExpiry(data) {
1387
- let lastUpdated;
1388
- if (typeof data === 'object' && data !== null && !(data instanceof Date)) {
1389
- // Object with optional lastUpdated field
1390
- lastUpdated = data.lastUpdated ? new Date(data.lastUpdated).getTime() : lastSnapshotUpdate;
1391
- }
1392
- else if (data instanceof Date) {
1393
- lastUpdated = data.getTime();
1394
- }
1395
- else if (typeof data === 'string' || typeof data === 'number') {
1396
- lastUpdated = new Date(data).getTime();
1397
- }
1398
- else {
1399
- // No data provided, use snapshot timestamp
1400
- lastUpdated = lastSnapshotUpdate;
1401
- }
1402
- return lastUpdated >= expiryTime;
1403
- }
1404
- if (completionConditions.currencies) {
1405
- for (const currencyId in completionConditions.currencies) {
1406
- const currency = playerSnap.currencies?.[currencyId];
1407
- if (!currency)
1408
- continue;
1409
- if (wasUpdatedAfterExpiry(currency))
1410
- return false;
1411
- }
1412
- }
1413
- if (completionConditions.levels) {
1414
- for (const skillId in completionConditions.levels) {
1415
- const level = playerSnap.levels?.[skillId];
1416
- if (!level)
1417
- continue;
1418
- if (wasUpdatedAfterExpiry(level))
1419
- return false;
1420
- }
1421
- }
1422
- if (completionConditions.quests) {
1423
- for (const questId in completionConditions.quests) {
1424
- const quest = playerSnap.quests?.[questId];
1425
- if (!quest)
1426
- continue;
1427
- if (wasUpdatedAfterExpiry(quest))
1428
- return false;
1429
- }
1430
- }
1431
- if (completionConditions.memberships) {
1432
- for (const membershipId in completionConditions.memberships) {
1433
- const membership = playerSnap.memberships?.[membershipId];
1434
- if (!membership)
1435
- continue;
1436
- if (wasUpdatedAfterExpiry(membership))
1437
- return false;
1438
- }
1439
- }
1440
- if (completionConditions.achievements) {
1441
- for (const achievementId in completionConditions.achievements) {
1442
- const achievement = playerSnap.achievements?.[achievementId];
1443
- if (!achievement)
1444
- continue;
1445
- if (wasUpdatedAfterExpiry(achievement))
1446
- return false;
1447
- }
1448
- }
1449
- if (completionConditions.stakedTokens) {
1450
- for (const tokenId in completionConditions.stakedTokens) {
1451
- const stakedToken = playerSnap.stakedTokens?.[tokenId];
1452
- if (!stakedToken)
1453
- continue;
1454
- const lastStakeTime = new Date(stakedToken.lastStake ?? 0).getTime();
1455
- const lastUnstakeTime = new Date(stakedToken.lastUnstake ?? 0).getTime();
1456
- const lastUpdated = Math.max(lastStakeTime, lastUnstakeTime);
1457
- if (lastUpdated >= expiryTime)
1458
- return false;
1459
- }
1460
- }
1461
- if (completionConditions.minTrustScore !== undefined || completionConditions.maxTrustScore !== undefined) {
1462
- if (wasUpdatedAfterExpiry(playerSnap.trustLastUpdated))
1463
- return false;
1464
- }
1465
- if (completionConditions.minDaysInGame !== undefined) {
1466
- if (wasUpdatedAfterExpiry(playerSnap.daysInGameLastUpdated))
1467
- return false;
1468
- }
1469
- if (completionConditions.login || completionConditions.loginStreak) {
1470
- if (wasUpdatedAfterExpiry())
1471
- return false;
1472
- }
1473
- if (completionConditions.dynamic?.conditions?.length) {
1474
- if (wasUpdatedAfterExpiry())
1475
- return false;
1476
- }
1477
- if (completionConditions.identifiers?.platforms?.length) {
1478
- if (wasUpdatedAfterExpiry())
1479
- return false;
1480
- }
1481
- if (completionConditions.social) {
1482
- // Check if social content was attached/validated after expiry
1483
- if (completionTrackers?.social?.lastChecked) {
1484
- if (wasUpdatedAfterExpiry(completionTrackers.social.lastChecked))
1485
- return false;
1486
- }
1487
- }
1488
- // Tracker-based conditions: use completionTrackers.lastUpdated if available.
1489
- // Legacy offers without lastUpdated skip these checks (preserving current grace-period behavior).
1490
- if (completionTrackers?.lastUpdated != null) {
1491
- const trackerUpdatedAfterExpiry = wasUpdatedAfterExpiry(completionTrackers.lastUpdated);
1492
- if (trackerUpdatedAfterExpiry && (completionConditions.buyItem ||
1493
- completionConditions.spendCurrency ||
1494
- completionConditions.depositCurrency ||
1495
- completionConditions.context ||
1496
- completionConditions.linkedCompletions ||
1497
- completionConditions.contractInteractions))
1498
- return false;
1499
- if (completionConditions.dynamicTracker?.conditions?.length) {
1500
- for (const cond of completionConditions.dynamicTracker.conditions) {
1501
- const perKeyTs = completionTrackers.dynamicTracker?.[cond.key]?.lastUpdated;
1502
- if (wasUpdatedAfterExpiry(perKeyTs ?? completionTrackers.lastUpdated))
1503
- return false;
1504
- }
1505
- }
1506
- }
1507
- // All conditions were met before expiry
1508
- return true;
1509
- };
1510
- exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
1511
- /**
1512
- * Checks if a dynamic object meets a set of dynamic field conditions.
1513
- * @param dynamicObj - The object with any key and string or number value.
1514
- * @param conditions - Array of conditions to check.
1515
- * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
1516
- * @returns true if all conditions are met, false otherwise.
1517
- */
1518
- /**
1519
- * Evaluates a single dynamic condition against the dynamic object.
1520
- */
1521
- function evaluateDynamicCondition(dynamicObj, cond, claimMultiplier = 1) {
1522
- if (!dynamicObj)
1523
- return false;
1524
- const val = dynamicObj[cond.key];
1525
- if (cond.operator === 'is_truthy')
1526
- return !!val;
1527
- if (cond.operator === 'is_falsy')
1528
- return !val;
1529
- if (cond.operator === 'is_defined')
1530
- return val != null;
1531
- if (val == undefined)
1532
- return false;
1533
- const isNumber = typeof val === 'number';
1534
- const isBoolean = typeof val === 'boolean';
1535
- if (isBoolean) {
1536
- switch (cond.operator) {
1537
- case '==':
1538
- return val === Boolean(cond.compareTo);
1539
- case '!=':
1540
- return val !== Boolean(cond.compareTo);
1541
- default:
1542
- return false;
1543
- }
1544
- }
1545
- const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
1546
- if (isNumber && typeof compareTo === 'number') {
1547
- const skipMultiplier = cond.operator === '==' || cond.operator === '!=';
1548
- const scaledCompareTo = skipMultiplier ? compareTo : compareTo * claimMultiplier;
1549
- switch (cond.operator) {
1550
- case '==':
1551
- return val === scaledCompareTo;
1552
- case '!=':
1553
- return val !== scaledCompareTo;
1554
- case '>':
1555
- return val > scaledCompareTo;
1556
- case '>=':
1557
- return val >= scaledCompareTo;
1558
- case '<':
1559
- return val < scaledCompareTo;
1560
- case '<=':
1561
- return val <= scaledCompareTo;
1562
- }
1563
- }
1564
- else if (!isNumber && typeof compareTo === 'string') {
1565
- switch (cond.operator) {
1566
- case '==':
1567
- return val === compareTo;
1568
- case '!=':
1569
- return val !== compareTo;
1570
- case 'has':
1571
- return val.includes(compareTo);
1572
- case 'not_has':
1573
- return !val.includes(compareTo);
1574
- }
1575
- }
1576
- return false;
1577
- }
1578
- /**
1579
- * Calculates the maximum number of claims supported by a single dynamic condition.
1580
- */
1581
- function getMaxClaimsForDynamicCondition(dynamicObj, cond) {
1582
- if (!dynamicObj)
1583
- return 0;
1584
- if (cond.operator === 'is_truthy')
1585
- return dynamicObj[cond.key] ? Infinity : 0;
1586
- if (cond.operator === 'is_falsy')
1587
- return !dynamicObj[cond.key] ? Infinity : 0;
1588
- if (cond.operator === 'is_defined')
1589
- return dynamicObj[cond.key] != null ? Infinity : 0;
1590
- const val = dynamicObj[cond.key];
1591
- if (val === undefined)
1592
- return 0;
1593
- if (typeof val === 'number') {
1594
- const base = Number(cond.compareTo);
1595
- if (isNaN(base)) {
1596
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
1597
- }
1598
- switch (cond.operator) {
1599
- case '>=':
1600
- if (base === 0)
1601
- return val >= 0 ? Infinity : 0;
1602
- if (base < 0)
1603
- return val >= base ? Infinity : 0;
1604
- return Math.max(0, Math.floor(val / base));
1605
- case '>':
1606
- if (base === 0)
1607
- return val > 0 ? Infinity : 0;
1608
- if (base < 0)
1609
- return val > base ? Infinity : 0;
1610
- if (val <= 0)
1611
- return 0;
1612
- return Math.max(0, Math.ceil(val / base) - 1);
1613
- case '==':
1614
- return val === base ? Infinity : 0;
1615
- case '!=':
1616
- return val !== base ? Infinity : 0;
1617
- case '<=':
1618
- if (base === 0)
1619
- return val <= 0 ? Infinity : 0;
1620
- if (base > 0)
1621
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
1622
- if (val >= 0)
1623
- return 0;
1624
- return Math.max(0, Math.floor(val / base));
1625
- case '<':
1626
- if (base === 0)
1627
- return val < 0 ? Infinity : 0;
1628
- if (base > 0)
1629
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
1630
- if (val >= 0)
1631
- return 0;
1632
- return Math.max(0, Math.ceil(val / base) - 1);
1633
- }
1634
- }
1635
- // we don't scale the rest, they are always true or always false
1636
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
1637
- }
1638
- /**
1639
- * Calculates the maximum number of claims supported by a group of dynamic conditions.
1640
- */
1641
- function getMaxClaimsForDynamicGroup(dynamicObj, dynamicGroup, currentClaimCount = 0) {
1642
- const { conditions, links } = dynamicGroup;
1643
- if (!conditions || conditions.length === 0)
1644
- return Infinity;
1645
- // AND only
1646
- if (!links || links.length === 0 || links.every((l) => l === 'AND')) {
1647
- let minClaims = Infinity;
1648
- for (const cond of conditions) {
1649
- const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
1650
- if (max === 0)
1651
- return 0;
1652
- minClaims = Math.min(minClaims, max);
1653
- }
1654
- return minClaims;
1655
- }
1656
- // OR only
1657
- if (links.every((l) => l === 'OR')) {
1658
- let maxClaims = 0;
1659
- for (const cond of conditions) {
1660
- const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
1661
- if (max === Infinity)
1662
- return Infinity;
1663
- maxClaims = Math.max(maxClaims, max);
1664
- }
1665
- return maxClaims;
1666
- }
1667
- // mixed:
1668
- const maxIterations = 100;
1669
- for (let n = currentClaimCount + 1; n <= currentClaimCount + maxIterations; n++) {
1670
- if (!meetsDynamicConditions(dynamicObj, dynamicGroup, n)) {
1671
- return n - 1;
1672
- }
1673
- }
1674
- return currentClaimCount + maxIterations;
1675
- }
1676
- /**
1677
- * Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
1678
- * @param dynamicObj - The player's dynamic object with any key and string or number value.
1679
- * @param dynamicGroup - The group of conditions and links to check.
1680
- * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
1681
- * @returns true if the group evaluates to true, false otherwise.
1682
- */
1683
- function meetsDynamicConditions(dynamicObj, dynamicGroup, claimMultiplier = 1) {
1684
- const { conditions, links } = dynamicGroup;
1685
- if (!conditions || conditions.length === 0)
1686
- return true;
1687
- if (!dynamicObj)
1688
- return false;
1689
- // If no links, treat as AND between all conditions
1690
- if (!links || links.length === 0) {
1691
- return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond, claimMultiplier));
1692
- }
1693
- // Evaluate the first condition
1694
- let result = evaluateDynamicCondition(dynamicObj, conditions[0], claimMultiplier);
1695
- for (let i = 0; i < links.length; i++) {
1696
- const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1], claimMultiplier);
1697
- const link = links[i];
1698
- if (link === 'AND') {
1699
- result = result && nextCond;
1700
- }
1701
- else if (link === 'OR') {
1702
- result = result || nextCond;
1703
- }
1704
- else if (link === 'AND NOT') {
1705
- result = result && !nextCond;
1706
- }
1707
- }
1708
- return result;
1709
- }
1710
- /**
1711
- * Checks if a PlayerOffer meets its claimable conditions (completed -> claimable transition).
1712
- * @param claimableConditions - The offer's claimableConditions (from IOffer)
1713
- * @param claimableTrackers - The player offer's claimableTrackers
1714
- */
1715
- function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, claimableTrackers, }) {
1716
- if (!claimableConditions) {
1717
- return { isValid: true };
1718
- }
1719
- if (claimableConditions.siblingCompletions) {
1720
- const siblingCount = playerOfferTrackers?.siblingPlayerOffer_ids?.length ?? 0;
1721
- let completedCount = claimableTrackers?.siblingCompletions ?? 0;
1722
- if (completedCount == -1)
1723
- completedCount = siblingCount; // treat -1 as all completed
1724
- // if siblings exist but not all are completed, return false
1725
- if (siblingCount > 0 && completedCount < siblingCount) {
1726
- return { isValid: false };
1727
- }
1728
- }
1729
- return { isValid: true };
1730
- }
1731
- // returns contractAddress:network -> balance
1732
- function aggregateTokenBalances(data) {
1733
- const aggregatedBalances = {};
1734
- for (const { balances } of data?.cryptoWallets || []) {
1735
- if (!balances)
1736
- continue;
1737
- for (const [key, balance] of Object.entries(balances)) {
1738
- if (!aggregatedBalances[key]) {
1739
- aggregatedBalances[key] = 0;
1740
- }
1741
- aggregatedBalances[key] += balance;
1742
- }
1743
- }
1744
- return aggregatedBalances;
1745
- }
1746
- //# sourceMappingURL=conditions.js.map