From 784acf7f05b9c302d17957f33ef52b5ef26f2c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos?= Date: Thu, 9 Jul 2026 14:38:20 +0200 Subject: [PATCH 1/5] feat: add trader-activity feed to social-controllers Add `fetchFeed` to `SocialService` (and the `SocialService:fetchFeed` messenger action) proxying the social-api `GET /feed` endpoint. - Supports a `scope` (`following` default, or `leaderboard`), `chains`, `limit`, and cursor pagination (`olderThan`/`newerThan`) for infinite scroll. - Adds `FeedItem`/`FeedPagination`/`FeedResponse`/`FetchFeedOptions` types; `FeedItem` reuses `Position` + `ProfileSummary`, and validation reuses `PositionStruct` via `assign`. - Adds a temporary `test` flag that serves the following feed for a fixed demo wallet until Clicker resolves feeds by profile id. --- packages/social-controllers/CHANGELOG.md | 5 + .../src/SocialService-method-action-types.ts | 17 ++ .../src/SocialService.test.ts | 151 ++++++++++++++++++ .../social-controllers/src/SocialService.ts | 95 +++++++++++ packages/social-controllers/src/index.ts | 5 + .../src/social-constants.ts | 2 + .../social-controllers/src/social-types.ts | 58 +++++++ 7 files changed, 333 insertions(+) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index a3f0bb9a25..b78a10da36 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `fetchFeed` method to `SocialService` (and the `SocialService:fetchFeed` messenger action) for the trader-activity feed. Calls `GET /feed` with an optional `scope` (`following`, default, personalized to the JWT-identified user; or `leaderboard`, the generic shared feed), `chains`, `limit`, and cursor pagination (`olderThan`/`newerThan`) for infinite scroll. Also accepts a temporary `test` flag that serves the `following` feed for a fixed demo wallet (bypassing the JWT profile) until Clicker resolves feeds by profile id ([#9354](https://github.com/MetaMask/core/pull/9354)) +- Add `FeedItem`, `FeedPagination`, `FeedResponse`, and `FetchFeedOptions` types. `FeedItem` extends `Position` with the trade's `actor` (`ProfileSummary`) and creation `timestamp`; `FeedPagination` exposes the `olderCursor`/`newerCursor` cursors ([#9354](https://github.com/MetaMask/core/pull/9354)) + ## [2.4.0] ### Added diff --git a/packages/social-controllers/src/SocialService-method-action-types.ts b/packages/social-controllers/src/SocialService-method-action-types.ts index 656883c7a4..cef6d46f89 100644 --- a/packages/social-controllers/src/SocialService-method-action-types.ts +++ b/packages/social-controllers/src/SocialService-method-action-types.ts @@ -109,6 +109,22 @@ export type SocialServiceFetchFollowingAction = { handler: SocialService['fetchFollowing']; }; +/** + * Fetches a page of the trader-activity feed. + * + * Calls `GET ${baseUrl}/feed`. For the `following` scope the current user is + * identified server-side from the JWT sub claim carried in the Authorization + * header; the `leaderboard` scope is generic and shared by all users. + * + * @param options - Optional feed query parameters (scope, chains, limit, and + * `olderThan`/`newerThan` pagination cursors). + * @returns The feed response with items and pagination cursors. + */ +export type SocialServiceFetchFeedAction = { + type: `SocialService:fetchFeed`; + handler: SocialService['fetchFeed']; +}; + /** * Follows one or more traders on behalf of the current user. * @@ -168,6 +184,7 @@ export type SocialServiceMethodActions = | SocialServiceFetchFollowersAction | SocialServiceFetchPositionByIdAction | SocialServiceFetchFollowingAction + | SocialServiceFetchFeedAction | SocialServiceFollowAction | SocialServiceUnfollowAction | SocialServiceOptOutOfLeaderboardAction diff --git a/packages/social-controllers/src/SocialService.test.ts b/packages/social-controllers/src/SocialService.test.ts index 2194cfd4ad..78eba84e8f 100644 --- a/packages/social-controllers/src/SocialService.test.ts +++ b/packages/social-controllers/src/SocialService.test.ts @@ -863,6 +863,157 @@ describe('SocialService', () => { }); }); + describe('fetchFeed', () => { + const mockFeedItem = { + ...mockPosition, + actor: mockProfileSummary, + timestamp: 1700000000, + }; + + const mockFeedResponse = { + items: [mockFeedItem], + pagination: { olderCursor: 'older-cursor', newerCursor: 'newer-cursor' }, + }; + + it('fetches the feed from the correct endpoint', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockFeedResponse), + }); + + const service = createService(); + const result = await service.fetchFeed(); + + expect(result).toStrictEqual(mockFeedResponse); + expect(mockFetch).toHaveBeenCalledWith(`${V1_URL}/feed`, { + headers: { Authorization: `Bearer ${MOCK_TOKEN}` }, + }); + }); + + it('appends scope, chains, limit, and pagination cursors', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockFeedResponse), + }); + + const service = createService(); + await service.fetchFeed({ + scope: 'leaderboard', + chains: ['base', 'solana'], + limit: 25, + olderThan: 'older-cursor', + newerThan: 'newer-cursor', + }); + + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).toContain('scope=leaderboard'); + expect(calledUrl).toContain('chains=base'); + expect(calledUrl).toContain('chains=solana'); + expect(calledUrl).toContain('limit=25'); + expect(calledUrl).toContain('olderThan=older-cursor'); + expect(calledUrl).toContain('newerThan=newer-cursor'); + }); + + it('appends test=true when test mode is enabled', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockFeedResponse), + }); + + const service = createService(); + await service.fetchFeed({ test: true }); + + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).toContain('test=true'); + }); + + it('omits the test param when test mode is off', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockFeedResponse), + }); + + const service = createService(); + await service.fetchFeed({ test: false }); + + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).not.toContain('test='); + }); + + it('validates a perp feed item', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + items: [ + { ...mockPerpPosition, actor: mockProfileSummary, timestamp: 1 }, + ], + pagination: { olderCursor: null, newerCursor: null }, + }), + }); + + const service = createService(); + const result = await service.fetchFeed(); + + expect(result.items).toHaveLength(1); + expect(result.pagination).toStrictEqual({ + olderCursor: null, + newerCursor: null, + }); + }); + + it('throws HttpError on non-ok response', async () => { + mockFetch.mockResolvedValue({ ok: false, status: 500 }); + + const service = createService(); + + await expect(service.fetchFeed()).rejects.toThrow( + `${SocialServiceErrorMessage.FETCH_FEED_FAILED}: 500`, + ); + }); + + it('throws when the feed item is missing its actor', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + items: [{ ...mockPosition, timestamp: 1700000000 }], + pagination: { olderCursor: null, newerCursor: null }, + }), + }); + + const service = createService(); + + await expect(service.fetchFeed()).rejects.toThrow( + SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE, + ); + }); + + it('throws when the pagination shape is invalid', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + items: [mockFeedItem], + pagination: { olderCursor: 123, newerCursor: null }, + }), + }); + + const service = createService(); + + await expect(service.fetchFeed()).rejects.toThrow( + SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE, + ); + }); + }); + describe('fetchFollowing', () => { const mockFollowingResponse = { following: [mockProfileSummary], diff --git a/packages/social-controllers/src/SocialService.ts b/packages/social-controllers/src/SocialService.ts index 3b85c82102..e7a3ffc2e3 100644 --- a/packages/social-controllers/src/SocialService.ts +++ b/packages/social-controllers/src/SocialService.ts @@ -10,6 +10,7 @@ import type { Messenger } from '@metamask/messenger'; import type { AuthenticationController } from '@metamask/profile-sync-controller'; import { array, + assign, boolean, enums, is, @@ -23,6 +24,8 @@ import { import { serviceName, SocialServiceErrorMessage } from './social-constants'; import type { + FeedResponse, + FetchFeedOptions, FetchFollowersOptions, FetchLeaderboardOptions, FetchPositionByIdOptions, @@ -154,6 +157,27 @@ const PositionsResponseStruct = structType({ computedAt: optional(nullable(string())), }); +// A feed item is a position plus the trader who made the trade (`actor`) and +// the item's creation timestamp. Reuses PositionStruct so the trade/position +// fields stay in lockstep with the positions endpoints. +const FeedItemStruct = assign( + PositionStruct, + structType({ + actor: ProfileSummaryStruct, + timestamp: number(), + }), +); + +const FeedPaginationStruct = structType({ + olderCursor: nullable(string()), + newerCursor: nullable(string()), +}); + +const FeedResponseStruct = structType({ + items: array(FeedItemStruct), + pagination: FeedPaginationStruct, +}); + const FollowersResponseStruct = structType({ followers: array(ProfileSummaryStruct), count: number(), @@ -184,6 +208,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'fetchFollowers', 'fetchFollowing', 'fetchPositionById', + 'fetchFeed', 'follow', 'unfollow', 'optOutOfLeaderboard', @@ -471,6 +496,76 @@ export class SocialService extends BaseDataService< return positionResponse; } + /** + * Fetches a page of the trader-activity feed. + * + * Calls `GET ${baseUrl}/feed`. For the `following` scope the current user is + * identified server-side from the JWT sub claim carried in the Authorization + * header; the `leaderboard` scope is generic and shared by all users. + * + * Cursor pagination supports infinite scroll: pass `pagination.olderCursor` + * from a prior response back as `olderThan` to load older items, and + * `pagination.newerCursor` as `newerThan` to fetch newer items. + * + * @param options - Options bag. + * @param options.scope - `following` (default) or `leaderboard`. + * @param options.chains - Filter by one or more chains. + * @param options.limit - Number of results per page. + * @param options.olderThan - Cursor for older items (scroll down). + * @param options.newerThan - Cursor for newer items (refresh). + * @param options.test - Test mode: serve the `following` feed for a fixed + * demo wallet instead of the JWT profile. Temporary until Clicker resolves + * feeds by profile id. + * @returns The feed response with items and pagination cursors. + */ + async fetchFeed(options?: FetchFeedOptions): Promise { + const feedResponse = await this.fetchQuery({ + queryKey: [`${this.name}:fetchFeed`, options ?? null], + staleTime: 0, + queryFn: async () => { + const { scope, chains, limit, olderThan, newerThan, test } = + options ?? {}; + const url = new URL(`${this.#v1Url}/feed`); + if (scope) { + url.searchParams.append('scope', scope); + } + if (chains) { + for (const chain of chains) { + url.searchParams.append('chains', chain); + } + } + if (limit !== undefined) { + url.searchParams.append('limit', String(limit)); + } + if (olderThan) { + url.searchParams.append('olderThan', olderThan); + } + if (newerThan) { + url.searchParams.append('newerThan', newerThan); + } + if (test) { + url.searchParams.append('test', 'true'); + } + + const authHeaders = await this.#getAuthHeaders(); + const response = await fetch(url.toString(), { + headers: authHeaders, + }); + SocialService.#throwIfNotOk( + response, + SocialServiceErrorMessage.FETCH_FEED_FAILED, + ); + const feedData = await response.json(); + if (!is(feedData, FeedResponseStruct)) { + throw new Error(SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE); + } + return feedData as FeedResponse; + }, + }); + + return feedResponse; + } + /** * Fetches the list of traders the current user is following. * diff --git a/packages/social-controllers/src/index.ts b/packages/social-controllers/src/index.ts index 9136ca157e..99c031d380 100644 --- a/packages/social-controllers/src/index.ts +++ b/packages/social-controllers/src/index.ts @@ -29,6 +29,7 @@ export { SocialService } from './SocialService'; export type { SocialServiceFetchClosedPositionsAction, + SocialServiceFetchFeedAction, SocialServiceFetchFollowersAction, SocialServiceFetchFollowingAction, SocialServiceFetchLeaderboardAction, @@ -43,6 +44,10 @@ export type { export { TradeStruct } from './social-types'; export type { + FeedItem, + FeedPagination, + FeedResponse, + FetchFeedOptions, FetchFollowersOptions, FetchLeaderboardOptions, FetchPositionByIdOptions, diff --git a/packages/social-controllers/src/social-constants.ts b/packages/social-controllers/src/social-constants.ts index 3a71e41bda..3eeb73c5b0 100644 --- a/packages/social-controllers/src/social-constants.ts +++ b/packages/social-controllers/src/social-constants.ts @@ -33,4 +33,6 @@ export const SocialServiceErrorMessage = { LEADERBOARD_OPT_OUT_FAILED: 'SocialService: Leaderboard opt-out request failed', LEADERBOARD_OPT_IN_FAILED: 'SocialService: Leaderboard opt-in request failed', + FETCH_FEED_FAILED: 'SocialService: Feed request failed', + FETCH_FEED_INVALID_RESPONSE: 'SocialService: Feed returned invalid response', } as const; diff --git a/packages/social-controllers/src/social-types.ts b/packages/social-controllers/src/social-types.ts index 168072cd5d..e02ea8e249 100644 --- a/packages/social-controllers/src/social-types.ts +++ b/packages/social-controllers/src/social-types.ts @@ -203,6 +203,40 @@ export type PositionsResponse = { computedAt?: string | null; }; +// --------------------------------------------------------------------------- +// Feed +// --------------------------------------------------------------------------- + +/** + * A single trader-activity feed item: a {@link Position} the trade belongs to, + * plus the {@link ProfileSummary} of the trader who made it (`actor`) and the + * item's creation `timestamp` (Unix seconds). + */ +export type FeedItem = Position & { + /** The trader who made this trade. */ + actor: ProfileSummary; + /** Unix timestamp (seconds) when the feed item was created. */ + timestamp: number; +}; + +/** + * Cursor pagination for the feed. Pass `olderCursor` back as `olderThan` to + * load older items (infinite scroll), and `newerCursor` as `newerThan` to + * fetch newer items. `null` when there are no items in that direction. + */ +export type FeedPagination = { + olderCursor: string | null; + newerCursor: string | null; +}; + +/** + * Response from `GET /v1/feed`. + */ +export type FeedResponse = { + items: FeedItem[]; + pagination: FeedPagination; +}; + // --------------------------------------------------------------------------- // Followers // --------------------------------------------------------------------------- @@ -274,6 +308,30 @@ export type FetchFollowersOptions = { addressOrId: string; }; +export type FetchFeedOptions = { + /** + * Which feed to fetch: `following` (personalized to the current user, + * identified server-side from the JWT) or `leaderboard` (generic, shared by + * all users). Defaults to `following` server-side when omitted. + */ + scope?: 'following' | 'leaderboard'; + /** Filter by one or more chains. Omit for the server defaults. */ + chains?: string[]; + /** Number of results to return. */ + limit?: number; + /** Cursor for older items (infinite scroll). Use `pagination.olderCursor`. */ + olderThan?: string; + /** Cursor for newer items (pull to refresh). Use `pagination.newerCursor`. */ + newerThan?: string; + /** + * Test mode. When `true`, the social-api ignores the JWT profile id and + * serves the `following` feed for a fixed demo wallet that has activity. + * Temporary until Clicker resolves feeds by profile id — remove once that + * ships. + */ + test?: boolean; +}; + export type FetchPositionByIdOptions = { /** Unique position ID (UUID). */ positionId: string; From c17000029dff81ce62f4186d3f3ae9152ca6505a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos?= Date: Thu, 9 Jul 2026 14:39:33 +0200 Subject: [PATCH 2/5] docs: reference PR number in social-controllers changelog --- packages/social-controllers/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index b78a10da36..3ec8be46ad 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `fetchFeed` method to `SocialService` (and the `SocialService:fetchFeed` messenger action) for the trader-activity feed. Calls `GET /feed` with an optional `scope` (`following`, default, personalized to the JWT-identified user; or `leaderboard`, the generic shared feed), `chains`, `limit`, and cursor pagination (`olderThan`/`newerThan`) for infinite scroll. Also accepts a temporary `test` flag that serves the `following` feed for a fixed demo wallet (bypassing the JWT profile) until Clicker resolves feeds by profile id ([#9354](https://github.com/MetaMask/core/pull/9354)) -- Add `FeedItem`, `FeedPagination`, `FeedResponse`, and `FetchFeedOptions` types. `FeedItem` extends `Position` with the trade's `actor` (`ProfileSummary`) and creation `timestamp`; `FeedPagination` exposes the `olderCursor`/`newerCursor` cursors ([#9354](https://github.com/MetaMask/core/pull/9354)) +- Add `fetchFeed` method to `SocialService` (and the `SocialService:fetchFeed` messenger action) for the trader-activity feed. Calls `GET /feed` with an optional `scope` (`following`, default, personalized to the JWT-identified user; or `leaderboard`, the generic shared feed), `chains`, `limit`, and cursor pagination (`olderThan`/`newerThan`) for infinite scroll. Also accepts a temporary `test` flag that serves the `following` feed for a fixed demo wallet (bypassing the JWT profile) until Clicker resolves feeds by profile id ([#9447](https://github.com/MetaMask/core/pull/9447)) +- Add `FeedItem`, `FeedPagination`, `FeedResponse`, and `FetchFeedOptions` types. `FeedItem` extends `Position` with the trade's `actor` (`ProfileSummary`) and creation `timestamp`; `FeedPagination` exposes the `olderCursor`/`newerCursor` cursors ([#9447](https://github.com/MetaMask/core/pull/9447)) ## [2.4.0] From ee508d7c1374b1b5cf338e44eb9c7cada0a5c0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos?= Date: Thu, 9 Jul 2026 16:46:11 +0200 Subject: [PATCH 3/5] refactor: drop feed test flag now that Clicker resolves by profile id Clicker's /v1/feed now accepts an `identifier` param, so the temporary `test` flag on `fetchFeed` is no longer needed. Remove it from `FetchFeedOptions` and the request builder. Also note that `chains` are CAIP-2 chain ids. --- packages/social-controllers/CHANGELOG.md | 2 +- .../src/SocialService.test.ts | 28 ------------------- .../social-controllers/src/SocialService.ts | 9 +----- .../social-controllers/src/social-types.ts | 12 +++----- 4 files changed, 6 insertions(+), 45 deletions(-) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index 3ec8be46ad..a5a92fb054 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `fetchFeed` method to `SocialService` (and the `SocialService:fetchFeed` messenger action) for the trader-activity feed. Calls `GET /feed` with an optional `scope` (`following`, default, personalized to the JWT-identified user; or `leaderboard`, the generic shared feed), `chains`, `limit`, and cursor pagination (`olderThan`/`newerThan`) for infinite scroll. Also accepts a temporary `test` flag that serves the `following` feed for a fixed demo wallet (bypassing the JWT profile) until Clicker resolves feeds by profile id ([#9447](https://github.com/MetaMask/core/pull/9447)) +- Add `fetchFeed` method to `SocialService` (and the `SocialService:fetchFeed` messenger action) for the trader-activity feed. Calls `GET /feed` with an optional `scope` (`following`, default, personalized to the JWT-identified user; or `leaderboard`, the generic shared feed), `chains` (as CAIP-2 chain ids), `limit`, and cursor pagination (`olderThan`/`newerThan`) for infinite scroll ([#9447](https://github.com/MetaMask/core/pull/9447)) - Add `FeedItem`, `FeedPagination`, `FeedResponse`, and `FetchFeedOptions` types. `FeedItem` extends `Position` with the trade's `actor` (`ProfileSummary`) and creation `timestamp`; `FeedPagination` exposes the `olderCursor`/`newerCursor` cursors ([#9447](https://github.com/MetaMask/core/pull/9447)) ## [2.4.0] diff --git a/packages/social-controllers/src/SocialService.test.ts b/packages/social-controllers/src/SocialService.test.ts index 78eba84e8f..35aaee3dd4 100644 --- a/packages/social-controllers/src/SocialService.test.ts +++ b/packages/social-controllers/src/SocialService.test.ts @@ -916,34 +916,6 @@ describe('SocialService', () => { expect(calledUrl).toContain('newerThan=newer-cursor'); }); - it('appends test=true when test mode is enabled', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - json: () => Promise.resolve(mockFeedResponse), - }); - - const service = createService(); - await service.fetchFeed({ test: true }); - - const calledUrl = mockFetch.mock.calls[0][0] as string; - expect(calledUrl).toContain('test=true'); - }); - - it('omits the test param when test mode is off', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - json: () => Promise.resolve(mockFeedResponse), - }); - - const service = createService(); - await service.fetchFeed({ test: false }); - - const calledUrl = mockFetch.mock.calls[0][0] as string; - expect(calledUrl).not.toContain('test='); - }); - it('validates a perp feed item', async () => { mockFetch.mockResolvedValue({ ok: true, diff --git a/packages/social-controllers/src/SocialService.ts b/packages/social-controllers/src/SocialService.ts index e7a3ffc2e3..f53cb28fe1 100644 --- a/packages/social-controllers/src/SocialService.ts +++ b/packages/social-controllers/src/SocialService.ts @@ -513,9 +513,6 @@ export class SocialService extends BaseDataService< * @param options.limit - Number of results per page. * @param options.olderThan - Cursor for older items (scroll down). * @param options.newerThan - Cursor for newer items (refresh). - * @param options.test - Test mode: serve the `following` feed for a fixed - * demo wallet instead of the JWT profile. Temporary until Clicker resolves - * feeds by profile id. * @returns The feed response with items and pagination cursors. */ async fetchFeed(options?: FetchFeedOptions): Promise { @@ -523,8 +520,7 @@ export class SocialService extends BaseDataService< queryKey: [`${this.name}:fetchFeed`, options ?? null], staleTime: 0, queryFn: async () => { - const { scope, chains, limit, olderThan, newerThan, test } = - options ?? {}; + const { scope, chains, limit, olderThan, newerThan } = options ?? {}; const url = new URL(`${this.#v1Url}/feed`); if (scope) { url.searchParams.append('scope', scope); @@ -543,9 +539,6 @@ export class SocialService extends BaseDataService< if (newerThan) { url.searchParams.append('newerThan', newerThan); } - if (test) { - url.searchParams.append('test', 'true'); - } const authHeaders = await this.#getAuthHeaders(); const response = await fetch(url.toString(), { diff --git a/packages/social-controllers/src/social-types.ts b/packages/social-controllers/src/social-types.ts index e02ea8e249..91d803e467 100644 --- a/packages/social-controllers/src/social-types.ts +++ b/packages/social-controllers/src/social-types.ts @@ -315,7 +315,10 @@ export type FetchFeedOptions = { * all users). Defaults to `following` server-side when omitted. */ scope?: 'following' | 'leaderboard'; - /** Filter by one or more chains. Omit for the server defaults. */ + /** + * Filter by one or more chains, given as CAIP-2 chain ids (e.g. + * `eip155:8453`). Omit for the server defaults. + */ chains?: string[]; /** Number of results to return. */ limit?: number; @@ -323,13 +326,6 @@ export type FetchFeedOptions = { olderThan?: string; /** Cursor for newer items (pull to refresh). Use `pagination.newerCursor`. */ newerThan?: string; - /** - * Test mode. When `true`, the social-api ignores the JWT profile id and - * serves the `following` feed for a fixed demo wallet that has activity. - * Temporary until Clicker resolves feeds by profile id — remove once that - * ships. - */ - test?: boolean; }; export type FetchPositionByIdOptions = { From 56ff233b6c6bde3ee4c3abc4b4d881466711621d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos?= Date: Fri, 10 Jul 2026 10:50:53 +0200 Subject: [PATCH 4/5] style: format SocialService.ts with prettier --- packages/social-controllers/src/SocialService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/social-controllers/src/SocialService.ts b/packages/social-controllers/src/SocialService.ts index f53cb28fe1..7fbd5c26a6 100644 --- a/packages/social-controllers/src/SocialService.ts +++ b/packages/social-controllers/src/SocialService.ts @@ -550,7 +550,9 @@ export class SocialService extends BaseDataService< ); const feedData = await response.json(); if (!is(feedData, FeedResponseStruct)) { - throw new Error(SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE); + throw new Error( + SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE, + ); } return feedData as FeedResponse; }, From f300e970d9d8753de1d1c755292bb015e3ecbeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos?= Date: Fri, 10 Jul 2026 12:10:22 +0200 Subject: [PATCH 5/5] chore: generate message action types --- .../src/SocialService-method-action-types.ts | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/social-controllers/src/SocialService-method-action-types.ts b/packages/social-controllers/src/SocialService-method-action-types.ts index cef6d46f89..5f6bcb217b 100644 --- a/packages/social-controllers/src/SocialService-method-action-types.ts +++ b/packages/social-controllers/src/SocialService-method-action-types.ts @@ -96,19 +96,6 @@ export type SocialServiceFetchPositionByIdAction = { handler: SocialService['fetchPositionById']; }; -/** - * Fetches the list of traders the current user is following. - * - * Calls `GET ${baseUrl}/users/me/following`. The caller is identified - * server-side from the JWT sub claim carried in the Authorization header. - * - * @returns The following response. - */ -export type SocialServiceFetchFollowingAction = { - type: `SocialService:fetchFollowing`; - handler: SocialService['fetchFollowing']; -}; - /** * Fetches a page of the trader-activity feed. * @@ -116,8 +103,16 @@ export type SocialServiceFetchFollowingAction = { * identified server-side from the JWT sub claim carried in the Authorization * header; the `leaderboard` scope is generic and shared by all users. * - * @param options - Optional feed query parameters (scope, chains, limit, and - * `olderThan`/`newerThan` pagination cursors). + * Cursor pagination supports infinite scroll: pass `pagination.olderCursor` + * from a prior response back as `olderThan` to load older items, and + * `pagination.newerCursor` as `newerThan` to fetch newer items. + * + * @param options - Options bag. + * @param options.scope - `following` (default) or `leaderboard`. + * @param options.chains - Filter by one or more chains. + * @param options.limit - Number of results per page. + * @param options.olderThan - Cursor for older items (scroll down). + * @param options.newerThan - Cursor for newer items (refresh). * @returns The feed response with items and pagination cursors. */ export type SocialServiceFetchFeedAction = { @@ -125,6 +120,19 @@ export type SocialServiceFetchFeedAction = { handler: SocialService['fetchFeed']; }; +/** + * Fetches the list of traders the current user is following. + * + * Calls `GET ${baseUrl}/users/me/following`. The caller is identified + * server-side from the JWT sub claim carried in the Authorization header. + * + * @returns The following response. + */ +export type SocialServiceFetchFollowingAction = { + type: `SocialService:fetchFollowing`; + handler: SocialService['fetchFollowing']; +}; + /** * Follows one or more traders on behalf of the current user. * @@ -183,8 +191,8 @@ export type SocialServiceMethodActions = | SocialServiceFetchClosedPositionsAction | SocialServiceFetchFollowersAction | SocialServiceFetchPositionByIdAction - | SocialServiceFetchFollowingAction | SocialServiceFetchFeedAction + | SocialServiceFetchFollowingAction | SocialServiceFollowAction | SocialServiceUnfollowAction | SocialServiceOptOutOfLeaderboardAction