@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.
@@ -634,25 +634,33 @@
634
634
  }
635
635
 
636
636
  class OfferStore {
637
- constructor(config) {
637
+ constructor(config, client) {
638
+ this.client = client;
638
639
  this.offers = new Map();
639
- this.player = null;
640
+ this.players = new Map();
640
641
  this.logger = createLogger(config, 'OfferStore');
641
642
  }
642
- getPlayer() {
643
- return this.player || null;
643
+ getPlayer(targetId = this.client.getSelfId()) {
644
+ if (!targetId)
645
+ return null;
646
+ return this.players.get(targetId) || null;
644
647
  }
645
648
  setPlayer(player) {
646
- this.player = player;
649
+ this.players.set(player.gameData.playerId, player);
647
650
  this.logger.log('Updated player:', player);
648
651
  }
649
652
  /**
650
653
  * Set all offers (replaces existing)
651
654
  */
652
- setOffers(offers) {
653
- this.offers.clear();
655
+ setOffers(offers, target) {
656
+ const targetPlayer = target || this.getPlayer();
657
+ if (!targetPlayer) {
658
+ this.logger.warn('No target player to set offers for');
659
+ return;
660
+ }
661
+ this.offers.set(targetPlayer.gameData.playerId, new Map());
654
662
  offers.forEach((offer) => {
655
- this.offers.set(offer.instanceId, offer);
663
+ this.offers.get(targetPlayer.gameData.playerId).set(offer.instanceId, offer);
656
664
  });
657
665
  this.logger.log(`Set ${offers.length} offers`);
658
666
  }
@@ -660,16 +668,23 @@
660
668
  * Add or update a single offer
661
669
  */
662
670
  upsertOffer(offer) {
663
- const previousOffer = this.offers.get(offer.instanceId);
664
- this.offers.set(offer.instanceId, offer);
671
+ let playerOffers = this.offers.get(offer.playerId);
672
+ if (!playerOffers) {
673
+ playerOffers = new Map();
674
+ this.offers.set(offer.playerId, playerOffers);
675
+ }
676
+ const previousOffer = playerOffers.get(offer.instanceId);
677
+ playerOffers.set(offer.instanceId, offer);
665
678
  this.logger.log(`${previousOffer ? 'Updated' : 'Added'} offer:`, offer.instanceId);
666
679
  return previousOffer;
667
680
  }
668
681
  /**
669
682
  * Remove an offer
670
683
  */
671
- removeOffer(offerId) {
672
- const removed = this.offers.delete(offerId);
684
+ removeOffer(offerId, targetId = this.client.getSelfId()) {
685
+ if (!targetId)
686
+ return false;
687
+ const removed = this.offers.get(targetId)?.delete(offerId) || false;
673
688
  if (removed) {
674
689
  this.logger.log(`Removed offer:`, offerId);
675
690
  }
@@ -678,26 +693,30 @@
678
693
  /**
679
694
  * Get a single offer
680
695
  */
681
- getOffer(offerId) {
682
- return this.offers.get(offerId);
696
+ getOffer(offerId, targetId = this.client.getSelfId()) {
697
+ if (!targetId)
698
+ return undefined;
699
+ return this.offers.get(targetId)?.get(offerId);
683
700
  }
684
701
  /**
685
702
  * Get all offers
686
703
  */
687
- getAllOffers() {
688
- return Array.from(this.offers.values());
704
+ getAllOffers(targetId = this.client.getSelfId()) {
705
+ if (!targetId)
706
+ return [];
707
+ return Array.from(this.offers.get(targetId)?.values() || []);
689
708
  }
690
709
  /**
691
710
  * Get offers filtered by status
692
711
  */
693
- getOffersByStatus(status) {
694
- return this.getAllOffers().filter((offer) => offer.status === status);
712
+ getOffersByStatus(status, targetId = this.client.getSelfId()) {
713
+ return this.getAllOffers(targetId).filter((offer) => offer.status === status);
695
714
  }
696
715
  /**
697
716
  * Get active offers (not expired, not claimed)
698
717
  */
699
- getActiveOffers() {
700
- return this.getAllOffers().filter((offer) => {
718
+ getActiveOffers(targetId = this.client.getSelfId()) {
719
+ return this.getAllOffers(targetId).filter((offer) => {
701
720
  if (!offer.status)
702
721
  return false; // Must have a status
703
722
  return (offer.status === 'surfaced' ||
@@ -708,14 +727,14 @@
708
727
  /**
709
728
  * Get claimable offers
710
729
  */
711
- getClaimableOffers() {
712
- return this.getOffersByStatus('claimable');
730
+ getClaimableOffers(targetId = this.client.getSelfId()) {
731
+ return this.getOffersByStatus('claimable', targetId);
713
732
  }
714
733
  /**
715
734
  * Check if an offer has expired
716
735
  */
717
- isOfferExpired(offerId) {
718
- const offer = this.getOffer(offerId);
736
+ isOfferExpired(offerId, targetId = this.client.getSelfId()) {
737
+ const offer = this.getOffer(offerId, targetId);
719
738
  if (!offer)
720
739
  return true;
721
740
  // Check status
@@ -728,17 +747,29 @@
728
747
  return false;
729
748
  }
730
749
  /**
731
- * Clear all offers
750
+ * Clear all offers for a specific player
751
+ */
752
+ clear(targetId = this.client.getSelfId()) {
753
+ if (!targetId)
754
+ return;
755
+ this.offers.set(targetId, new Map());
756
+ this.logger.log('Cleared all offers for player:', targetId);
757
+ }
758
+ /**
759
+ * Clear all offers for all players
732
760
  */
733
- clear() {
761
+ clearAll() {
734
762
  this.offers.clear();
735
- this.logger.log('Cleared all offers');
763
+ this.logger.log('Cleared all offers for all players');
736
764
  }
737
765
  /**
738
- * Get offer count
766
+ * Get offer count (for self player)
739
767
  */
740
768
  get size() {
741
- return this.offers.size;
769
+ const selfId = this.client.getSelfId();
770
+ if (!selfId)
771
+ return 0;
772
+ return this.offers.get(selfId)?.size || 0;
742
773
  }
743
774
  }
744
775
 
@@ -841,7 +872,9 @@
841
872
  return null;
842
873
  }
843
874
  setCurrencyAssetContents(currencies) {
844
- this.currencies = currencies;
875
+ Object.keys(currencies).forEach((key) => {
876
+ this.currencies[key] = currencies[key];
877
+ });
845
878
  }
846
879
  resolveReward(reward) {
847
880
  if (reward.kind === 'loyalty_currency' && reward.rewardId) {
@@ -897,6 +930,7 @@
897
930
  constructor(config) {
898
931
  this.isInitializing = false;
899
932
  this.pendingStackedToken = null;
933
+ this.selfId = null;
900
934
  this.config = {
901
935
  autoConnect: config.autoConnect ?? false,
902
936
  reconnect: config.reconnect ?? true,
@@ -909,7 +943,7 @@
909
943
  this.hooks = this.config.hooks || {};
910
944
  this.logger = createLogger(this.config, 'OfferwallClient');
911
945
  this.eventEmitter = new EventEmitter(this.config);
912
- this.offerStore = new OfferStore(this.config);
946
+ this.offerStore = new OfferStore(this.config, this);
913
947
  this.tokenManager = new TokenManager(this.config);
914
948
  this.assetHelper = new AssetHelper(this.config);
915
949
  this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
@@ -950,6 +984,9 @@
950
984
  get assets() {
951
985
  return this.assetHelper;
952
986
  }
987
+ getSelfId() {
988
+ return this.selfId;
989
+ }
953
990
  /**
954
991
  * Initialize the offerwall client and connect
955
992
  */
@@ -1012,8 +1049,9 @@
1012
1049
  if (this.sseConnection) {
1013
1050
  this.sseConnection.disconnect();
1014
1051
  }
1015
- this.offerStore.clear();
1052
+ this.offerStore.clearAll();
1016
1053
  this.tokenManager.clearToken();
1054
+ this.selfId = null;
1017
1055
  if (this.hooks.afterDisconnect) {
1018
1056
  await this.hooks.afterDisconnect();
1019
1057
  }
@@ -1021,8 +1059,8 @@
1021
1059
  /**
1022
1060
  * Claim rewards for an offer
1023
1061
  */
1024
- async claimReward(instanceId) {
1025
- const offer = this.offerStore.getOffer(instanceId);
1062
+ async claimReward(instanceId, targetId = this.getSelfId()) {
1063
+ const offer = this.offerStore.getOffer(instanceId, targetId);
1026
1064
  if (!offer) {
1027
1065
  throw new Error(`Offer ${instanceId} not found`);
1028
1066
  }
@@ -1037,7 +1075,7 @@
1037
1075
  }
1038
1076
  }
1039
1077
  try {
1040
- const response = await this.claimOfferAPI(instanceId);
1078
+ const response = await this.claimOfferAPI(instanceId, targetId);
1041
1079
  const updatedOffer = { ...offer, status: 'claimed' };
1042
1080
  this.offerStore.upsertOffer(updatedOffer);
1043
1081
  this.eventEmitter.emit(exports.OfferEvent.OFFER_CLAIMED, {
@@ -1093,7 +1131,7 @@
1093
1131
  });
1094
1132
  */
1095
1133
  this.eventEmitter.on(exports.OfferEvent.OFFER_SURFACED, ({ offer }) => {
1096
- this.offerStore.upsertOffer(offer);
1134
+ this.offerStore.upsertOffer(offer); // should always be selfId
1097
1135
  this.logger.log(`Surfaced offer: ${offer.instanceId}`);
1098
1136
  });
1099
1137
  }
@@ -1123,34 +1161,40 @@
1123
1161
  }
1124
1162
  return response.json();
1125
1163
  }
1126
- async claimOfferAPI(instanceId) {
1164
+ async claimOfferAPI(instanceId, targetId = null) {
1127
1165
  return this.postWithAuth('/v1/client/reward/claim', {
1128
1166
  instanceId,
1129
1167
  kind: 'offer',
1168
+ targetId: targetId || undefined,
1130
1169
  });
1131
1170
  }
1132
- getPlayer() {
1133
- return this.offerStore.getPlayer();
1171
+ getPlayer(targetId = this.getSelfId()) {
1172
+ return this.offerStore.getPlayer(targetId);
1134
1173
  }
1135
- getOffers() {
1136
- return this.offerStore.getAllOffers();
1174
+ getOffers(targetId = this.getSelfId()) {
1175
+ return this.offerStore.getAllOffers(targetId);
1137
1176
  }
1138
- async refreshOffersAndPlayer() {
1177
+ async refreshOffersAndPlayer(targetId = null) {
1139
1178
  try {
1140
- const { offers, player } = await this.getOffersAndPlayer();
1141
- this.offerStore.setOffers(offers);
1179
+ const { offers, player } = await this.getOffersAndPlayer(targetId);
1180
+ if (targetId == null) {
1181
+ this.selfId = player.gameData.playerId;
1182
+ }
1142
1183
  this.offerStore.setPlayer(player);
1184
+ this.offerStore.setOffers(offers, player);
1143
1185
  this.eventEmitter.emit(exports.OfferEvent.REFRESH, { offers, player: player });
1144
1186
  this.logger.log('Refreshed offers and player snapshot');
1187
+ return offers;
1145
1188
  }
1146
1189
  catch (error) {
1147
1190
  this.handleError(error, 'refreshOffersAndPlayer');
1148
1191
  throw error;
1149
1192
  }
1150
1193
  }
1151
- async getOffersAndPlayer() {
1194
+ async getOffersAndPlayer(targetId = null) {
1152
1195
  const data = await this.postWithAuth('/v1/client/player/campaigns', {
1153
1196
  viewingCampaigns: true,
1197
+ targetId: targetId || undefined,
1154
1198
  });
1155
1199
  if (!data.offers || !Array.isArray(data.offers)) {
1156
1200
  throw new Error('No offers returned from offers endpoint');
@@ -1283,6 +1327,8 @@
1283
1327
  }
1284
1328
  }
1285
1329
 
1330
+ const DEFAULT_ENTITY_KIND = '_default';
1331
+
1286
1332
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1287
1333
  /**
1288
1334
  * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
@@ -1320,11 +1366,25 @@
1320
1366
  }));
1321
1367
  }
1322
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
+
1323
1381
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1324
1382
  /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1325
1383
  * check doesn't use this since we don't have a playerOffer at surfacing time
1326
1384
  */
1327
- playerOffer, }) => {
1385
+ playerOffer,
1386
+ /** Additional data like fetched token balances that isn't part of playerSnap */
1387
+ additionalData, }) => {
1328
1388
  const conditionData = [];
1329
1389
  let isValid = true;
1330
1390
  if (conditions?.minDaysInGame) {
@@ -1652,7 +1712,8 @@
1652
1712
  // Validate link count conditions
1653
1713
  if (conditions?.links && 'entityLinks' in playerSnap) {
1654
1714
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1655
- 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;
1656
1717
  if (constraint.min !== undefined) {
1657
1718
  const isDisqualify = linkCount < constraint.min;
1658
1719
  if (addDetails) {
@@ -1739,16 +1800,60 @@
1739
1800
  return { isValid: false };
1740
1801
  }
1741
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
+ }
1742
1847
  return { isValid, conditionData: addDetails ? conditionData : undefined };
1743
1848
  };
1744
- const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
1849
+ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, additionalData, }) => {
1745
1850
  if (surfacingConditions?.contexts?.length &&
1746
1851
  !surfacingConditions.contexts?.includes(context || '')) {
1747
1852
  // context is not in the list of surfacing contexts, so we don't want to surface this offer
1748
1853
  return { isValid: false };
1749
1854
  }
1750
1855
  if (surfacingConditions?.targetEntityTypes?.length) {
1751
- const playerTarget = playerSnap.entityKind || 'default';
1856
+ const playerTarget = playerSnap.entityKind || DEFAULT_ENTITY_KIND;
1752
1857
  // check if entity type is allowed
1753
1858
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1754
1859
  return { isValid: false };
@@ -1829,7 +1934,7 @@
1829
1934
  }
1830
1935
  }
1831
1936
  }
1832
- return meetsBaseConditions({ conditions, playerSnap });
1937
+ return meetsBaseConditions({ conditions, playerSnap, additionalData });
1833
1938
  };
1834
1939
  const hasConditions = (conditions) => {
1835
1940
  if (!conditions)
@@ -1887,6 +1992,8 @@
1887
1992
  return true;
1888
1993
  if (surCond.networkRestrictions?.length)
1889
1994
  return true;
1995
+ if (surCond.linkedEntityOffers?.offer_id)
1996
+ return true;
1890
1997
  const compCond = conditions;
1891
1998
  if (compCond.context)
1892
1999
  return true;
@@ -1906,23 +2013,47 @@
1906
2013
  return true;
1907
2014
  if (compCond.dynamicTracker?.conditions?.length)
1908
2015
  return true;
2016
+ if (conditions.tokenBalances?.length)
2017
+ return true;
2018
+ if (Object.keys(compCond.contractInteractions || {}).length > 0)
2019
+ return true;
1909
2020
  return false;
1910
2021
  };
1911
- 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) => {
1912
2039
  return meetsCompletionConditions({
1913
2040
  completionConditions: offer.completionConditions || {},
1914
2041
  completionTrackers: offer.completionTrackers,
1915
2042
  playerSnap: snapshot,
1916
2043
  playerOffer: offer,
1917
2044
  addDetails: true,
2045
+ maxClaimCount: offer.maxClaimCount,
2046
+ additionalData,
1918
2047
  });
1919
2048
  };
1920
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
2049
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
1921
2050
  if (completionConditions) {
1922
2051
  const conditions = completionConditions;
1923
2052
  // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
1924
2053
  const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
1925
- const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
2054
+ const claimMultiplier = shouldScale
2055
+ ? (playerOffer.trackers?.claimedCount || 0) + 1
2056
+ : 1;
1926
2057
  const conditionData = [];
1927
2058
  let isValid = true;
1928
2059
  let maxTotalClaimsFromScaling = Infinity;
@@ -2053,12 +2184,14 @@
2053
2184
  }
2054
2185
  }
2055
2186
  if (conditions?.social) {
2056
- const tSocial = completionTrackers?.social;
2187
+ const tSocialAccumulate = completionTrackers?.social;
2188
+ const tSocialAttach = completionTrackers?.social;
2057
2189
  const cSocial = completionConditions.social;
2058
2190
  const mode = cSocial?.mode || 'attach';
2191
+ const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
2059
2192
  const hasContent = Boolean(mode === 'accumulate'
2060
- ? tSocial?.mode === 'accumulate'
2061
- : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
2193
+ ? tSocialAccumulate?.matchCount > 0
2194
+ : tSocialAttach?.videoId);
2062
2195
  // Only scale social metrics in accumulate mode (attach mode is single content)
2063
2196
  const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2064
2197
  const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
@@ -2093,7 +2226,7 @@
2093
2226
  .join(' | ');
2094
2227
  const requiredWords = cSocial?.requiredWords ?? [];
2095
2228
  if (mode === 'accumulate') {
2096
- const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
2229
+ const matchCount = tSocialAccumulate?.matchCount || 0;
2097
2230
  conditionData.push({
2098
2231
  isMet: hasContent,
2099
2232
  kind: 'social',
@@ -2106,7 +2239,7 @@
2106
2239
  });
2107
2240
  }
2108
2241
  else {
2109
- const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
2242
+ const title = tSocialAttach?.title;
2110
2243
  conditionData.push({
2111
2244
  isMet: hasContent,
2112
2245
  kind: 'social',
@@ -2183,22 +2316,22 @@
2183
2316
  if (conditions?.dynamicTracker?.conditions?.length) {
2184
2317
  const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2185
2318
  // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2186
- const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2319
+ const dynamicResult = meetsDynamicConditions(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2187
2320
  ...conditions.dynamicTracker,
2188
2321
  conditions: resolvedConditions,
2189
2322
  }, claimMultiplier);
2190
2323
  if (shouldScale) {
2191
- const dynamicMax = getMaxClaimsForDynamicGroup(completionTrackers?.dynamicTracker || {}, {
2324
+ const dynamicMax = getMaxClaimsForDynamicGroup(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2192
2325
  ...conditions.dynamicTracker,
2193
2326
  conditions: resolvedConditions,
2194
- }, playerOffer.claimedCount || 0);
2327
+ }, playerOffer?.trackers?.claimedCount || 0);
2195
2328
  updateMax(dynamicMax);
2196
2329
  }
2197
2330
  if (addDetails) {
2198
2331
  conditionData.push({
2199
2332
  isMet: dynamicResult,
2200
- kind: 'dynamic',
2201
- text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
2333
+ kind: 'dynamicTracker',
2334
+ text: renderTemplate(conditions.dynamicTracker.template, dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
2202
2335
  });
2203
2336
  if (!dynamicResult)
2204
2337
  isValid = false;
@@ -2208,19 +2341,64 @@
2208
2341
  return { isValid: false, availableClaimsNow: 0 };
2209
2342
  }
2210
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
+ }
2211
2388
  const r = meetsBaseConditions({
2212
2389
  conditions,
2213
2390
  playerSnap,
2214
2391
  addDetails: true,
2215
2392
  playerOffer,
2393
+ additionalData,
2216
2394
  });
2217
2395
  isValid = isValid && r.isValid;
2218
2396
  conditionData.push(...(r.conditionData || []));
2219
2397
  if (maxClaimCount && maxClaimCount > 0) {
2220
2398
  updateMax(maxClaimCount);
2221
2399
  }
2222
- const claimedCount = playerOffer.claimedCount || 0;
2223
- let availableClaimsNow = !isValid
2400
+ const claimedCount = playerOffer?.trackers?.claimedCount || 0;
2401
+ const availableClaimsNow = !isValid
2224
2402
  ? 0
2225
2403
  : maxTotalClaimsFromScaling === Infinity
2226
2404
  ? -1
@@ -2571,6 +2749,19 @@
2571
2749
  }
2572
2750
  return { isValid: true };
2573
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
+ }
2574
2765
 
2575
2766
  const offerListenerEvents = ['claim_offer'];
2576
2767
  const PlayerOfferStatuses = [
@@ -2619,11 +2810,13 @@
2619
2810
  };
2620
2811
 
2621
2812
  exports.AssetHelper = AssetHelper;
2813
+ exports.DEFAULT_ENTITY_KIND = DEFAULT_ENTITY_KIND;
2622
2814
  exports.EventEmitter = EventEmitter;
2623
2815
  exports.OfferStore = OfferStore;
2624
2816
  exports.OfferwallClient = OfferwallClient;
2625
2817
  exports.PlayerOfferStatuses = PlayerOfferStatuses;
2626
2818
  exports.SSEConnection = SSEConnection;
2819
+ exports.aggregateTokenBalances = aggregateTokenBalances;
2627
2820
  exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
2628
2821
  exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
2629
2822
  exports.hasConditions = hasConditions;
@@ -2632,6 +2825,7 @@
2632
2825
  exports.meetsCompletionConditions = meetsCompletionConditions;
2633
2826
  exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
2634
2827
  exports.meetsDynamicConditions = meetsDynamicConditions;
2828
+ exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
2635
2829
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2636
2830
  exports.offerListenerEvents = offerListenerEvents;
2637
2831
  exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;