@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.
package/dist/index.js CHANGED
@@ -642,7 +642,7 @@ class OfferStore {
642
642
  return this.players.get(targetId) || null;
643
643
  }
644
644
  setPlayer(player) {
645
- this.players.set(player.snapshot.playerId, player);
645
+ this.players.set(player.gameData.playerId, player);
646
646
  this.logger.log('Updated player:', player);
647
647
  }
648
648
  /**
@@ -654,9 +654,9 @@ class OfferStore {
654
654
  this.logger.warn('No target player to set offers for');
655
655
  return;
656
656
  }
657
- this.offers.set(targetPlayer.snapshot.playerId, new Map());
657
+ this.offers.set(targetPlayer.gameData.playerId, new Map());
658
658
  offers.forEach((offer) => {
659
- this.offers.get(targetPlayer.snapshot.playerId).set(offer.instanceId, offer);
659
+ this.offers.get(targetPlayer.gameData.playerId).set(offer.instanceId, offer);
660
660
  });
661
661
  this.logger.log(`Set ${offers.length} offers`);
662
662
  }
@@ -1174,12 +1174,13 @@ class OfferwallClient {
1174
1174
  try {
1175
1175
  const { offers, player } = await this.getOffersAndPlayer(targetId);
1176
1176
  if (targetId == null) {
1177
- this.selfId = player.snapshot.playerId;
1177
+ this.selfId = player.gameData.playerId;
1178
1178
  }
1179
1179
  this.offerStore.setPlayer(player);
1180
1180
  this.offerStore.setOffers(offers, player);
1181
1181
  this.eventEmitter.emit(exports.OfferEvent.REFRESH, { offers, player: player });
1182
1182
  this.logger.log('Refreshed offers and player snapshot');
1183
+ return offers;
1183
1184
  }
1184
1185
  catch (error) {
1185
1186
  this.handleError(error, 'refreshOffersAndPlayer');
@@ -1322,6 +1323,8 @@ class OfferwallClient {
1322
1323
  }
1323
1324
  }
1324
1325
 
1326
+ const DEFAULT_ENTITY_KIND = '_default';
1327
+
1325
1328
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1326
1329
  /**
1327
1330
  * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
@@ -1359,11 +1362,25 @@ function replaceDynamicConditionKeys(conditions, trackers) {
1359
1362
  }));
1360
1363
  }
1361
1364
 
1365
+ const dynamicTrackerToPrimitive = (dynaTrack) => {
1366
+ const primitive = {};
1367
+ for (const key in dynaTrack) {
1368
+ primitive[key] = dynaTrack[key].value || 0;
1369
+ }
1370
+ return primitive;
1371
+ };
1372
+
1373
+ const addressNetworkId = (contractAddress, network) => {
1374
+ return `${contractAddress.toLowerCase()}:${network.toUpperCase()}`;
1375
+ };
1376
+
1362
1377
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1363
1378
  /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1364
1379
  * check doesn't use this since we don't have a playerOffer at surfacing time
1365
1380
  */
1366
- playerOffer, }) => {
1381
+ playerOffer,
1382
+ /** Additional data like fetched token balances that isn't part of playerSnap */
1383
+ additionalData, }) => {
1367
1384
  const conditionData = [];
1368
1385
  let isValid = true;
1369
1386
  if (conditions?.minDaysInGame) {
@@ -1691,7 +1708,8 @@ playerOffer, }) => {
1691
1708
  // Validate link count conditions
1692
1709
  if (conditions?.links && 'entityLinks' in playerSnap) {
1693
1710
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1694
- const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
1711
+ // linkType should always exist. and be default is none was specified
1712
+ const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || DEFAULT_ENTITY_KIND) === linkType).length || 0;
1695
1713
  if (constraint.min !== undefined) {
1696
1714
  const isDisqualify = linkCount < constraint.min;
1697
1715
  if (addDetails) {
@@ -1778,16 +1796,60 @@ playerOffer, }) => {
1778
1796
  return { isValid: false };
1779
1797
  }
1780
1798
  }
1799
+ // Evaluate token balance conditions
1800
+ for (const tokenCond of conditions?.tokenBalances || []) {
1801
+ const contracts = tokenCond.contracts || [];
1802
+ let totalBalance = 0;
1803
+ const fetchedBalances = aggregateTokenBalances(additionalData);
1804
+ for (const contract of contracts) {
1805
+ const balanceKey = addressNetworkId(contract.contractAddress, contract.network);
1806
+ totalBalance += fetchedBalances[balanceKey] || 0;
1807
+ }
1808
+ if (tokenCond.min !== undefined) {
1809
+ const isDisqualify = totalBalance < tokenCond.min;
1810
+ if (addDetails) {
1811
+ conditionData.push({
1812
+ isMet: !isDisqualify,
1813
+ kind: 'tokenBalances',
1814
+ trackerAmount: totalBalance,
1815
+ text: `Have at least ${tokenCond.min} ${tokenCond.name || 'tokens'}`,
1816
+ });
1817
+ if (isDisqualify)
1818
+ isValid = false;
1819
+ }
1820
+ else {
1821
+ if (isDisqualify)
1822
+ return { isValid: false };
1823
+ }
1824
+ }
1825
+ if (tokenCond.max !== undefined) {
1826
+ const isDisqualify = totalBalance > tokenCond.max;
1827
+ if (addDetails) {
1828
+ conditionData.push({
1829
+ isMet: !isDisqualify,
1830
+ kind: 'tokenBalances',
1831
+ trackerAmount: totalBalance,
1832
+ text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
1833
+ });
1834
+ if (isDisqualify)
1835
+ isValid = false;
1836
+ }
1837
+ else {
1838
+ if (isDisqualify)
1839
+ return { isValid: false };
1840
+ }
1841
+ }
1842
+ }
1781
1843
  return { isValid, conditionData: addDetails ? conditionData : undefined };
1782
1844
  };
1783
- const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
1845
+ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, additionalData, }) => {
1784
1846
  if (surfacingConditions?.contexts?.length &&
1785
1847
  !surfacingConditions.contexts?.includes(context || '')) {
1786
1848
  // context is not in the list of surfacing contexts, so we don't want to surface this offer
1787
1849
  return { isValid: false };
1788
1850
  }
1789
1851
  if (surfacingConditions?.targetEntityTypes?.length) {
1790
- const playerTarget = playerSnap.entityKind || 'default';
1852
+ const playerTarget = playerSnap.entityKind || DEFAULT_ENTITY_KIND;
1791
1853
  // check if entity type is allowed
1792
1854
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1793
1855
  return { isValid: false };
@@ -1868,7 +1930,7 @@ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, pl
1868
1930
  }
1869
1931
  }
1870
1932
  }
1871
- return meetsBaseConditions({ conditions, playerSnap });
1933
+ return meetsBaseConditions({ conditions, playerSnap, additionalData });
1872
1934
  };
1873
1935
  const hasConditions = (conditions) => {
1874
1936
  if (!conditions)
@@ -1926,6 +1988,8 @@ const hasConditions = (conditions) => {
1926
1988
  return true;
1927
1989
  if (surCond.networkRestrictions?.length)
1928
1990
  return true;
1991
+ if (surCond.linkedEntityOffers?.offer_id)
1992
+ return true;
1929
1993
  const compCond = conditions;
1930
1994
  if (compCond.context)
1931
1995
  return true;
@@ -1945,23 +2009,47 @@ const hasConditions = (conditions) => {
1945
2009
  return true;
1946
2010
  if (compCond.dynamicTracker?.conditions?.length)
1947
2011
  return true;
2012
+ if (conditions.tokenBalances?.length)
2013
+ return true;
2014
+ if (Object.keys(compCond.contractInteractions || {}).length > 0)
2015
+ return true;
1948
2016
  return false;
1949
2017
  };
1950
- const offerMeetsCompletionConditions = (offer, snapshot) => {
2018
+ const meetsLinkedEntityOffersCondition = ({ linkedEntityOffers, matchingLinks, linkedPOfferMap, }) => {
2019
+ if (!linkedPOfferMap)
2020
+ return { isValid: false };
2021
+ const linkedPlayerOffer_ids = [];
2022
+ for (const link of matchingLinks) {
2023
+ const key = `${link.playerId}:${linkedEntityOffers.offer_id}`;
2024
+ const po = linkedPOfferMap.get(key);
2025
+ if (po) {
2026
+ linkedPlayerOffer_ids.push(po._id.toString());
2027
+ }
2028
+ }
2029
+ if (linkedPlayerOffer_ids.length > 0) {
2030
+ return { isValid: true, linkedPlayerOffer_ids };
2031
+ }
2032
+ return { isValid: false };
2033
+ };
2034
+ const offerMeetsCompletionConditions = (offer, snapshot, additionalData) => {
1951
2035
  return meetsCompletionConditions({
1952
2036
  completionConditions: offer.completionConditions || {},
1953
2037
  completionTrackers: offer.completionTrackers,
1954
2038
  playerSnap: snapshot,
1955
2039
  playerOffer: offer,
1956
2040
  addDetails: true,
2041
+ maxClaimCount: offer.maxClaimCount,
2042
+ additionalData,
1957
2043
  });
1958
2044
  };
1959
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
2045
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
1960
2046
  if (completionConditions) {
1961
2047
  const conditions = completionConditions;
1962
2048
  // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
1963
2049
  const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
1964
- const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
2050
+ const claimMultiplier = shouldScale
2051
+ ? (playerOffer.trackers?.claimedCount || 0) + 1
2052
+ : 1;
1965
2053
  const conditionData = [];
1966
2054
  let isValid = true;
1967
2055
  let maxTotalClaimsFromScaling = Infinity;
@@ -2092,12 +2180,14 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2092
2180
  }
2093
2181
  }
2094
2182
  if (conditions?.social) {
2095
- const tSocial = completionTrackers?.social;
2183
+ const tSocialAccumulate = completionTrackers?.social;
2184
+ const tSocialAttach = completionTrackers?.social;
2096
2185
  const cSocial = completionConditions.social;
2097
2186
  const mode = cSocial?.mode || 'attach';
2187
+ const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
2098
2188
  const hasContent = Boolean(mode === 'accumulate'
2099
- ? tSocial?.mode === 'accumulate'
2100
- : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
2189
+ ? tSocialAccumulate?.matchCount > 0
2190
+ : tSocialAttach?.videoId);
2101
2191
  // Only scale social metrics in accumulate mode (attach mode is single content)
2102
2192
  const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2103
2193
  const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
@@ -2132,7 +2222,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2132
2222
  .join(' | ');
2133
2223
  const requiredWords = cSocial?.requiredWords ?? [];
2134
2224
  if (mode === 'accumulate') {
2135
- const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
2225
+ const matchCount = tSocialAccumulate?.matchCount || 0;
2136
2226
  conditionData.push({
2137
2227
  isMet: hasContent,
2138
2228
  kind: 'social',
@@ -2145,7 +2235,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2145
2235
  });
2146
2236
  }
2147
2237
  else {
2148
- const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
2238
+ const title = tSocialAttach?.title;
2149
2239
  conditionData.push({
2150
2240
  isMet: hasContent,
2151
2241
  kind: 'social',
@@ -2222,22 +2312,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2222
2312
  if (conditions?.dynamicTracker?.conditions?.length) {
2223
2313
  const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2224
2314
  // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2225
- const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2315
+ const dynamicResult = meetsDynamicConditions(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2226
2316
  ...conditions.dynamicTracker,
2227
2317
  conditions: resolvedConditions,
2228
2318
  }, claimMultiplier);
2229
2319
  if (shouldScale) {
2230
- const dynamicMax = getMaxClaimsForDynamicGroup(completionTrackers?.dynamicTracker || {}, {
2320
+ const dynamicMax = getMaxClaimsForDynamicGroup(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2231
2321
  ...conditions.dynamicTracker,
2232
2322
  conditions: resolvedConditions,
2233
- }, playerOffer.claimedCount || 0);
2323
+ }, playerOffer?.trackers?.claimedCount || 0);
2234
2324
  updateMax(dynamicMax);
2235
2325
  }
2236
2326
  if (addDetails) {
2237
2327
  conditionData.push({
2238
2328
  isMet: dynamicResult,
2239
- kind: 'dynamic',
2240
- text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
2329
+ kind: 'dynamicTracker',
2330
+ text: renderTemplate(conditions.dynamicTracker.template, dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
2241
2331
  });
2242
2332
  if (!dynamicResult)
2243
2333
  isValid = false;
@@ -2247,19 +2337,64 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2247
2337
  return { isValid: false, availableClaimsNow: 0 };
2248
2338
  }
2249
2339
  }
2340
+ // Evaluate contractInteractions completion trackers
2341
+ if (conditions?.contractInteractions) {
2342
+ for (const [conditionId, condition] of Object.entries(conditions.contractInteractions)) {
2343
+ const baseAmount = condition.amount || 0;
2344
+ const scaledAmount = baseAmount * claimMultiplier;
2345
+ const trackerValue = completionTrackers?.contractInteractions?.[conditionId] || 0;
2346
+ const isDisqualify = trackerValue < scaledAmount;
2347
+ if (shouldScale && baseAmount > 0) {
2348
+ updateMax(Math.floor(trackerValue / baseAmount));
2349
+ }
2350
+ if (addDetails) {
2351
+ let displayText;
2352
+ const eventType = condition.event;
2353
+ const name = condition.name || 'tokens';
2354
+ if (eventType === 'spend') {
2355
+ displayText = `Spend ${scaledAmount} ${name}`;
2356
+ }
2357
+ else if (eventType === 'earn') {
2358
+ displayText = `Earn ${scaledAmount} ${name}`;
2359
+ }
2360
+ else if (eventType === 'gain') {
2361
+ displayText = `Gain ${scaledAmount} ${name}`;
2362
+ }
2363
+ else if (eventType === 'lose') {
2364
+ displayText = `Lose ${scaledAmount} ${name}`;
2365
+ }
2366
+ else {
2367
+ displayText = `${name}: ${scaledAmount}`;
2368
+ }
2369
+ conditionData.push({
2370
+ isMet: !isDisqualify,
2371
+ kind: 'contractInteractions',
2372
+ trackerAmount: trackerValue,
2373
+ text: displayText,
2374
+ });
2375
+ if (isDisqualify)
2376
+ isValid = false;
2377
+ }
2378
+ else {
2379
+ if (isDisqualify)
2380
+ return { isValid: false, availableClaimsNow: 0 };
2381
+ }
2382
+ }
2383
+ }
2250
2384
  const r = meetsBaseConditions({
2251
2385
  conditions,
2252
2386
  playerSnap,
2253
2387
  addDetails: true,
2254
2388
  playerOffer,
2389
+ additionalData,
2255
2390
  });
2256
2391
  isValid = isValid && r.isValid;
2257
2392
  conditionData.push(...(r.conditionData || []));
2258
2393
  if (maxClaimCount && maxClaimCount > 0) {
2259
2394
  updateMax(maxClaimCount);
2260
2395
  }
2261
- const claimedCount = playerOffer.claimedCount || 0;
2262
- let availableClaimsNow = !isValid
2396
+ const claimedCount = playerOffer?.trackers?.claimedCount || 0;
2397
+ const availableClaimsNow = !isValid
2263
2398
  ? 0
2264
2399
  : maxTotalClaimsFromScaling === Infinity
2265
2400
  ? -1
@@ -2610,6 +2745,19 @@ function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, cl
2610
2745
  }
2611
2746
  return { isValid: true };
2612
2747
  }
2748
+ // returns contractAddress:network -> balance
2749
+ function aggregateTokenBalances(data) {
2750
+ const aggregatedBalances = {};
2751
+ for (const { balances } of data?.cryptoWallets || []) {
2752
+ for (const [key, balance] of Object.entries(balances)) {
2753
+ if (!aggregatedBalances[key]) {
2754
+ aggregatedBalances[key] = 0;
2755
+ }
2756
+ aggregatedBalances[key] += balance;
2757
+ }
2758
+ }
2759
+ return aggregatedBalances;
2760
+ }
2613
2761
 
2614
2762
  const offerListenerEvents = ['claim_offer'];
2615
2763
  const PlayerOfferStatuses = [
@@ -2658,11 +2806,13 @@ const rewardSchema = {
2658
2806
  };
2659
2807
 
2660
2808
  exports.AssetHelper = AssetHelper;
2809
+ exports.DEFAULT_ENTITY_KIND = DEFAULT_ENTITY_KIND;
2661
2810
  exports.EventEmitter = EventEmitter;
2662
2811
  exports.OfferStore = OfferStore;
2663
2812
  exports.OfferwallClient = OfferwallClient;
2664
2813
  exports.PlayerOfferStatuses = PlayerOfferStatuses;
2665
2814
  exports.SSEConnection = SSEConnection;
2815
+ exports.aggregateTokenBalances = aggregateTokenBalances;
2666
2816
  exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
2667
2817
  exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
2668
2818
  exports.hasConditions = hasConditions;
@@ -2671,6 +2821,7 @@ exports.meetsClaimableConditions = meetsClaimableConditions;
2671
2821
  exports.meetsCompletionConditions = meetsCompletionConditions;
2672
2822
  exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
2673
2823
  exports.meetsDynamicConditions = meetsDynamicConditions;
2824
+ exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
2674
2825
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2675
2826
  exports.offerListenerEvents = offerListenerEvents;
2676
2827
  exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;