@metamask/social-controllers 3.6.0 → 3.7.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.
@@ -33,10 +33,13 @@ const PositionStruct = structType({
33
33
  chain: string(),
34
34
  positionAmount: number(),
35
35
  isOpen: optional(boolean()),
36
- boughtUsd: number(),
37
- soldUsd: number(),
38
- realizedPnl: number(),
39
- costBasis: number(),
36
+ // Incomplete Clicker metrics serialize these as `null` (`undefined + n`
37
+ // becomes `NaN`, then JSON `null`). Rejecting the field used to fail the
38
+ // entire feed page, so they are nullable here.
39
+ boughtUsd: nullable(number()),
40
+ soldUsd: nullable(number()),
41
+ realizedPnl: nullable(number()),
42
+ costBasis: nullable(number()),
40
43
  trades: array(TradeStruct),
41
44
  lastTradeAt: number(),
42
45
  tokenImageUrl: optional(nullable(string())),
@@ -182,6 +185,34 @@ const FeedResponseStruct = structType({
182
185
  items: array(FeedItemStruct),
183
186
  pagination: FeedPaginationStruct,
184
187
  });
188
+ /**
189
+ * One malformed feed item must not blank the page. When the envelope is
190
+ * well-shaped, keep items that match {@link FeedItemStruct} and drop the rest.
191
+ * Still throw when `items` or `pagination` themselves are unusable.
192
+ *
193
+ * @param feedData - Parsed JSON body from a feed endpoint.
194
+ * @param invalidMessage - Error message to throw when the envelope is unusable.
195
+ * @returns The validated feed response, possibly with invalid items dropped.
196
+ */
197
+ const parseFeedResponse = (feedData, invalidMessage) => {
198
+ if (is(feedData, FeedResponseStruct)) {
199
+ return feedData;
200
+ }
201
+ if (typeof feedData !== 'object' ||
202
+ feedData === null ||
203
+ !('items' in feedData) ||
204
+ !('pagination' in feedData)) {
205
+ throw new Error(invalidMessage);
206
+ }
207
+ const { items, pagination } = feedData;
208
+ if (!Array.isArray(items) || !is(pagination, FeedPaginationStruct)) {
209
+ throw new Error(invalidMessage);
210
+ }
211
+ return {
212
+ items: items.filter((item) => is(item, FeedItemStruct)),
213
+ pagination,
214
+ };
215
+ };
185
216
  const FollowersResponseStruct = structType({
186
217
  followers: array(ProfileSummaryStruct),
187
218
  count: number(),
@@ -227,7 +258,10 @@ const MESSENGER_EXPOSED_METHODS = [
227
258
  'fetchOpenPositions',
228
259
  'fetchClosedPositions',
229
260
  'fetchFollowers',
261
+ 'fetchTraderFollowing',
230
262
  'fetchFollowing',
263
+ 'fetchMyFollowing',
264
+ 'fetchMyFollowers',
231
265
  'fetchPositionById',
232
266
  'fetchFeed',
233
267
  'fetchTraderFeed',
@@ -314,7 +348,7 @@ export class SocialService extends BaseDataService {
314
348
  headers: authHeaders,
315
349
  });
316
350
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_LEADERBOARD_FAILED);
317
- const leaderboardData = await response.json();
351
+ const leaderboardData = (await response.json());
318
352
  if (!is(leaderboardData, LeaderboardResponseStruct)) {
319
353
  throw new Error(SocialServiceErrorMessage.FETCH_LEADERBOARD_INVALID_RESPONSE);
320
354
  }
@@ -341,7 +375,7 @@ export class SocialService extends BaseDataService {
341
375
  const authHeaders = await this.#getAuthHeaders();
342
376
  const response = await fetch(url, { headers: authHeaders });
343
377
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TRADER_PROFILE_FAILED);
344
- const traderProfileData = await response.json();
378
+ const traderProfileData = (await response.json());
345
379
  if (!is(traderProfileData, TraderProfileResponseStruct)) {
346
380
  throw new Error(SocialServiceErrorMessage.FETCH_TRADER_PROFILE_INVALID_RESPONSE);
347
381
  }
@@ -385,22 +419,31 @@ export class SocialService extends BaseDataService {
385
419
  /**
386
420
  * Fetches a trader's MetaMask followers.
387
421
  *
388
- * Calls `GET ${baseUrl}/traders/${addressOrId}/followers`.
422
+ * Calls `GET ${baseUrl}/traders/${addressOrId}/followers`. Pass
423
+ * `mutuals: true` to append `?mutuals=true` (profiles that follow both this
424
+ * trader and the authenticated user; `count` is the mutual total). Requires a
425
+ * bearer token; the trader is identified by `addressOrId`, not the JWT
426
+ * subject (see {@link fetchMyFollowers} for the current user's inbound list).
389
427
  *
390
428
  * @param options - Options bag.
391
429
  * @param options.addressOrId - Wallet address or Clicker profile ID.
430
+ * @param options.mutuals - When true, request mutual followers only.
392
431
  * @returns The followers response.
393
432
  */
394
433
  async fetchFollowers(options) {
395
- const { addressOrId } = options;
434
+ const { addressOrId, mutuals } = options;
435
+ const mutualsOnly = mutuals === true;
396
436
  const followersResponse = await this.fetchQuery({
397
- queryKey: [`${this.name}:fetchFollowers`, addressOrId],
437
+ queryKey: [`${this.name}:fetchFollowers`, addressOrId, mutualsOnly],
398
438
  queryFn: async () => {
399
- const url = `${this.#v1Url}/traders/${encodeURIComponent(addressOrId)}/followers`;
439
+ const url = new URL(`${this.#v1Url}/traders/${encodeURIComponent(addressOrId)}/followers`);
440
+ if (mutualsOnly) {
441
+ url.searchParams.append('mutuals', 'true');
442
+ }
400
443
  const authHeaders = await this.#getAuthHeaders();
401
- const response = await fetch(url, { headers: authHeaders });
444
+ const response = await fetch(url.toString(), { headers: authHeaders });
402
445
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_FOLLOWERS_FAILED);
403
- const followersData = await response.json();
446
+ const followersData = (await response.json());
404
447
  if (!is(followersData, FollowersResponseStruct)) {
405
448
  throw new Error(SocialServiceErrorMessage.FETCH_FOLLOWERS_INVALID_RESPONSE);
406
449
  }
@@ -409,6 +452,35 @@ export class SocialService extends BaseDataService {
409
452
  });
410
453
  return followersResponse;
411
454
  }
455
+ /**
456
+ * Fetches the list of traders a profile follows.
457
+ *
458
+ * Calls `GET ${baseUrl}/traders/${addressOrId}/following`. Requires a bearer
459
+ * token; the trader is identified by `addressOrId`, not the JWT subject (see
460
+ * {@link fetchFollowing} for the current user's outbound list).
461
+ *
462
+ * @param options - Options bag.
463
+ * @param options.addressOrId - Wallet address or Clicker profile ID.
464
+ * @returns The following response.
465
+ */
466
+ async fetchTraderFollowing(options) {
467
+ const { addressOrId } = options;
468
+ const followingResponse = await this.fetchQuery({
469
+ queryKey: [`${this.name}:fetchTraderFollowing`, addressOrId],
470
+ queryFn: async () => {
471
+ const url = `${this.#v1Url}/traders/${encodeURIComponent(addressOrId)}/following`;
472
+ const authHeaders = await this.#getAuthHeaders();
473
+ const response = await fetch(url, { headers: authHeaders });
474
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TRADER_FOLLOWING_FAILED);
475
+ const followingData = (await response.json());
476
+ if (!is(followingData, FollowingResponseStruct)) {
477
+ throw new Error(SocialServiceErrorMessage.FETCH_TRADER_FOLLOWING_INVALID_RESPONSE);
478
+ }
479
+ return followingData;
480
+ },
481
+ });
482
+ return followingResponse;
483
+ }
412
484
  /**
413
485
  * Fetches a single position by its unique ID.
414
486
  *
@@ -427,7 +499,7 @@ export class SocialService extends BaseDataService {
427
499
  const authHeaders = await this.#getAuthHeaders();
428
500
  const response = await fetch(url, { headers: authHeaders });
429
501
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED);
430
- const positionData = await response.json();
502
+ const positionData = (await response.json());
431
503
  if (!is(positionData, PositionStruct)) {
432
504
  throw new Error(SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE);
433
505
  }
@@ -484,11 +556,8 @@ export class SocialService extends BaseDataService {
484
556
  headers: authHeaders,
485
557
  });
486
558
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_FEED_FAILED);
487
- const feedData = await response.json();
488
- if (!is(feedData, FeedResponseStruct)) {
489
- throw new Error(SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE);
490
- }
491
- return feedData;
559
+ const feedData = (await response.json());
560
+ return parseFeedResponse(feedData, SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE);
492
561
  },
493
562
  });
494
563
  return feedResponse;
@@ -541,11 +610,8 @@ export class SocialService extends BaseDataService {
541
610
  headers: authHeaders,
542
611
  });
543
612
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TRADER_FEED_FAILED);
544
- const feedData = await response.json();
545
- if (!is(feedData, FeedResponseStruct)) {
546
- throw new Error(SocialServiceErrorMessage.FETCH_TRADER_FEED_INVALID_RESPONSE);
547
- }
548
- return feedData;
613
+ const feedData = (await response.json());
614
+ return parseFeedResponse(feedData, SocialServiceErrorMessage.FETCH_TRADER_FEED_INVALID_RESPONSE);
549
615
  },
550
616
  });
551
617
  return traderFeedResponse;
@@ -600,10 +666,7 @@ export class SocialService extends BaseDataService {
600
666
  });
601
667
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TOKEN_FEED_FAILED);
602
668
  const feedData = (await response.json());
603
- if (!is(feedData, FeedResponseStruct)) {
604
- throw new Error(SocialServiceErrorMessage.FETCH_TOKEN_FEED_INVALID_RESPONSE);
605
- }
606
- return feedData;
669
+ return parseFeedResponse(feedData, SocialServiceErrorMessage.FETCH_TOKEN_FEED_INVALID_RESPONSE);
607
670
  },
608
671
  });
609
672
  return tokenFeedResponse;
@@ -633,7 +696,7 @@ export class SocialService extends BaseDataService {
633
696
  body: JSON.stringify({ emotion }),
634
697
  });
635
698
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.REACT_TO_COMMENT_FAILED);
636
- const metrics = await response.json();
699
+ const metrics = (await response.json());
637
700
  if (!is(metrics, CommentEngagementStruct)) {
638
701
  throw new Error(SocialServiceErrorMessage.REACT_TO_COMMENT_INVALID_RESPONSE);
639
702
  }
@@ -664,7 +727,7 @@ export class SocialService extends BaseDataService {
664
727
  headers: authHeaders,
665
728
  });
666
729
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.REMOVE_COMMENT_REACTION_FAILED);
667
- const metrics = await response.json();
730
+ const metrics = (await response.json());
668
731
  if (!is(metrics, CommentEngagementStruct)) {
669
732
  throw new Error(SocialServiceErrorMessage.REMOVE_COMMENT_REACTION_INVALID_RESPONSE);
670
733
  }
@@ -729,6 +792,10 @@ export class SocialService extends BaseDataService {
729
792
  * Calls `GET ${baseUrl}/users/me/following`. The caller is identified
730
793
  * server-side from the JWT sub claim carried in the Authorization header.
731
794
  *
795
+ * Prefer {@link fetchMyFollowing} in new code (pairs with
796
+ * {@link fetchMyFollowers}). This method is unchanged and remains the
797
+ * `/users/me/following` read.
798
+ *
732
799
  * @returns The following response.
733
800
  */
734
801
  async fetchFollowing() {
@@ -740,7 +807,7 @@ export class SocialService extends BaseDataService {
740
807
  const authHeaders = await this.#getAuthHeaders();
741
808
  const response = await fetch(url, { headers: authHeaders });
742
809
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_FOLLOWING_FAILED);
743
- const followingData = await response.json();
810
+ const followingData = (await response.json());
744
811
  if (!is(followingData, FollowingResponseStruct)) {
745
812
  throw new Error(SocialServiceErrorMessage.FETCH_FOLLOWING_INVALID_RESPONSE);
746
813
  }
@@ -749,6 +816,43 @@ export class SocialService extends BaseDataService {
749
816
  });
750
817
  return followingResponse;
751
818
  }
819
+ /**
820
+ * Fetches the list of traders the current user is following.
821
+ *
822
+ * Alias of {@link fetchFollowing}. Calls `GET ${baseUrl}/users/me/following`.
823
+ * Preferred name in new code so `/users/me` reads are both `fetchMy*`.
824
+ *
825
+ * @returns The following response.
826
+ */
827
+ async fetchMyFollowing() {
828
+ return this.fetchFollowing();
829
+ }
830
+ /**
831
+ * Fetches the list of traders following the current user.
832
+ *
833
+ * Calls `GET ${baseUrl}/users/me/followers`. The caller is identified
834
+ * server-side from the JWT sub claim carried in the Authorization header.
835
+ *
836
+ * @returns The followers response.
837
+ */
838
+ async fetchMyFollowers() {
839
+ const followersResponse = await this.fetchQuery({
840
+ queryKey: [`${this.name}:fetchMyFollowers`],
841
+ staleTime: 0,
842
+ queryFn: async () => {
843
+ const url = `${this.#v1Url}/users/me/followers`;
844
+ const authHeaders = await this.#getAuthHeaders();
845
+ const response = await fetch(url, { headers: authHeaders });
846
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_MY_FOLLOWERS_FAILED);
847
+ const followersData = (await response.json());
848
+ if (!is(followersData, FollowersResponseStruct)) {
849
+ throw new Error(SocialServiceErrorMessage.FETCH_MY_FOLLOWERS_INVALID_RESPONSE);
850
+ }
851
+ return followersData;
852
+ },
853
+ });
854
+ return followersResponse;
855
+ }
752
856
  /**
753
857
  * Follows one or more traders on behalf of the current user.
754
858
  *
@@ -773,7 +877,7 @@ export class SocialService extends BaseDataService {
773
877
  body: JSON.stringify({ targets }),
774
878
  });
775
879
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FOLLOW_FAILED);
776
- const followData = await response.json();
880
+ const followData = (await response.json());
777
881
  if (!is(followData, FollowResponseStruct)) {
778
882
  throw new Error(SocialServiceErrorMessage.FOLLOW_INVALID_RESPONSE);
779
883
  }
@@ -810,7 +914,7 @@ export class SocialService extends BaseDataService {
810
914
  headers: authHeaders,
811
915
  });
812
916
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.UNFOLLOW_FAILED);
813
- const unfollowData = await response.json();
917
+ const unfollowData = (await response.json());
814
918
  if (!is(unfollowData, UnfollowResponseStruct)) {
815
919
  throw new Error(SocialServiceErrorMessage.UNFOLLOW_INVALID_RESPONSE);
816
920
  }
@@ -1035,7 +1139,7 @@ export class SocialService extends BaseDataService {
1035
1139
  headers: authHeaders,
1036
1140
  });
1037
1141
  _a.#throwIfNotOk(response, failedMessage);
1038
- const positionsData = await response.json();
1142
+ const positionsData = (await response.json());
1039
1143
  if (!is(positionsData, PositionsResponseStruct)) {
1040
1144
  throw new Error(invalidMessage);
1041
1145
  }