@pixels-online/pixels-client-js-sdk 1.19.0 → 1.20.0

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.
@@ -646,7 +646,7 @@
646
646
  return this.players.get(targetId) || null;
647
647
  }
648
648
  setPlayer(player) {
649
- this.players.set(player.snapshot.playerId, player);
649
+ this.players.set(player.gameData.playerId, player);
650
650
  this.logger.log('Updated player:', player);
651
651
  }
652
652
  /**
@@ -658,9 +658,9 @@
658
658
  this.logger.warn('No target player to set offers for');
659
659
  return;
660
660
  }
661
- this.offers.set(targetPlayer.snapshot.playerId, new Map());
661
+ this.offers.set(targetPlayer.gameData.playerId, new Map());
662
662
  offers.forEach((offer) => {
663
- this.offers.get(targetPlayer.snapshot.playerId).set(offer.instanceId, offer);
663
+ this.offers.get(targetPlayer.gameData.playerId).set(offer.instanceId, offer);
664
664
  });
665
665
  this.logger.log(`Set ${offers.length} offers`);
666
666
  }
@@ -1178,12 +1178,13 @@
1178
1178
  try {
1179
1179
  const { offers, player } = await this.getOffersAndPlayer(targetId);
1180
1180
  if (targetId == null) {
1181
- this.selfId = player.snapshot.playerId;
1181
+ this.selfId = player.gameData.playerId;
1182
1182
  }
1183
1183
  this.offerStore.setPlayer(player);
1184
1184
  this.offerStore.setOffers(offers, player);
1185
1185
  this.eventEmitter.emit(exports.OfferEvent.REFRESH, { offers, player: player });
1186
1186
  this.logger.log('Refreshed offers and player snapshot');
1187
+ return offers;
1187
1188
  }
1188
1189
  catch (error) {
1189
1190
  this.handleError(error, 'refreshOffersAndPlayer');
@@ -1326,6 +1327,8 @@
1326
1327
  }
1327
1328
  }
1328
1329
 
1330
+ const DEFAULT_ENTITY_KIND = '_default';
1331
+
1329
1332
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1330
1333
  /**
1331
1334
  * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
@@ -1363,11 +1366,25 @@
1363
1366
  }));
1364
1367
  }
1365
1368
 
1369
+ const dynamicTrackerToPrimitive = (dynaTrack) => {
1370
+ const primitive = {};
1371
+ for (const key in dynaTrack) {
1372
+ primitive[key] = dynaTrack[key].value || 0;
1373
+ }
1374
+ return primitive;
1375
+ };
1376
+
1377
+ const addressNetworkId = (contractAddress, network) => {
1378
+ return `${contractAddress.toLowerCase()}:${network.toUpperCase()}`;
1379
+ };
1380
+
1366
1381
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1367
1382
  /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1368
1383
  * check doesn't use this since we don't have a playerOffer at surfacing time
1369
1384
  */
1370
- playerOffer, }) => {
1385
+ playerOffer,
1386
+ /** Additional data like fetched token balances that isn't part of playerSnap */
1387
+ additionalData, }) => {
1371
1388
  const conditionData = [];
1372
1389
  let isValid = true;
1373
1390
  if (conditions?.minDaysInGame) {
@@ -1695,7 +1712,8 @@
1695
1712
  // Validate link count conditions
1696
1713
  if (conditions?.links && 'entityLinks' in playerSnap) {
1697
1714
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1698
- const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
1715
+ // linkType should always exist. and be default is none was specified
1716
+ const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || DEFAULT_ENTITY_KIND) === linkType).length || 0;
1699
1717
  if (constraint.min !== undefined) {
1700
1718
  const isDisqualify = linkCount < constraint.min;
1701
1719
  if (addDetails) {
@@ -1782,16 +1800,60 @@
1782
1800
  return { isValid: false };
1783
1801
  }
1784
1802
  }
1803
+ // Evaluate token balance conditions
1804
+ for (const tokenCond of conditions?.tokenBalances || []) {
1805
+ const contracts = tokenCond.contracts || [];
1806
+ let totalBalance = 0;
1807
+ const fetchedBalances = aggregateTokenBalances(additionalData);
1808
+ for (const contract of contracts) {
1809
+ const balanceKey = addressNetworkId(contract.contractAddress, contract.network);
1810
+ totalBalance += fetchedBalances[balanceKey] || 0;
1811
+ }
1812
+ if (tokenCond.min !== undefined) {
1813
+ const isDisqualify = totalBalance < tokenCond.min;
1814
+ if (addDetails) {
1815
+ conditionData.push({
1816
+ isMet: !isDisqualify,
1817
+ kind: 'tokenBalances',
1818
+ trackerAmount: totalBalance,
1819
+ text: `Have at least ${tokenCond.min} ${tokenCond.name || 'tokens'}`,
1820
+ });
1821
+ if (isDisqualify)
1822
+ isValid = false;
1823
+ }
1824
+ else {
1825
+ if (isDisqualify)
1826
+ return { isValid: false };
1827
+ }
1828
+ }
1829
+ if (tokenCond.max !== undefined) {
1830
+ const isDisqualify = totalBalance > tokenCond.max;
1831
+ if (addDetails) {
1832
+ conditionData.push({
1833
+ isMet: !isDisqualify,
1834
+ kind: 'tokenBalances',
1835
+ trackerAmount: totalBalance,
1836
+ text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
1837
+ });
1838
+ if (isDisqualify)
1839
+ isValid = false;
1840
+ }
1841
+ else {
1842
+ if (isDisqualify)
1843
+ return { isValid: false };
1844
+ }
1845
+ }
1846
+ }
1785
1847
  return { isValid, conditionData: addDetails ? conditionData : undefined };
1786
1848
  };
1787
- const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
1849
+ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, additionalData, }) => {
1788
1850
  if (surfacingConditions?.contexts?.length &&
1789
1851
  !surfacingConditions.contexts?.includes(context || '')) {
1790
1852
  // context is not in the list of surfacing contexts, so we don't want to surface this offer
1791
1853
  return { isValid: false };
1792
1854
  }
1793
1855
  if (surfacingConditions?.targetEntityTypes?.length) {
1794
- const playerTarget = playerSnap.entityKind || 'default';
1856
+ const playerTarget = playerSnap.entityKind || DEFAULT_ENTITY_KIND;
1795
1857
  // check if entity type is allowed
1796
1858
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1797
1859
  return { isValid: false };
@@ -1872,7 +1934,7 @@
1872
1934
  }
1873
1935
  }
1874
1936
  }
1875
- return meetsBaseConditions({ conditions, playerSnap });
1937
+ return meetsBaseConditions({ conditions, playerSnap, additionalData });
1876
1938
  };
1877
1939
  const hasConditions = (conditions) => {
1878
1940
  if (!conditions)
@@ -1930,6 +1992,8 @@
1930
1992
  return true;
1931
1993
  if (surCond.networkRestrictions?.length)
1932
1994
  return true;
1995
+ if (surCond.linkedEntityOffers?.offer_id)
1996
+ return true;
1933
1997
  const compCond = conditions;
1934
1998
  if (compCond.context)
1935
1999
  return true;
@@ -1949,23 +2013,47 @@
1949
2013
  return true;
1950
2014
  if (compCond.dynamicTracker?.conditions?.length)
1951
2015
  return true;
2016
+ if (conditions.tokenBalances?.length)
2017
+ return true;
2018
+ if (Object.keys(compCond.contractInteractions || {}).length > 0)
2019
+ return true;
1952
2020
  return false;
1953
2021
  };
1954
- const offerMeetsCompletionConditions = (offer, snapshot) => {
2022
+ const meetsLinkedEntityOffersCondition = ({ linkedEntityOffers, matchingLinks, linkedPOfferMap, }) => {
2023
+ if (!linkedPOfferMap)
2024
+ return { isValid: false };
2025
+ const linkedPlayerOffer_ids = [];
2026
+ for (const link of matchingLinks) {
2027
+ const key = `${link.playerId}:${linkedEntityOffers.offer_id}`;
2028
+ const po = linkedPOfferMap.get(key);
2029
+ if (po) {
2030
+ linkedPlayerOffer_ids.push(po._id.toString());
2031
+ }
2032
+ }
2033
+ if (linkedPlayerOffer_ids.length > 0) {
2034
+ return { isValid: true, linkedPlayerOffer_ids };
2035
+ }
2036
+ return { isValid: false };
2037
+ };
2038
+ const offerMeetsCompletionConditions = (offer, snapshot, additionalData) => {
1955
2039
  return meetsCompletionConditions({
1956
2040
  completionConditions: offer.completionConditions || {},
1957
2041
  completionTrackers: offer.completionTrackers,
1958
2042
  playerSnap: snapshot,
1959
2043
  playerOffer: offer,
1960
2044
  addDetails: true,
2045
+ maxClaimCount: offer.maxClaimCount,
2046
+ additionalData,
1961
2047
  });
1962
2048
  };
1963
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
2049
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
1964
2050
  if (completionConditions) {
1965
2051
  const conditions = completionConditions;
1966
2052
  // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
1967
2053
  const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
1968
- const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
2054
+ const claimMultiplier = shouldScale
2055
+ ? (playerOffer.trackers?.claimedCount || 0) + 1
2056
+ : 1;
1969
2057
  const conditionData = [];
1970
2058
  let isValid = true;
1971
2059
  let maxTotalClaimsFromScaling = Infinity;
@@ -2096,12 +2184,14 @@
2096
2184
  }
2097
2185
  }
2098
2186
  if (conditions?.social) {
2099
- const tSocial = completionTrackers?.social;
2187
+ const tSocialAccumulate = completionTrackers?.social;
2188
+ const tSocialAttach = completionTrackers?.social;
2100
2189
  const cSocial = completionConditions.social;
2101
2190
  const mode = cSocial?.mode || 'attach';
2191
+ const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
2102
2192
  const hasContent = Boolean(mode === 'accumulate'
2103
- ? tSocial?.mode === 'accumulate'
2104
- : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
2193
+ ? tSocialAccumulate?.matchCount > 0
2194
+ : tSocialAttach?.videoId);
2105
2195
  // Only scale social metrics in accumulate mode (attach mode is single content)
2106
2196
  const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2107
2197
  const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
@@ -2136,7 +2226,7 @@
2136
2226
  .join(' | ');
2137
2227
  const requiredWords = cSocial?.requiredWords ?? [];
2138
2228
  if (mode === 'accumulate') {
2139
- const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
2229
+ const matchCount = tSocialAccumulate?.matchCount || 0;
2140
2230
  conditionData.push({
2141
2231
  isMet: hasContent,
2142
2232
  kind: 'social',
@@ -2149,7 +2239,7 @@
2149
2239
  });
2150
2240
  }
2151
2241
  else {
2152
- const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
2242
+ const title = tSocialAttach?.title;
2153
2243
  conditionData.push({
2154
2244
  isMet: hasContent,
2155
2245
  kind: 'social',
@@ -2226,22 +2316,22 @@
2226
2316
  if (conditions?.dynamicTracker?.conditions?.length) {
2227
2317
  const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2228
2318
  // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2229
- const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2319
+ const dynamicResult = meetsDynamicConditions(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2230
2320
  ...conditions.dynamicTracker,
2231
2321
  conditions: resolvedConditions,
2232
2322
  }, claimMultiplier);
2233
2323
  if (shouldScale) {
2234
- const dynamicMax = getMaxClaimsForDynamicGroup(completionTrackers?.dynamicTracker || {}, {
2324
+ const dynamicMax = getMaxClaimsForDynamicGroup(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2235
2325
  ...conditions.dynamicTracker,
2236
2326
  conditions: resolvedConditions,
2237
- }, playerOffer.claimedCount || 0);
2327
+ }, playerOffer?.trackers?.claimedCount || 0);
2238
2328
  updateMax(dynamicMax);
2239
2329
  }
2240
2330
  if (addDetails) {
2241
2331
  conditionData.push({
2242
2332
  isMet: dynamicResult,
2243
- kind: 'dynamic',
2244
- text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
2333
+ kind: 'dynamicTracker',
2334
+ text: renderTemplate(conditions.dynamicTracker.template, dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
2245
2335
  });
2246
2336
  if (!dynamicResult)
2247
2337
  isValid = false;
@@ -2251,19 +2341,64 @@
2251
2341
  return { isValid: false, availableClaimsNow: 0 };
2252
2342
  }
2253
2343
  }
2344
+ // Evaluate contractInteractions completion trackers
2345
+ if (conditions?.contractInteractions) {
2346
+ for (const [conditionId, condition] of Object.entries(conditions.contractInteractions)) {
2347
+ const baseAmount = condition.amount || 0;
2348
+ const scaledAmount = baseAmount * claimMultiplier;
2349
+ const trackerValue = completionTrackers?.contractInteractions?.[conditionId] || 0;
2350
+ const isDisqualify = trackerValue < scaledAmount;
2351
+ if (shouldScale && baseAmount > 0) {
2352
+ updateMax(Math.floor(trackerValue / baseAmount));
2353
+ }
2354
+ if (addDetails) {
2355
+ let displayText;
2356
+ const eventType = condition.event;
2357
+ const name = condition.name || 'tokens';
2358
+ if (eventType === 'spend') {
2359
+ displayText = `Spend ${scaledAmount} ${name}`;
2360
+ }
2361
+ else if (eventType === 'earn') {
2362
+ displayText = `Earn ${scaledAmount} ${name}`;
2363
+ }
2364
+ else if (eventType === 'gain') {
2365
+ displayText = `Gain ${scaledAmount} ${name}`;
2366
+ }
2367
+ else if (eventType === 'lose') {
2368
+ displayText = `Lose ${scaledAmount} ${name}`;
2369
+ }
2370
+ else {
2371
+ displayText = `${name}: ${scaledAmount}`;
2372
+ }
2373
+ conditionData.push({
2374
+ isMet: !isDisqualify,
2375
+ kind: 'contractInteractions',
2376
+ trackerAmount: trackerValue,
2377
+ text: displayText,
2378
+ });
2379
+ if (isDisqualify)
2380
+ isValid = false;
2381
+ }
2382
+ else {
2383
+ if (isDisqualify)
2384
+ return { isValid: false, availableClaimsNow: 0 };
2385
+ }
2386
+ }
2387
+ }
2254
2388
  const r = meetsBaseConditions({
2255
2389
  conditions,
2256
2390
  playerSnap,
2257
2391
  addDetails: true,
2258
2392
  playerOffer,
2393
+ additionalData,
2259
2394
  });
2260
2395
  isValid = isValid && r.isValid;
2261
2396
  conditionData.push(...(r.conditionData || []));
2262
2397
  if (maxClaimCount && maxClaimCount > 0) {
2263
2398
  updateMax(maxClaimCount);
2264
2399
  }
2265
- const claimedCount = playerOffer.claimedCount || 0;
2266
- let availableClaimsNow = !isValid
2400
+ const claimedCount = playerOffer?.trackers?.claimedCount || 0;
2401
+ const availableClaimsNow = !isValid
2267
2402
  ? 0
2268
2403
  : maxTotalClaimsFromScaling === Infinity
2269
2404
  ? -1
@@ -2614,6 +2749,19 @@
2614
2749
  }
2615
2750
  return { isValid: true };
2616
2751
  }
2752
+ // returns contractAddress:network -> balance
2753
+ function aggregateTokenBalances(data) {
2754
+ const aggregatedBalances = {};
2755
+ for (const { balances } of data?.cryptoWallets || []) {
2756
+ for (const [key, balance] of Object.entries(balances)) {
2757
+ if (!aggregatedBalances[key]) {
2758
+ aggregatedBalances[key] = 0;
2759
+ }
2760
+ aggregatedBalances[key] += balance;
2761
+ }
2762
+ }
2763
+ return aggregatedBalances;
2764
+ }
2617
2765
 
2618
2766
  const offerListenerEvents = ['claim_offer'];
2619
2767
  const PlayerOfferStatuses = [
@@ -2662,11 +2810,13 @@
2662
2810
  };
2663
2811
 
2664
2812
  exports.AssetHelper = AssetHelper;
2813
+ exports.DEFAULT_ENTITY_KIND = DEFAULT_ENTITY_KIND;
2665
2814
  exports.EventEmitter = EventEmitter;
2666
2815
  exports.OfferStore = OfferStore;
2667
2816
  exports.OfferwallClient = OfferwallClient;
2668
2817
  exports.PlayerOfferStatuses = PlayerOfferStatuses;
2669
2818
  exports.SSEConnection = SSEConnection;
2819
+ exports.aggregateTokenBalances = aggregateTokenBalances;
2670
2820
  exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
2671
2821
  exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
2672
2822
  exports.hasConditions = hasConditions;
@@ -2675,6 +2825,7 @@
2675
2825
  exports.meetsCompletionConditions = meetsCompletionConditions;
2676
2826
  exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
2677
2827
  exports.meetsDynamicConditions = meetsDynamicConditions;
2828
+ exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
2678
2829
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2679
2830
  exports.offerListenerEvents = offerListenerEvents;
2680
2831
  exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;