@pixels-online/pixels-client-js-sdk 1.18.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
@@ -630,25 +630,33 @@ class SSEConnection {
630
630
  }
631
631
 
632
632
  class OfferStore {
633
- constructor(config) {
633
+ constructor(config, client) {
634
+ this.client = client;
634
635
  this.offers = new Map();
635
- this.player = null;
636
+ this.players = new Map();
636
637
  this.logger = createLogger(config, 'OfferStore');
637
638
  }
638
- getPlayer() {
639
- return this.player || null;
639
+ getPlayer(targetId = this.client.getSelfId()) {
640
+ if (!targetId)
641
+ return null;
642
+ return this.players.get(targetId) || null;
640
643
  }
641
644
  setPlayer(player) {
642
- this.player = player;
645
+ this.players.set(player.gameData.playerId, player);
643
646
  this.logger.log('Updated player:', player);
644
647
  }
645
648
  /**
646
649
  * Set all offers (replaces existing)
647
650
  */
648
- setOffers(offers) {
649
- this.offers.clear();
651
+ setOffers(offers, target) {
652
+ const targetPlayer = target || this.getPlayer();
653
+ if (!targetPlayer) {
654
+ this.logger.warn('No target player to set offers for');
655
+ return;
656
+ }
657
+ this.offers.set(targetPlayer.gameData.playerId, new Map());
650
658
  offers.forEach((offer) => {
651
- this.offers.set(offer.instanceId, offer);
659
+ this.offers.get(targetPlayer.gameData.playerId).set(offer.instanceId, offer);
652
660
  });
653
661
  this.logger.log(`Set ${offers.length} offers`);
654
662
  }
@@ -656,16 +664,23 @@ class OfferStore {
656
664
  * Add or update a single offer
657
665
  */
658
666
  upsertOffer(offer) {
659
- const previousOffer = this.offers.get(offer.instanceId);
660
- this.offers.set(offer.instanceId, offer);
667
+ let playerOffers = this.offers.get(offer.playerId);
668
+ if (!playerOffers) {
669
+ playerOffers = new Map();
670
+ this.offers.set(offer.playerId, playerOffers);
671
+ }
672
+ const previousOffer = playerOffers.get(offer.instanceId);
673
+ playerOffers.set(offer.instanceId, offer);
661
674
  this.logger.log(`${previousOffer ? 'Updated' : 'Added'} offer:`, offer.instanceId);
662
675
  return previousOffer;
663
676
  }
664
677
  /**
665
678
  * Remove an offer
666
679
  */
667
- removeOffer(offerId) {
668
- const removed = this.offers.delete(offerId);
680
+ removeOffer(offerId, targetId = this.client.getSelfId()) {
681
+ if (!targetId)
682
+ return false;
683
+ const removed = this.offers.get(targetId)?.delete(offerId) || false;
669
684
  if (removed) {
670
685
  this.logger.log(`Removed offer:`, offerId);
671
686
  }
@@ -674,26 +689,30 @@ class OfferStore {
674
689
  /**
675
690
  * Get a single offer
676
691
  */
677
- getOffer(offerId) {
678
- return this.offers.get(offerId);
692
+ getOffer(offerId, targetId = this.client.getSelfId()) {
693
+ if (!targetId)
694
+ return undefined;
695
+ return this.offers.get(targetId)?.get(offerId);
679
696
  }
680
697
  /**
681
698
  * Get all offers
682
699
  */
683
- getAllOffers() {
684
- return Array.from(this.offers.values());
700
+ getAllOffers(targetId = this.client.getSelfId()) {
701
+ if (!targetId)
702
+ return [];
703
+ return Array.from(this.offers.get(targetId)?.values() || []);
685
704
  }
686
705
  /**
687
706
  * Get offers filtered by status
688
707
  */
689
- getOffersByStatus(status) {
690
- return this.getAllOffers().filter((offer) => offer.status === status);
708
+ getOffersByStatus(status, targetId = this.client.getSelfId()) {
709
+ return this.getAllOffers(targetId).filter((offer) => offer.status === status);
691
710
  }
692
711
  /**
693
712
  * Get active offers (not expired, not claimed)
694
713
  */
695
- getActiveOffers() {
696
- return this.getAllOffers().filter((offer) => {
714
+ getActiveOffers(targetId = this.client.getSelfId()) {
715
+ return this.getAllOffers(targetId).filter((offer) => {
697
716
  if (!offer.status)
698
717
  return false; // Must have a status
699
718
  return (offer.status === 'surfaced' ||
@@ -704,14 +723,14 @@ class OfferStore {
704
723
  /**
705
724
  * Get claimable offers
706
725
  */
707
- getClaimableOffers() {
708
- return this.getOffersByStatus('claimable');
726
+ getClaimableOffers(targetId = this.client.getSelfId()) {
727
+ return this.getOffersByStatus('claimable', targetId);
709
728
  }
710
729
  /**
711
730
  * Check if an offer has expired
712
731
  */
713
- isOfferExpired(offerId) {
714
- const offer = this.getOffer(offerId);
732
+ isOfferExpired(offerId, targetId = this.client.getSelfId()) {
733
+ const offer = this.getOffer(offerId, targetId);
715
734
  if (!offer)
716
735
  return true;
717
736
  // Check status
@@ -724,17 +743,29 @@ class OfferStore {
724
743
  return false;
725
744
  }
726
745
  /**
727
- * Clear all offers
746
+ * Clear all offers for a specific player
747
+ */
748
+ clear(targetId = this.client.getSelfId()) {
749
+ if (!targetId)
750
+ return;
751
+ this.offers.set(targetId, new Map());
752
+ this.logger.log('Cleared all offers for player:', targetId);
753
+ }
754
+ /**
755
+ * Clear all offers for all players
728
756
  */
729
- clear() {
757
+ clearAll() {
730
758
  this.offers.clear();
731
- this.logger.log('Cleared all offers');
759
+ this.logger.log('Cleared all offers for all players');
732
760
  }
733
761
  /**
734
- * Get offer count
762
+ * Get offer count (for self player)
735
763
  */
736
764
  get size() {
737
- return this.offers.size;
765
+ const selfId = this.client.getSelfId();
766
+ if (!selfId)
767
+ return 0;
768
+ return this.offers.get(selfId)?.size || 0;
738
769
  }
739
770
  }
740
771
 
@@ -837,7 +868,9 @@ class AssetHelper {
837
868
  return null;
838
869
  }
839
870
  setCurrencyAssetContents(currencies) {
840
- this.currencies = currencies;
871
+ Object.keys(currencies).forEach((key) => {
872
+ this.currencies[key] = currencies[key];
873
+ });
841
874
  }
842
875
  resolveReward(reward) {
843
876
  if (reward.kind === 'loyalty_currency' && reward.rewardId) {
@@ -893,6 +926,7 @@ class OfferwallClient {
893
926
  constructor(config) {
894
927
  this.isInitializing = false;
895
928
  this.pendingStackedToken = null;
929
+ this.selfId = null;
896
930
  this.config = {
897
931
  autoConnect: config.autoConnect ?? false,
898
932
  reconnect: config.reconnect ?? true,
@@ -905,7 +939,7 @@ class OfferwallClient {
905
939
  this.hooks = this.config.hooks || {};
906
940
  this.logger = createLogger(this.config, 'OfferwallClient');
907
941
  this.eventEmitter = new EventEmitter(this.config);
908
- this.offerStore = new OfferStore(this.config);
942
+ this.offerStore = new OfferStore(this.config, this);
909
943
  this.tokenManager = new TokenManager(this.config);
910
944
  this.assetHelper = new AssetHelper(this.config);
911
945
  this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
@@ -946,6 +980,9 @@ class OfferwallClient {
946
980
  get assets() {
947
981
  return this.assetHelper;
948
982
  }
983
+ getSelfId() {
984
+ return this.selfId;
985
+ }
949
986
  /**
950
987
  * Initialize the offerwall client and connect
951
988
  */
@@ -1008,8 +1045,9 @@ class OfferwallClient {
1008
1045
  if (this.sseConnection) {
1009
1046
  this.sseConnection.disconnect();
1010
1047
  }
1011
- this.offerStore.clear();
1048
+ this.offerStore.clearAll();
1012
1049
  this.tokenManager.clearToken();
1050
+ this.selfId = null;
1013
1051
  if (this.hooks.afterDisconnect) {
1014
1052
  await this.hooks.afterDisconnect();
1015
1053
  }
@@ -1017,8 +1055,8 @@ class OfferwallClient {
1017
1055
  /**
1018
1056
  * Claim rewards for an offer
1019
1057
  */
1020
- async claimReward(instanceId) {
1021
- const offer = this.offerStore.getOffer(instanceId);
1058
+ async claimReward(instanceId, targetId = this.getSelfId()) {
1059
+ const offer = this.offerStore.getOffer(instanceId, targetId);
1022
1060
  if (!offer) {
1023
1061
  throw new Error(`Offer ${instanceId} not found`);
1024
1062
  }
@@ -1033,7 +1071,7 @@ class OfferwallClient {
1033
1071
  }
1034
1072
  }
1035
1073
  try {
1036
- const response = await this.claimOfferAPI(instanceId);
1074
+ const response = await this.claimOfferAPI(instanceId, targetId);
1037
1075
  const updatedOffer = { ...offer, status: 'claimed' };
1038
1076
  this.offerStore.upsertOffer(updatedOffer);
1039
1077
  this.eventEmitter.emit(exports.OfferEvent.OFFER_CLAIMED, {
@@ -1089,7 +1127,7 @@ class OfferwallClient {
1089
1127
  });
1090
1128
  */
1091
1129
  this.eventEmitter.on(exports.OfferEvent.OFFER_SURFACED, ({ offer }) => {
1092
- this.offerStore.upsertOffer(offer);
1130
+ this.offerStore.upsertOffer(offer); // should always be selfId
1093
1131
  this.logger.log(`Surfaced offer: ${offer.instanceId}`);
1094
1132
  });
1095
1133
  }
@@ -1119,34 +1157,40 @@ class OfferwallClient {
1119
1157
  }
1120
1158
  return response.json();
1121
1159
  }
1122
- async claimOfferAPI(instanceId) {
1160
+ async claimOfferAPI(instanceId, targetId = null) {
1123
1161
  return this.postWithAuth('/v1/client/reward/claim', {
1124
1162
  instanceId,
1125
1163
  kind: 'offer',
1164
+ targetId: targetId || undefined,
1126
1165
  });
1127
1166
  }
1128
- getPlayer() {
1129
- return this.offerStore.getPlayer();
1167
+ getPlayer(targetId = this.getSelfId()) {
1168
+ return this.offerStore.getPlayer(targetId);
1130
1169
  }
1131
- getOffers() {
1132
- return this.offerStore.getAllOffers();
1170
+ getOffers(targetId = this.getSelfId()) {
1171
+ return this.offerStore.getAllOffers(targetId);
1133
1172
  }
1134
- async refreshOffersAndPlayer() {
1173
+ async refreshOffersAndPlayer(targetId = null) {
1135
1174
  try {
1136
- const { offers, player } = await this.getOffersAndPlayer();
1137
- this.offerStore.setOffers(offers);
1175
+ const { offers, player } = await this.getOffersAndPlayer(targetId);
1176
+ if (targetId == null) {
1177
+ this.selfId = player.gameData.playerId;
1178
+ }
1138
1179
  this.offerStore.setPlayer(player);
1180
+ this.offerStore.setOffers(offers, player);
1139
1181
  this.eventEmitter.emit(exports.OfferEvent.REFRESH, { offers, player: player });
1140
1182
  this.logger.log('Refreshed offers and player snapshot');
1183
+ return offers;
1141
1184
  }
1142
1185
  catch (error) {
1143
1186
  this.handleError(error, 'refreshOffersAndPlayer');
1144
1187
  throw error;
1145
1188
  }
1146
1189
  }
1147
- async getOffersAndPlayer() {
1190
+ async getOffersAndPlayer(targetId = null) {
1148
1191
  const data = await this.postWithAuth('/v1/client/player/campaigns', {
1149
1192
  viewingCampaigns: true,
1193
+ targetId: targetId || undefined,
1150
1194
  });
1151
1195
  if (!data.offers || !Array.isArray(data.offers)) {
1152
1196
  throw new Error('No offers returned from offers endpoint');
@@ -1279,6 +1323,8 @@ class OfferwallClient {
1279
1323
  }
1280
1324
  }
1281
1325
 
1326
+ const DEFAULT_ENTITY_KIND = '_default';
1327
+
1282
1328
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1283
1329
  /**
1284
1330
  * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
@@ -1316,11 +1362,25 @@ function replaceDynamicConditionKeys(conditions, trackers) {
1316
1362
  }));
1317
1363
  }
1318
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
+
1319
1377
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1320
1378
  /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1321
1379
  * check doesn't use this since we don't have a playerOffer at surfacing time
1322
1380
  */
1323
- playerOffer, }) => {
1381
+ playerOffer,
1382
+ /** Additional data like fetched token balances that isn't part of playerSnap */
1383
+ additionalData, }) => {
1324
1384
  const conditionData = [];
1325
1385
  let isValid = true;
1326
1386
  if (conditions?.minDaysInGame) {
@@ -1648,7 +1708,8 @@ playerOffer, }) => {
1648
1708
  // Validate link count conditions
1649
1709
  if (conditions?.links && 'entityLinks' in playerSnap) {
1650
1710
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1651
- 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;
1652
1713
  if (constraint.min !== undefined) {
1653
1714
  const isDisqualify = linkCount < constraint.min;
1654
1715
  if (addDetails) {
@@ -1735,16 +1796,60 @@ playerOffer, }) => {
1735
1796
  return { isValid: false };
1736
1797
  }
1737
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
+ }
1738
1843
  return { isValid, conditionData: addDetails ? conditionData : undefined };
1739
1844
  };
1740
- const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
1845
+ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, additionalData, }) => {
1741
1846
  if (surfacingConditions?.contexts?.length &&
1742
1847
  !surfacingConditions.contexts?.includes(context || '')) {
1743
1848
  // context is not in the list of surfacing contexts, so we don't want to surface this offer
1744
1849
  return { isValid: false };
1745
1850
  }
1746
1851
  if (surfacingConditions?.targetEntityTypes?.length) {
1747
- const playerTarget = playerSnap.entityKind || 'default';
1852
+ const playerTarget = playerSnap.entityKind || DEFAULT_ENTITY_KIND;
1748
1853
  // check if entity type is allowed
1749
1854
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1750
1855
  return { isValid: false };
@@ -1825,7 +1930,7 @@ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, pl
1825
1930
  }
1826
1931
  }
1827
1932
  }
1828
- return meetsBaseConditions({ conditions, playerSnap });
1933
+ return meetsBaseConditions({ conditions, playerSnap, additionalData });
1829
1934
  };
1830
1935
  const hasConditions = (conditions) => {
1831
1936
  if (!conditions)
@@ -1883,6 +1988,8 @@ const hasConditions = (conditions) => {
1883
1988
  return true;
1884
1989
  if (surCond.networkRestrictions?.length)
1885
1990
  return true;
1991
+ if (surCond.linkedEntityOffers?.offer_id)
1992
+ return true;
1886
1993
  const compCond = conditions;
1887
1994
  if (compCond.context)
1888
1995
  return true;
@@ -1902,23 +2009,47 @@ const hasConditions = (conditions) => {
1902
2009
  return true;
1903
2010
  if (compCond.dynamicTracker?.conditions?.length)
1904
2011
  return true;
2012
+ if (conditions.tokenBalances?.length)
2013
+ return true;
2014
+ if (Object.keys(compCond.contractInteractions || {}).length > 0)
2015
+ return true;
1905
2016
  return false;
1906
2017
  };
1907
- 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) => {
1908
2035
  return meetsCompletionConditions({
1909
2036
  completionConditions: offer.completionConditions || {},
1910
2037
  completionTrackers: offer.completionTrackers,
1911
2038
  playerSnap: snapshot,
1912
2039
  playerOffer: offer,
1913
2040
  addDetails: true,
2041
+ maxClaimCount: offer.maxClaimCount,
2042
+ additionalData,
1914
2043
  });
1915
2044
  };
1916
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
2045
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
1917
2046
  if (completionConditions) {
1918
2047
  const conditions = completionConditions;
1919
2048
  // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
1920
2049
  const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
1921
- const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
2050
+ const claimMultiplier = shouldScale
2051
+ ? (playerOffer.trackers?.claimedCount || 0) + 1
2052
+ : 1;
1922
2053
  const conditionData = [];
1923
2054
  let isValid = true;
1924
2055
  let maxTotalClaimsFromScaling = Infinity;
@@ -2049,12 +2180,14 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2049
2180
  }
2050
2181
  }
2051
2182
  if (conditions?.social) {
2052
- const tSocial = completionTrackers?.social;
2183
+ const tSocialAccumulate = completionTrackers?.social;
2184
+ const tSocialAttach = completionTrackers?.social;
2053
2185
  const cSocial = completionConditions.social;
2054
2186
  const mode = cSocial?.mode || 'attach';
2187
+ const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
2055
2188
  const hasContent = Boolean(mode === 'accumulate'
2056
- ? tSocial?.mode === 'accumulate'
2057
- : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
2189
+ ? tSocialAccumulate?.matchCount > 0
2190
+ : tSocialAttach?.videoId);
2058
2191
  // Only scale social metrics in accumulate mode (attach mode is single content)
2059
2192
  const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2060
2193
  const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
@@ -2089,7 +2222,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2089
2222
  .join(' | ');
2090
2223
  const requiredWords = cSocial?.requiredWords ?? [];
2091
2224
  if (mode === 'accumulate') {
2092
- const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
2225
+ const matchCount = tSocialAccumulate?.matchCount || 0;
2093
2226
  conditionData.push({
2094
2227
  isMet: hasContent,
2095
2228
  kind: 'social',
@@ -2102,7 +2235,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2102
2235
  });
2103
2236
  }
2104
2237
  else {
2105
- const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
2238
+ const title = tSocialAttach?.title;
2106
2239
  conditionData.push({
2107
2240
  isMet: hasContent,
2108
2241
  kind: 'social',
@@ -2179,22 +2312,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2179
2312
  if (conditions?.dynamicTracker?.conditions?.length) {
2180
2313
  const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2181
2314
  // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2182
- const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2315
+ const dynamicResult = meetsDynamicConditions(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2183
2316
  ...conditions.dynamicTracker,
2184
2317
  conditions: resolvedConditions,
2185
2318
  }, claimMultiplier);
2186
2319
  if (shouldScale) {
2187
- const dynamicMax = getMaxClaimsForDynamicGroup(completionTrackers?.dynamicTracker || {}, {
2320
+ const dynamicMax = getMaxClaimsForDynamicGroup(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2188
2321
  ...conditions.dynamicTracker,
2189
2322
  conditions: resolvedConditions,
2190
- }, playerOffer.claimedCount || 0);
2323
+ }, playerOffer?.trackers?.claimedCount || 0);
2191
2324
  updateMax(dynamicMax);
2192
2325
  }
2193
2326
  if (addDetails) {
2194
2327
  conditionData.push({
2195
2328
  isMet: dynamicResult,
2196
- kind: 'dynamic',
2197
- text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
2329
+ kind: 'dynamicTracker',
2330
+ text: renderTemplate(conditions.dynamicTracker.template, dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
2198
2331
  });
2199
2332
  if (!dynamicResult)
2200
2333
  isValid = false;
@@ -2204,19 +2337,64 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2204
2337
  return { isValid: false, availableClaimsNow: 0 };
2205
2338
  }
2206
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
+ }
2207
2384
  const r = meetsBaseConditions({
2208
2385
  conditions,
2209
2386
  playerSnap,
2210
2387
  addDetails: true,
2211
2388
  playerOffer,
2389
+ additionalData,
2212
2390
  });
2213
2391
  isValid = isValid && r.isValid;
2214
2392
  conditionData.push(...(r.conditionData || []));
2215
2393
  if (maxClaimCount && maxClaimCount > 0) {
2216
2394
  updateMax(maxClaimCount);
2217
2395
  }
2218
- const claimedCount = playerOffer.claimedCount || 0;
2219
- let availableClaimsNow = !isValid
2396
+ const claimedCount = playerOffer?.trackers?.claimedCount || 0;
2397
+ const availableClaimsNow = !isValid
2220
2398
  ? 0
2221
2399
  : maxTotalClaimsFromScaling === Infinity
2222
2400
  ? -1
@@ -2567,6 +2745,19 @@ function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, cl
2567
2745
  }
2568
2746
  return { isValid: true };
2569
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
+ }
2570
2761
 
2571
2762
  const offerListenerEvents = ['claim_offer'];
2572
2763
  const PlayerOfferStatuses = [
@@ -2615,11 +2806,13 @@ const rewardSchema = {
2615
2806
  };
2616
2807
 
2617
2808
  exports.AssetHelper = AssetHelper;
2809
+ exports.DEFAULT_ENTITY_KIND = DEFAULT_ENTITY_KIND;
2618
2810
  exports.EventEmitter = EventEmitter;
2619
2811
  exports.OfferStore = OfferStore;
2620
2812
  exports.OfferwallClient = OfferwallClient;
2621
2813
  exports.PlayerOfferStatuses = PlayerOfferStatuses;
2622
2814
  exports.SSEConnection = SSEConnection;
2815
+ exports.aggregateTokenBalances = aggregateTokenBalances;
2623
2816
  exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
2624
2817
  exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
2625
2818
  exports.hasConditions = hasConditions;
@@ -2628,6 +2821,7 @@ exports.meetsClaimableConditions = meetsClaimableConditions;
2628
2821
  exports.meetsCompletionConditions = meetsCompletionConditions;
2629
2822
  exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
2630
2823
  exports.meetsDynamicConditions = meetsDynamicConditions;
2824
+ exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
2631
2825
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2632
2826
  exports.offerListenerEvents = offerListenerEvents;
2633
2827
  exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;