From 5c3bdcf799e6a2a05c32b2314d9f008983f0afc0 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Tue, 28 Apr 2026 07:55:51 +0200 Subject: [PATCH 1/2] feat: adds getByPositionId --- .../src/SocialService-method-action-types.ts | 15 ++++ .../src/SocialService.test.ts | 79 +++++++++++++++++++ .../social-controllers/src/SocialService.ts | 38 +++++++++ packages/social-controllers/src/index.ts | 2 + .../src/social-constants.ts | 3 + .../social-controllers/src/social-types.ts | 5 ++ 6 files changed, 142 insertions(+) diff --git a/packages/social-controllers/src/SocialService-method-action-types.ts b/packages/social-controllers/src/SocialService-method-action-types.ts index 1c9b729e31f..6d3daa7605e 100644 --- a/packages/social-controllers/src/SocialService-method-action-types.ts +++ b/packages/social-controllers/src/SocialService-method-action-types.ts @@ -82,6 +82,20 @@ export type SocialServiceFetchFollowersAction = { handler: SocialService['fetchFollowers']; }; +/** + * Fetches a single position by its unique ID. + * + * Calls `GET ${baseUrl}/traders/position/${positionId}`. + * + * @param options - Options bag. + * @param options.positionId - Unique position ID (UUID). + * @returns The position. + */ +export type SocialServiceFetchPositionByIdAction = { + type: `SocialService:fetchPositionById`; + handler: SocialService['fetchPositionById']; +}; + /** * Fetches the list of traders the current user is following. * @@ -136,6 +150,7 @@ export type SocialServiceMethodActions = | SocialServiceFetchOpenPositionsAction | SocialServiceFetchClosedPositionsAction | SocialServiceFetchFollowersAction + | SocialServiceFetchPositionByIdAction | SocialServiceFetchFollowingAction | SocialServiceFollowAction | SocialServiceUnfollowAction; diff --git a/packages/social-controllers/src/SocialService.test.ts b/packages/social-controllers/src/SocialService.test.ts index a9c159ce811..43defcc5fcd 100644 --- a/packages/social-controllers/src/SocialService.test.ts +++ b/packages/social-controllers/src/SocialService.test.ts @@ -628,6 +628,85 @@ describe('SocialService', () => { }); }); + describe('fetchPositionById', () => { + it('fetches position from correct endpoint', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockPosition), + }); + + const service = createService(); + const result = await service.fetchPositionById({ + positionId: 'position-1', + }); + + expect(result).toStrictEqual(mockPosition); + expect(mockFetch).toHaveBeenCalledWith( + `${V1_URL}/traders/position/position-1`, + { headers: { Authorization: `Bearer ${MOCK_TOKEN}` } }, + ); + }); + + it('encodes the positionId in the URL', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockPosition), + }); + + const service = createService(); + await service.fetchPositionById({ positionId: 'pos/with/slashes' }); + + expect(mockFetch).toHaveBeenCalledWith( + `${V1_URL}/traders/position/pos%2Fwith%2Fslashes`, + { headers: { Authorization: `Bearer ${MOCK_TOKEN}` } }, + ); + }); + + it('throws HttpError on non-ok response', async () => { + mockFetch.mockResolvedValue({ ok: false, status: 404 }); + + const service = createService(); + + await expect( + service.fetchPositionById({ positionId: 'position-1' }), + ).rejects.toThrow( + `${SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED}: 404`, + ); + }); + + it('throws when response schema is invalid', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ positionId: 123 }), + }); + + const service = createService(); + + await expect( + service.fetchPositionById({ positionId: 'position-1' }), + ).rejects.toThrow( + SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE, + ); + }); + + it('returns cached result on repeated calls with same positionId', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockPosition), + }); + + const service = createService(); + await service.fetchPositionById({ positionId: 'position-1' }); + await service.fetchPositionById({ positionId: 'position-1' }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + }); + describe('fetchFollowing', () => { const mockFollowingResponse = { following: [mockProfileSummary], diff --git a/packages/social-controllers/src/SocialService.ts b/packages/social-controllers/src/SocialService.ts index 02efdfd5a4e..993897345ba 100644 --- a/packages/social-controllers/src/SocialService.ts +++ b/packages/social-controllers/src/SocialService.ts @@ -24,6 +24,7 @@ import { serviceName, SocialServiceErrorMessage } from './social-constants'; import type { FetchFollowersOptions, FetchLeaderboardOptions, + FetchPositionByIdOptions, FetchPositionsOptions, FetchTraderProfileOptions, FollowersResponse, @@ -31,6 +32,7 @@ import type { FollowOptions, FollowResponse, LeaderboardResponse, + Position, PositionsResponse, TraderProfileResponse, UnfollowOptions, @@ -174,6 +176,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'fetchClosedPositions', 'fetchFollowers', 'fetchFollowing', + 'fetchPositionById', 'follow', 'unfollow', ] as const; @@ -422,6 +425,41 @@ export class SocialService extends BaseDataService< return followersResponse; } + /** + * Fetches a single position by its unique ID. + * + * Calls `GET ${baseUrl}/traders/position/${positionId}`. + * + * @param options - Options bag. + * @param options.positionId - Unique position ID (UUID). + * @returns The position. + */ + async fetchPositionById(options: FetchPositionByIdOptions): Promise { + const { positionId } = options; + + const positionResponse = await this.fetchQuery({ + queryKey: [`${this.name}:fetchPositionById`, positionId], + queryFn: async () => { + const url = `${this.#v1Url}/traders/position/${encodeURIComponent(positionId)}`; + const authHeaders = await this.#getAuthHeaders(); + const response = await fetch(url, { headers: authHeaders }); + SocialService.#throwIfNotOk( + response, + SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED, + ); + const positionData = await response.json(); + if (!is(positionData, PositionStruct)) { + throw new Error( + SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE, + ); + } + return positionData as Position; + }, + }); + + return positionResponse; + } + /** * 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 af4661c3401..5d08d155669 100644 --- a/packages/social-controllers/src/index.ts +++ b/packages/social-controllers/src/index.ts @@ -31,6 +31,7 @@ export type { SocialServiceFetchFollowingAction, SocialServiceFetchLeaderboardAction, SocialServiceFetchOpenPositionsAction, + SocialServiceFetchPositionByIdAction, SocialServiceFetchTraderProfileAction, SocialServiceFollowAction, SocialServiceUnfollowAction, @@ -40,6 +41,7 @@ export { TradeStruct } from './social-types'; export type { FetchFollowersOptions, FetchLeaderboardOptions, + FetchPositionByIdOptions, FetchPositionsOptions, FetchTraderProfileOptions, FollowersResponse, diff --git a/packages/social-controllers/src/social-constants.ts b/packages/social-controllers/src/social-constants.ts index e3acf0003c3..8da17127e90 100644 --- a/packages/social-controllers/src/social-constants.ts +++ b/packages/social-controllers/src/social-constants.ts @@ -27,4 +27,7 @@ export const SocialServiceErrorMessage = { UNFOLLOW_FAILED: 'SocialService: Unfollow request failed', UNFOLLOW_INVALID_RESPONSE: 'SocialService: Unfollow returned invalid response', + FETCH_POSITION_BY_ID_FAILED: 'SocialService: Position request failed', + FETCH_POSITION_BY_ID_INVALID_RESPONSE: + 'SocialService: Position returned invalid response', } as const; diff --git a/packages/social-controllers/src/social-types.ts b/packages/social-controllers/src/social-types.ts index efa5fd12502..c7de2384fb9 100644 --- a/packages/social-controllers/src/social-types.ts +++ b/packages/social-controllers/src/social-types.ts @@ -242,6 +242,11 @@ export type FetchFollowersOptions = { addressOrId: string; }; +export type FetchPositionByIdOptions = { + /** Unique position ID (UUID). */ + positionId: string; +}; + export type FollowOptions = { /** Array of wallet addresses or profile IDs to follow. */ targets: string[]; From 495c91e2c0c764291eefee98e09b78d76465e960 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Tue, 28 Apr 2026 08:10:14 +0200 Subject: [PATCH 2/2] chore: lint and changelog --- packages/social-controllers/CHANGELOG.md | 4 ++++ packages/social-controllers/src/SocialService.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index 29651b6b974..09496f584df 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `SocialService.fetchPositionById` method exposing `GET /v1/traders/position/:positionId`, returning a single `Position` by ID ([#8602](https://github.com/MetaMask/core/pull/8602)) + ## [2.1.0] ### Added diff --git a/packages/social-controllers/src/SocialService.ts b/packages/social-controllers/src/SocialService.ts index 993897345ba..2442c37ecf2 100644 --- a/packages/social-controllers/src/SocialService.ts +++ b/packages/social-controllers/src/SocialService.ts @@ -434,7 +434,9 @@ export class SocialService extends BaseDataService< * @param options.positionId - Unique position ID (UUID). * @returns The position. */ - async fetchPositionById(options: FetchPositionByIdOptions): Promise { + async fetchPositionById( + options: FetchPositionByIdOptions, + ): Promise { const { positionId } = options; const positionResponse = await this.fetchQuery({