Skip to content

Commit 2e4ac7f

Browse files
feat: add perps order notifications (#6464)
## Explanation Adds perp order notification support. This is technically already supported here, but moving to core will reduce logic/complexity of setting up notifs. https://github.com/MetaMask/metamask-mobile/blob/3428d076e871e1ab7a68978bcc2da9770c4200b7/app/components/UI/Perps/controllers/PerpsController.ts#L697 ## 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 --> ## 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/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 91f4190 commit 2e4ac7f

File tree

11 files changed

+346
-1
lines changed

11 files changed

+346
-1
lines changed

packages/notification-services-controller/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `sendPerpPlaceOrderNotification` method to `NotificationServicesController` ([#6464](https://github.com/MetaMask/core/pull/6464))
13+
- Add `createPerpOrderNotification` function to invoke perp notification service ([#6464](https://github.com/MetaMask/core/pull/6464))
14+
- Add `perps/schema.ts` file from perp notification OpenAPI types ([#6464](https://github.com/MetaMask/core/pull/6464))
15+
- Add exported `OrderInput` type ([#6464](https://github.com/MetaMask/core/pull/6464))
16+
1017
### Changed
1118

1219
- Bump `@metamask/base-controller` from `^8.1.0` to `^8.3.0` ([#6355](https://github.com/MetaMask/core/pull/6355), [#6465](https://github.com/MetaMask/core/pull/6465))

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
mockGetOnChainNotifications,
1717
mockFetchFeatureAnnouncementNotifications,
1818
mockMarkNotificationsAsRead,
19+
mockCreatePerpNotification,
1920
} from './__fixtures__/mockServices';
2021
import { waitFor } from './__fixtures__/test-utils';
2122
import { TRIGGER_TYPES } from './constants';
@@ -38,7 +39,7 @@ import { processFeatureAnnouncement } from './processors';
3839
import { processNotification } from './processors/process-notifications';
3940
import { processSnapNotification } from './processors/process-snap-notifications';
4041
import { notificationsConfigCache } from './services/notification-config-cache';
41-
import type { INotification } from './types';
42+
import type { INotification, OrderInput } from './types';
4243
import type {
4344
NotificationServicesPushControllerDisablePushNotificationsAction,
4445
NotificationServicesPushControllerEnablePushNotificationsAction,
@@ -1159,6 +1160,82 @@ describe('metamask-notifications - disablePushNotifications', () => {
11591160
});
11601161
});
11611162

1163+
describe('metamask-notifications - sendPerpPlaceOrderNotification()', () => {
1164+
const arrangeMocks = () => {
1165+
const messengerMocks = mockNotificationMessenger();
1166+
const mockCreatePerpAPI = mockCreatePerpNotification({
1167+
status: 200,
1168+
body: { success: true },
1169+
});
1170+
return { ...messengerMocks, mockCreatePerpAPI };
1171+
};
1172+
1173+
const mockOrderInput: OrderInput = {
1174+
user_id: '0x111', // User Address
1175+
coin: '0x222', // Asset address
1176+
};
1177+
1178+
it('should successfully send perp order notification when authenticated', async () => {
1179+
const { messenger, mockCreatePerpAPI } = arrangeMocks();
1180+
const controller = new NotificationServicesController({
1181+
messenger,
1182+
env: { featureAnnouncements: featureAnnouncementsEnv },
1183+
});
1184+
1185+
await controller.sendPerpPlaceOrderNotification(mockOrderInput);
1186+
1187+
expect(mockCreatePerpAPI.isDone()).toBe(true);
1188+
});
1189+
1190+
it('should handle authentication errors gracefully', async () => {
1191+
const mocks = arrangeMocks();
1192+
mocks.mockIsSignedIn.mockReturnValue(false);
1193+
1194+
const controller = new NotificationServicesController({
1195+
messenger: mocks.messenger,
1196+
env: { featureAnnouncements: featureAnnouncementsEnv },
1197+
});
1198+
1199+
await controller.sendPerpPlaceOrderNotification(mockOrderInput);
1200+
1201+
expect(mocks.mockCreatePerpAPI.isDone()).toBe(false);
1202+
});
1203+
1204+
it('should handle bearer token retrieval errors gracefully', async () => {
1205+
const mocks = arrangeMocks();
1206+
mocks.mockGetBearerToken.mockRejectedValueOnce(
1207+
new Error('Failed to get bearer token'),
1208+
);
1209+
1210+
const controller = new NotificationServicesController({
1211+
messenger: mocks.messenger,
1212+
env: { featureAnnouncements: featureAnnouncementsEnv },
1213+
});
1214+
1215+
await controller.sendPerpPlaceOrderNotification(mockOrderInput);
1216+
1217+
expect(mocks.mockCreatePerpAPI.isDone()).toBe(false);
1218+
});
1219+
1220+
it('should handle API call failures gracefully', async () => {
1221+
const { messenger } = mockNotificationMessenger();
1222+
// Mock API to fail
1223+
const mockCreatePerpAPI = mockCreatePerpNotification({ status: 500 });
1224+
const mockConsoleError = jest
1225+
.spyOn(console, 'error')
1226+
.mockImplementation(jest.fn());
1227+
1228+
const controller = new NotificationServicesController({
1229+
messenger,
1230+
env: { featureAnnouncements: featureAnnouncementsEnv },
1231+
});
1232+
1233+
await controller.sendPerpPlaceOrderNotification(mockOrderInput);
1234+
expect(mockCreatePerpAPI.isDone()).toBe(true);
1235+
expect(mockConsoleError).toHaveBeenCalled();
1236+
});
1237+
});
1238+
11621239
// Type-Computation - we are extracting args and parameters from a generic type utility
11631240
// Thus this `AnyFunc` can be used to help constrain the generic parameters correctly
11641241
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ import {
2828
} from './processors/process-notifications';
2929
import * as FeatureNotifications from './services/feature-announcements';
3030
import * as OnChainNotifications from './services/onchain-notifications';
31+
import { createPerpOrderNotification } from './services/perp-notifications';
3132
import type {
3233
INotification,
3334
MarkAsReadNotificationsParam,
3435
} from './types/notification/notification';
3536
import type { OnChainRawNotification } from './types/on-chain-notification/on-chain-notification';
37+
import type { OrderInput } from './types/perps';
3638
import type {
3739
NotificationServicesPushControllerEnablePushNotificationsAction,
3840
NotificationServicesPushControllerDisablePushNotificationsAction,
@@ -451,6 +453,7 @@ export default class NotificationServicesController extends BaseController<
451453
subscribe: () => {
452454
this.messagingSystem.subscribe(
453455
'KeyringController:stateChange',
456+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
454457
async (totalAccounts, prevTotalAccounts) => {
455458
const hasTotalAccountsChanged = totalAccounts !== prevTotalAccounts;
456459
if (
@@ -1249,4 +1252,19 @@ export default class NotificationServicesController extends BaseController<
12491252
);
12501253
}
12511254
}
1255+
1256+
/**
1257+
* Creates an perp order notification subscription.
1258+
* Requires notifications and auth to be enabled to start receiving this notifications
1259+
*
1260+
* @param input perp input
1261+
*/
1262+
public async sendPerpPlaceOrderNotification(input: OrderInput) {
1263+
try {
1264+
const { bearerToken } = await this.#getBearerToken();
1265+
await createPerpOrderNotification(bearerToken, input);
1266+
} catch {
1267+
// Do Nothing
1268+
}
1269+
}
12521270
}

packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getMockFeatureAnnouncementResponse,
77
getMockListNotificationsResponse,
88
getMockMarkNotificationsAsReadResponse,
9+
getMockCreatePerpOrderNotification,
910
} from '../mocks/mockResponses';
1011

1112
type MockReply = {
@@ -70,3 +71,13 @@ export const mockMarkNotificationsAsRead = (mockReply?: MockReply) => {
7071

7172
return mockEndpoint;
7273
};
74+
75+
export const mockCreatePerpNotification = (mockReply?: MockReply) => {
76+
const mockResponse = getMockCreatePerpOrderNotification();
77+
const reply = mockReply ?? { status: 201 };
78+
const mockEndpoint = nock(mockResponse.url)
79+
.persist()
80+
.post('')
81+
.reply(reply.status);
82+
return mockEndpoint;
83+
};

packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TRIGGER_API_NOTIFICATIONS_ENDPOINT,
88
TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT,
99
} from '../services/onchain-notifications';
10+
import { PERPS_API_CREATE_ORDERS } from '../services/perp-notifications';
1011

1112
type MockResponse = {
1213
url: string;
@@ -58,3 +59,11 @@ export const getMockMarkNotificationsAsReadResponse = () => {
5859
response: null,
5960
} satisfies MockResponse;
6061
};
62+
63+
export const getMockCreatePerpOrderNotification = () => {
64+
return {
65+
url: PERPS_API_CREATE_ORDERS,
66+
requestMethod: 'POST',
67+
response: null,
68+
} satisfies MockResponse;
69+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createPerpOrderNotification } from './perp-notifications';
2+
import { mockCreatePerpNotification } from '../__fixtures__/mockServices';
3+
import type { OrderInput } from '../types/perps';
4+
5+
const mockOrderInput = (): OrderInput => ({
6+
user_id: '0x111', // User Address
7+
coin: '0x222', // Asset address
8+
});
9+
10+
const mockBearerToken = 'mock-jwt-token';
11+
12+
describe('Perps Service - createPerpOrderNotification', () => {
13+
beforeEach(() => {
14+
jest.clearAllMocks();
15+
});
16+
17+
const arrangeMocks = () => {
18+
const consoleErrorSpy = jest
19+
.spyOn(console, 'error')
20+
.mockImplementation(jest.fn());
21+
22+
return { consoleErrorSpy };
23+
};
24+
25+
it('should successfully create a perp order notification', async () => {
26+
const { consoleErrorSpy } = arrangeMocks();
27+
const mockEndpoint = mockCreatePerpNotification();
28+
await createPerpOrderNotification(mockBearerToken, mockOrderInput());
29+
30+
expect(mockEndpoint.isDone()).toBe(true);
31+
expect(consoleErrorSpy).not.toHaveBeenCalled();
32+
});
33+
34+
it('should handle fetch errors gracefully', async () => {
35+
const { consoleErrorSpy } = arrangeMocks();
36+
const mockEndpoint = mockCreatePerpNotification({ status: 500 });
37+
let numberOfRequests = 0;
38+
mockEndpoint.on('request', () => (numberOfRequests += 1));
39+
40+
await createPerpOrderNotification(mockBearerToken, mockOrderInput());
41+
42+
expect(mockEndpoint.isDone()).toBe(true);
43+
expect(consoleErrorSpy).toHaveBeenCalled();
44+
expect(numberOfRequests).toBe(4); // 4 requests made - 1 initial + 3 retries
45+
});
46+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
createServicePolicy,
3+
successfulFetch,
4+
} from '@metamask/controller-utils';
5+
6+
import type { OrderInput } from '../types';
7+
8+
export const PERPS_API = 'https://perps.api.cx.metamask.io';
9+
export const PERPS_API_CREATE_ORDERS = `${PERPS_API}/api/v1/orders`;
10+
11+
/**
12+
* Sends a perp order to our API to create a perp order subscription
13+
*
14+
* @param bearerToken - JWT for authentication
15+
* @param orderInput - order input shape
16+
*/
17+
export async function createPerpOrderNotification(
18+
bearerToken: string,
19+
orderInput: OrderInput,
20+
) {
21+
try {
22+
await createServicePolicy().execute(async () => {
23+
// console.log('called');
24+
return successfulFetch(PERPS_API_CREATE_ORDERS, {
25+
method: 'POST',
26+
headers: {
27+
'Content-Type': 'application/json',
28+
Authorization: `Bearer ${bearerToken}`,
29+
},
30+
body: JSON.stringify(orderInput),
31+
});
32+
});
33+
} catch (e) {
34+
console.error('Failed to create perp order notification', e);
35+
}
36+
}

packages/notification-services-controller/src/NotificationServicesController/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export type * from './feature-announcement';
22
export type * from './notification';
33
export type * from './on-chain-notification';
44
export type * from './snaps/snaps';
5+
export type * from './perps';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type * from './perp-types';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { components } from './schema';
2+
3+
export type OrderInput = components['schemas']['OrderInput'];

0 commit comments

Comments
 (0)