Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/social-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -136,6 +150,7 @@ export type SocialServiceMethodActions =
| SocialServiceFetchOpenPositionsAction
| SocialServiceFetchClosedPositionsAction
| SocialServiceFetchFollowersAction
| SocialServiceFetchPositionByIdAction
| SocialServiceFetchFollowingAction
| SocialServiceFollowAction
| SocialServiceUnfollowAction;
79 changes: 79 additions & 0 deletions packages/social-controllers/src/SocialService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
40 changes: 40 additions & 0 deletions packages/social-controllers/src/SocialService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import { serviceName, SocialServiceErrorMessage } from './social-constants';
import type {
FetchFollowersOptions,
FetchLeaderboardOptions,
FetchPositionByIdOptions,
FetchPositionsOptions,
FetchTraderProfileOptions,
FollowersResponse,
FollowingResponse,
FollowOptions,
FollowResponse,
LeaderboardResponse,
Position,
PositionsResponse,
TraderProfileResponse,
UnfollowOptions,
Expand Down Expand Up @@ -174,6 +176,7 @@ const MESSENGER_EXPOSED_METHODS = [
'fetchClosedPositions',
'fetchFollowers',
'fetchFollowing',
'fetchPositionById',
'follow',
'unfollow',
] as const;
Expand Down Expand Up @@ -422,6 +425,43 @@ 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<Position> {
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.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/social-controllers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type {
SocialServiceFetchFollowingAction,
SocialServiceFetchLeaderboardAction,
SocialServiceFetchOpenPositionsAction,
SocialServiceFetchPositionByIdAction,
SocialServiceFetchTraderProfileAction,
SocialServiceFollowAction,
SocialServiceUnfollowAction,
Expand All @@ -40,6 +41,7 @@ export { TradeStruct } from './social-types';
export type {
FetchFollowersOptions,
FetchLeaderboardOptions,
FetchPositionByIdOptions,
FetchPositionsOptions,
FetchTraderProfileOptions,
FollowersResponse,
Expand Down
3 changes: 3 additions & 0 deletions packages/social-controllers/src/social-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 5 additions & 0 deletions packages/social-controllers/src/social-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Loading