Skip to content

Commit 12ba53a

Browse files
authored
feat: adds getByPositionId (MetaMask#8602)
## Explanation Expose GET /v1/traders/position/:positionId through SocialService as fetchPositionById, returning a single Position by ID. Reuses the existing Position type and PositionStruct validator; no new response types needed. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> - Related to https://consensyssoftware.atlassian.net/jira/software/c/projects/TSA/boards/3368?assignee=5b58c0f5eda3e92ca73222ee&selectedIssue=TSA-461 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Additive change that introduces a new read-only API wrapper method with schema validation and test coverage; minimal impact on existing flows. > > **Overview** > Adds `SocialService.fetchPositionById` to retrieve a single `Position` via `GET /v1/traders/position/:positionId`, including URL-encoding, caching via `fetchQuery`, and response validation with the existing `PositionStruct`. > > Exposes the method through messenger action types/exports, introduces `FetchPositionByIdOptions` plus new error messages, and adds unit tests + changelog entry for the new endpoint wrapper. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 495c91e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 604f3d8 commit 12ba53a

7 files changed

Lines changed: 148 additions & 0 deletions

File tree

packages/social-controllers/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `SocialService.fetchPositionById` method exposing `GET /v1/traders/position/:positionId`, returning a single `Position` by ID ([#8602](https://github.com/MetaMask/core/pull/8602))
13+
1014
## [2.1.0]
1115

1216
### Added

packages/social-controllers/src/SocialService-method-action-types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ export type SocialServiceFetchFollowersAction = {
8282
handler: SocialService['fetchFollowers'];
8383
};
8484

85+
/**
86+
* Fetches a single position by its unique ID.
87+
*
88+
* Calls `GET ${baseUrl}/traders/position/${positionId}`.
89+
*
90+
* @param options - Options bag.
91+
* @param options.positionId - Unique position ID (UUID).
92+
* @returns The position.
93+
*/
94+
export type SocialServiceFetchPositionByIdAction = {
95+
type: `SocialService:fetchPositionById`;
96+
handler: SocialService['fetchPositionById'];
97+
};
98+
8599
/**
86100
* Fetches the list of traders the current user is following.
87101
*
@@ -136,6 +150,7 @@ export type SocialServiceMethodActions =
136150
| SocialServiceFetchOpenPositionsAction
137151
| SocialServiceFetchClosedPositionsAction
138152
| SocialServiceFetchFollowersAction
153+
| SocialServiceFetchPositionByIdAction
139154
| SocialServiceFetchFollowingAction
140155
| SocialServiceFollowAction
141156
| SocialServiceUnfollowAction;

packages/social-controllers/src/SocialService.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,85 @@ describe('SocialService', () => {
628628
});
629629
});
630630

631+
describe('fetchPositionById', () => {
632+
it('fetches position from correct endpoint', async () => {
633+
mockFetch.mockResolvedValue({
634+
ok: true,
635+
status: 200,
636+
json: () => Promise.resolve(mockPosition),
637+
});
638+
639+
const service = createService();
640+
const result = await service.fetchPositionById({
641+
positionId: 'position-1',
642+
});
643+
644+
expect(result).toStrictEqual(mockPosition);
645+
expect(mockFetch).toHaveBeenCalledWith(
646+
`${V1_URL}/traders/position/position-1`,
647+
{ headers: { Authorization: `Bearer ${MOCK_TOKEN}` } },
648+
);
649+
});
650+
651+
it('encodes the positionId in the URL', async () => {
652+
mockFetch.mockResolvedValue({
653+
ok: true,
654+
status: 200,
655+
json: () => Promise.resolve(mockPosition),
656+
});
657+
658+
const service = createService();
659+
await service.fetchPositionById({ positionId: 'pos/with/slashes' });
660+
661+
expect(mockFetch).toHaveBeenCalledWith(
662+
`${V1_URL}/traders/position/pos%2Fwith%2Fslashes`,
663+
{ headers: { Authorization: `Bearer ${MOCK_TOKEN}` } },
664+
);
665+
});
666+
667+
it('throws HttpError on non-ok response', async () => {
668+
mockFetch.mockResolvedValue({ ok: false, status: 404 });
669+
670+
const service = createService();
671+
672+
await expect(
673+
service.fetchPositionById({ positionId: 'position-1' }),
674+
).rejects.toThrow(
675+
`${SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED}: 404`,
676+
);
677+
});
678+
679+
it('throws when response schema is invalid', async () => {
680+
mockFetch.mockResolvedValue({
681+
ok: true,
682+
status: 200,
683+
json: () => Promise.resolve({ positionId: 123 }),
684+
});
685+
686+
const service = createService();
687+
688+
await expect(
689+
service.fetchPositionById({ positionId: 'position-1' }),
690+
).rejects.toThrow(
691+
SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE,
692+
);
693+
});
694+
695+
it('returns cached result on repeated calls with same positionId', async () => {
696+
mockFetch.mockResolvedValue({
697+
ok: true,
698+
status: 200,
699+
json: () => Promise.resolve(mockPosition),
700+
});
701+
702+
const service = createService();
703+
await service.fetchPositionById({ positionId: 'position-1' });
704+
await service.fetchPositionById({ positionId: 'position-1' });
705+
706+
expect(mockFetch).toHaveBeenCalledTimes(1);
707+
});
708+
});
709+
631710
describe('fetchFollowing', () => {
632711
const mockFollowingResponse = {
633712
following: [mockProfileSummary],

packages/social-controllers/src/SocialService.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import { serviceName, SocialServiceErrorMessage } from './social-constants';
2424
import type {
2525
FetchFollowersOptions,
2626
FetchLeaderboardOptions,
27+
FetchPositionByIdOptions,
2728
FetchPositionsOptions,
2829
FetchTraderProfileOptions,
2930
FollowersResponse,
3031
FollowingResponse,
3132
FollowOptions,
3233
FollowResponse,
3334
LeaderboardResponse,
35+
Position,
3436
PositionsResponse,
3537
TraderProfileResponse,
3638
UnfollowOptions,
@@ -174,6 +176,7 @@ const MESSENGER_EXPOSED_METHODS = [
174176
'fetchClosedPositions',
175177
'fetchFollowers',
176178
'fetchFollowing',
179+
'fetchPositionById',
177180
'follow',
178181
'unfollow',
179182
] as const;
@@ -422,6 +425,43 @@ export class SocialService extends BaseDataService<
422425
return followersResponse;
423426
}
424427

428+
/**
429+
* Fetches a single position by its unique ID.
430+
*
431+
* Calls `GET ${baseUrl}/traders/position/${positionId}`.
432+
*
433+
* @param options - Options bag.
434+
* @param options.positionId - Unique position ID (UUID).
435+
* @returns The position.
436+
*/
437+
async fetchPositionById(
438+
options: FetchPositionByIdOptions,
439+
): Promise<Position> {
440+
const { positionId } = options;
441+
442+
const positionResponse = await this.fetchQuery({
443+
queryKey: [`${this.name}:fetchPositionById`, positionId],
444+
queryFn: async () => {
445+
const url = `${this.#v1Url}/traders/position/${encodeURIComponent(positionId)}`;
446+
const authHeaders = await this.#getAuthHeaders();
447+
const response = await fetch(url, { headers: authHeaders });
448+
SocialService.#throwIfNotOk(
449+
response,
450+
SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED,
451+
);
452+
const positionData = await response.json();
453+
if (!is(positionData, PositionStruct)) {
454+
throw new Error(
455+
SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE,
456+
);
457+
}
458+
return positionData as Position;
459+
},
460+
});
461+
462+
return positionResponse;
463+
}
464+
425465
/**
426466
* Fetches the list of traders the current user is following.
427467
*

packages/social-controllers/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type {
3131
SocialServiceFetchFollowingAction,
3232
SocialServiceFetchLeaderboardAction,
3333
SocialServiceFetchOpenPositionsAction,
34+
SocialServiceFetchPositionByIdAction,
3435
SocialServiceFetchTraderProfileAction,
3536
SocialServiceFollowAction,
3637
SocialServiceUnfollowAction,
@@ -40,6 +41,7 @@ export { TradeStruct } from './social-types';
4041
export type {
4142
FetchFollowersOptions,
4243
FetchLeaderboardOptions,
44+
FetchPositionByIdOptions,
4345
FetchPositionsOptions,
4446
FetchTraderProfileOptions,
4547
FollowersResponse,

packages/social-controllers/src/social-constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ export const SocialServiceErrorMessage = {
2727
UNFOLLOW_FAILED: 'SocialService: Unfollow request failed',
2828
UNFOLLOW_INVALID_RESPONSE:
2929
'SocialService: Unfollow returned invalid response',
30+
FETCH_POSITION_BY_ID_FAILED: 'SocialService: Position request failed',
31+
FETCH_POSITION_BY_ID_INVALID_RESPONSE:
32+
'SocialService: Position returned invalid response',
3033
} as const;

packages/social-controllers/src/social-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ export type FetchFollowersOptions = {
242242
addressOrId: string;
243243
};
244244

245+
export type FetchPositionByIdOptions = {
246+
/** Unique position ID (UUID). */
247+
positionId: string;
248+
};
249+
245250
export type FollowOptions = {
246251
/** Array of wallet addresses or profile IDs to follow. */
247252
targets: string[];

0 commit comments

Comments
 (0)