From e7cc442b2939ec973d31a513c629f79acb6f0c5e Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 10:20:32 +0100 Subject: [PATCH 1/7] chore: generate new notifications api schema --- .../types/notification-api/schema.ts | 394 +++++++++++++++++- 1 file changed, 388 insertions(+), 6 deletions(-) diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts index 84c085a9d5..12276176d1 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts @@ -5,10 +5,107 @@ /** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. - * Script: `npx openapi-typescript -o ./schema.d.ts` + * Script: `npx openapi-typescript -o ./schema.ts` */ -export type paths = { +export interface paths { + '/api/v4/notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List both platform and on-chain notifications for a certain user/address(es) + * @description Same behaviour as /api/v3/notifications, but the returned notification_type and notification_subtype are taken directly from the producer-set database fields: platform notifications expose platform_notifications.notification_type / notification_subtype, while on-chain notifications expose the constant "wallet_activity" as notification_type and notifications_part.kind as notification_subtype. Clients should distinguish the two shapes structurally (presence of "payload" for on-chain vs "template" for platform). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['NotificationInputV3']; + }; + }; + responses: { + /** @description Notifications listed successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['NotificationOutputV4']; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v4/notifications/mark-as-read': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Mark notifications as read */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + ids?: string[]; + }; + }; + }; + responses: { + /** @description Successfully marked notifications as read */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v3/notifications': { parameters: { query?: never; @@ -103,15 +200,204 @@ export type paths = { patch?: never; trace?: never; }; -}; + '/api/v2/notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** List all notifications for a certain user/address(es) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['NotificationInput'][]; + }; + }; + responses: { + /** @description Notifications listed successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WalletNotification'][]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v2/notifications/mark-as-read': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Mark notifications as read */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + ids?: string[]; + }; + }; + }; + responses: { + /** @description Successfully marked notifications as read */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** List all notifications ordered by most recent */ + post: { + parameters: { + query?: { + /** @description Page number for pagination */ + page?: number; + /** @description Number of notifications per page for pagination */ + per_page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + 'application/json': { + trigger_ids: string[]; + chain_ids?: number[]; + kinds?: string[]; + unread?: boolean; + }; + }; + }; + responses: { + /** @description Successfully fetched a list of notifications */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WalletNotification'][]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/notifications/mark-as-read': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Mark notifications as read */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + ids?: string[]; + }; + }; + }; + responses: { + /** @description Successfully marked notifications as read */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} export type webhooks = Record; -export type components = { +export interface components { schemas: { + /** + * @example mobile + * @enum {string} + */ + AppPlatform: 'portfolio' | 'extension' | 'mobile'; NotificationInputV3: { /** @example en-US */ locale: string; + platform: components['schemas']['AppPlatform']; addresses: string[]; - platform: 'extension' | 'mobile'; }; NotificationOutputV3: ( | components['schemas']['PlatformNotification'] @@ -125,6 +411,8 @@ export type components = { id: string; /** @enum {string} */ notification_type: 'platform'; + /** @example position_liquidated */ + notification_subtype: string; /** @example false */ unread: boolean; template: components['schemas']['LocalizedNotification']; @@ -188,6 +476,100 @@ export type components = { | components['schemas']['Data_ERC1155Sent'] | components['schemas']['Data_ERC1155Received']; }; + /** @description A heterogeneous list of platform and on-chain notifications. The two shapes are discriminated by `notification_type`: a value of "wallet_activity" identifies an OnChainNotificationV4, any other value identifies a PlatformNotificationV4. "wallet_activity" is a reserved value and MUST NOT be used as a producer-set platform notification_type. (This cannot be modelled as a formal OpenAPI discriminator because the platform notification_type set is open-ended.) */ + NotificationOutputV4: ( + | components['schemas']['PlatformNotificationV4'] + | components['schemas']['OnChainNotificationV4'] + )[]; + PlatformNotificationV4: { + /** + * Format: uuid + * @example 3fa85f64-5717-4562-b3fc-2c963f66afa6 + */ + id: string; + /** + * @description Producer-set platform_notifications.notification_type value. + * @example perps + */ + notification_type: string; + /** + * @description Producer-set platform_notifications.notification_subtype value. + * @example position_liquidated + */ + notification_subtype: string; + /** @example false */ + unread: boolean; + template: components['schemas']['LocalizedNotification']; + /** + * Format: date-time + * @example 2025-10-09T09:45:34.202Z + */ + created_at: string; + }; + OnChainNotificationV4: { + /** + * Format: uuid + * @example 3fa85f64-5717-4562-b3fc-2c963f66afa6 + */ + id: string; + /** @enum {string} */ + notification_type: 'wallet_activity'; + /** + * @description notifications_part.kind value. + * @example metamask_swap_completed + */ + notification_subtype: string; + /** @example false */ + unread: boolean; + /** + * Format: date-time + * @example 2025-10-09T09:45:34.202Z + */ + created_at: string; + payload: components['schemas']['OnChainPayload']; + }; + NotificationInput: { + /** Format: address */ + address: string; + }; + WalletNotification: { + /** Format: uuid */ + id: string; + /** Format: uuid */ + trigger_id: string; + /** @example 1 */ + chain_id: number; + /** @example 17485840 */ + block_number: number; + block_timestamp: string; + /** + * Format: address + * @example 0x881D40237659C251811CEC9c364ef91dC08D300C + */ + tx_hash: string; + /** @example false */ + unread: boolean; + /** Format: date-time */ + created_at: string; + /** Format: address */ + address: string; + data?: + | components['schemas']['Data_MetamaskSwapCompleted'] + | components['schemas']['Data_LidoStakeReadyToBeWithdrawn'] + | components['schemas']['Data_LidoStakeCompleted'] + | components['schemas']['Data_LidoWithdrawalRequested'] + | components['schemas']['Data_LidoWithdrawalCompleted'] + | components['schemas']['Data_RocketPoolStakeCompleted'] + | components['schemas']['Data_RocketPoolUnstakeCompleted'] + | components['schemas']['Data_ETHSent'] + | components['schemas']['Data_ETHReceived'] + | components['schemas']['Data_ERC20Sent'] + | components['schemas']['Data_ERC20Received'] + | components['schemas']['Data_ERC721Sent'] + | components['schemas']['Data_ERC721Received'] + | components['schemas']['Data_ERC1155Sent'] + | components['schemas']['Data_ERC1155Received']; + }; Data_MetamaskSwapCompleted: { /** @enum {string} */ kind: 'metamask_swap_completed'; @@ -405,6 +787,6 @@ export type components = { requestBodies: never; headers: never; pathItems: never; -}; +} export type $defs = Record; export type operations = Record; From a6cba9f5321d881858f177c1085deccc271a0993 Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 10:21:19 +0100 Subject: [PATCH 2/7] feat: upgrade notification-services-controller to v4 notifications api --- .../services/api-notifications.ts | 10 ++++---- .../notification-api/notification-api.ts | 12 +++------- .../src/shared/index.ts | 1 + .../shared/notification-api-type-guards.ts | 23 ++++++++++++++++++ .../src/shared/to-raw-notification.ts | 24 ++++++------------- 5 files changed, 39 insertions(+), 31 deletions(-) create mode 100644 packages/notification-services-controller/src/shared/notification-api-type-guards.ts diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts index 14276ebb99..f4cedf5435 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts @@ -36,12 +36,12 @@ export const TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT = ( // Lists notifications for each address provided export const NOTIFICATION_API_LIST_ENDPOINT = (env: ENV = 'prd'): string => - `${NOTIFICATION_API(env)}/api/v3/notifications`; + `${NOTIFICATION_API(env)}/api/v4/notifications`; // Marks notifications as read export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = ( env: ENV = 'prd', -): string => `${NOTIFICATION_API(env)}/api/v3/notifications/mark-as-read`; +): string => `${NOTIFICATION_API(env)}/api/v4/notifications/mark-as-read`; /** * fetches notification config (accounts enabled vs disabled) @@ -112,9 +112,9 @@ export async function getAPINotifications( } type RequestBody = - Schema.paths['/api/v3/notifications']['post']['requestBody']['content']['application/json']; + Schema.paths['/api/v4/notifications']['post']['requestBody']['content']['application/json']; type APIResponse = - Schema.paths['/api/v3/notifications']['post']['responses']['200']['content']['application/json']; + Schema.paths['/api/v4/notifications']['post']['responses']['200']['content']['application/json']; const body: RequestBody = { addresses: addresses.map((addr) => addr.toLowerCase()), @@ -170,7 +170,7 @@ export async function markNotificationsAsRead( } type ResponseBody = - Schema.paths['/api/v3/notifications/mark-as-read']['post']['requestBody']['content']['application/json']; + Schema.paths['/api/v4/notifications/mark-as-read']['post']['requestBody']['content']['application/json']; const body: ResponseBody = { ids: notificationIds, }; diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts index 86547285e0..3bb9159ef1 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts @@ -27,15 +27,9 @@ export type Data_ERC721Received = components['schemas']['Data_ERC721Received']; export type NetworkMetadata = components['schemas']['NetworkMetadata']; export type BlockExplorer = components['schemas']['BlockExplorer']; -type Notification = components['schemas']['NotificationOutputV3'][number]; -type PlatformNotification = Extract< - Notification, - { notification_type: 'platform' } ->; -type OnChainNotification = Extract< - Notification, - { notification_type: 'on-chain' } ->; +export type Notification = components['schemas']['NotificationOutputV4'][number]; +export type PlatformNotification = components['schemas']['PlatformNotificationV4']; +export type OnChainNotification = components['schemas']['OnChainNotificationV4']; type ConvertToEnum = { [K in TRIGGER_TYPES]: Kind extends `${K}` ? K : never; diff --git a/packages/notification-services-controller/src/shared/index.ts b/packages/notification-services-controller/src/shared/index.ts index bebb282354..342d8287ed 100644 --- a/packages/notification-services-controller/src/shared/index.ts +++ b/packages/notification-services-controller/src/shared/index.ts @@ -1,2 +1,3 @@ export * from './is-onchain-notification'; +export * from './notification-api-type-guards'; export * from './to-raw-notification'; diff --git a/packages/notification-services-controller/src/shared/notification-api-type-guards.ts b/packages/notification-services-controller/src/shared/notification-api-type-guards.ts new file mode 100644 index 0000000000..a5981e58ab --- /dev/null +++ b/packages/notification-services-controller/src/shared/notification-api-type-guards.ts @@ -0,0 +1,23 @@ +import type { + Notification, + OnChainNotification, + PlatformNotification, +} from '../NotificationServicesController/types/notification-api'; + +/** + * Narrows a v4 API notification to an on-chain notification. + */ +export function isOnChainNotification( + notification: Notification, +): notification is OnChainNotification { + return notification.notification_type === 'wallet_activity'; +} + +/** + * Narrows a v4 API notification to a platform notification. + */ +export function isPlatformNotification( + notification: Notification, +): notification is PlatformNotification { + return !isOnChainNotification(notification); +} diff --git a/packages/notification-services-controller/src/shared/to-raw-notification.ts b/packages/notification-services-controller/src/shared/to-raw-notification.ts index 4894c8f48b..ed88b96444 100644 --- a/packages/notification-services-controller/src/shared/to-raw-notification.ts +++ b/packages/notification-services-controller/src/shared/to-raw-notification.ts @@ -4,6 +4,7 @@ import type { OnChainRawNotification, PlatformRawNotification, } from 'src/NotificationServicesController/types/notification-api'; +import { isOnChainNotification } from './notification-api-type-guards'; /** * A true "raw notification" does not have some fields that exist on this type. E.g. the `type` field. @@ -16,15 +17,8 @@ import type { export function toRawAPINotification( data: UnprocessedRawNotification, ): NormalisedAPINotification { - const exhaustedAllCases = (_: never): never => { - const type: string = data?.notification_type; - throw new Error( - `toRawAPINotification - No processor found for notification kind ${type}`, - ); - }; - - if (data.notification_type === 'on-chain') { - if (!data?.payload?.data?.kind) { + if (isOnChainNotification(data)) { + if (!data.payload.data?.kind) { throw new Error( 'toRawAPINotification - No kind found for on-chain notification', ); @@ -35,12 +29,8 @@ export function toRawAPINotification( } as OnChainRawNotification; } - if (data.notification_type === 'platform') { - return { - ...data, - type: data.notification_type, - } as PlatformRawNotification; - } - - return exhaustedAllCases(data); + return { + ...data, + type: data.notification_type, + } as PlatformRawNotification; } From 1db01e46fd5565a71967184b08b70c1f4f46b6ab Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 10:25:03 +0100 Subject: [PATCH 3/7] chore: update changelog --- .../CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index b20f315b4e..f6b564b4d4 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export `isOnChainNotification` and `isPlatformNotification` type guards for discriminating v4 API notification shapes +- Export `Notification`, `PlatformNotification`, and `OnChainNotification` types derived from the v4 Notification API schema + +### Changed + +- **BREAKING:** Moved Notification API from v3 to v4 + - API Endpoint Changes: Updated from `/api/v3/notifications` to `/api/v4/notifications` for listing notifications and marking as read + - Response Structure: `notification_type` and `notification_subtype` now reflect producer-set database fields instead of fixed enum values + - On-chain notifications: `notification_type` is now `"wallet_activity"` (was `"on-chain"`), with `notification_subtype` set to the on-chain kind (e.g. `"metamask_swap_completed"`) + - Platform notifications: `notification_type` is now a producer-set value (e.g. `"perps"`, was `"platform"`), with `notification_subtype` set to the platform subtype (e.g. `"position_liquidated"`) + - Clients should use the `isOnChainNotification` / `isPlatformNotification` type guards to distinguish on-chain vs platform notifications + - Type System: + - `UnprocessedRawNotification` now uses `NotificationOutputV4` shapes (`PlatformNotificationV4` | `OnChainNotificationV4`) + - `toRawAPINotification()` now normalises v4 notifications, mapping on-chain `notification_subtype` to the `type` field + - Regenerated `schema.ts` from the latest Notification API OpenAPI spec, including v4 paths and legacy v1–v3 schemas + - `AppPlatform` now includes `"portfolio"` in addition to `"extension"` and `"mobile"` + ## [24.3.0] ### Added From 5a641e677314e32a821f0251f524606636af5e3a Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 10:50:17 +0100 Subject: [PATCH 4/7] fix: set platform trigger type and update notification mocks for v4 --- .../mocks/mock-raw-notifications.ts | 48 ++++++++++++------- .../notification-api/notification-api.ts | 15 +++--- .../src/shared/to-raw-notification.ts | 3 +- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/packages/notification-services-controller/src/NotificationServicesController/mocks/mock-raw-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/mocks/mock-raw-notifications.ts index 68e57b71c3..01f2bb3396 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/mocks/mock-raw-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/mocks/mock-raw-notifications.ts @@ -9,7 +9,8 @@ import type { NormalisedAPINotification } from '../types/notification-api/notifi export function createMockNotificationEthSent(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ETH_SENT, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'eth_sent', id: '3fa85f64-5717-4562-b3fc-2c963f66afa7', unread: true, created_at: '2022-03-01T00:00:00Z', @@ -55,7 +56,8 @@ export function createMockNotificationEthSent(): NormalisedAPINotification { export function createMockNotificationEthReceived(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ETH_RECEIVED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'eth_received', id: '3fa85f64-5717-4562-b3fc-2c963f66afa8', unread: true, created_at: '2022-03-01T00:00:00Z', @@ -101,7 +103,8 @@ export function createMockNotificationEthReceived(): NormalisedAPINotification { export function createMockNotificationERC20Sent(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC20_SENT, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'erc20_sent', id: '3fa85f64-5717-4562-b3fc-2c963f66afa9', unread: true, created_at: '2022-03-01T00:00:00Z', @@ -153,7 +156,8 @@ export function createMockNotificationERC20Sent(): NormalisedAPINotification { export function createMockNotificationERC20Received(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC20_RECEIVED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'erc20_received', id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', unread: true, created_at: '2022-03-01T00:00:00Z', @@ -205,7 +209,8 @@ export function createMockNotificationERC20Received(): NormalisedAPINotification export function createMockNotificationERC721Sent(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC721_SENT, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'erc721_sent', id: 'a4193058-9814-537e-9df4-79dcac727fb6', created_at: '2023-11-15T11:08:17.895407Z', unread: true, @@ -260,7 +265,8 @@ export function createMockNotificationERC721Sent(): NormalisedAPINotification { export function createMockNotificationERC721Received(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC721_RECEIVED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'erc721_received', id: '00a79d24-befa-57ed-a55a-9eb8696e1654', created_at: '2023-11-14T17:40:52.319281Z', unread: true, @@ -315,7 +321,8 @@ export function createMockNotificationERC721Received(): NormalisedAPINotificatio export function createMockNotificationERC1155Sent(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC1155_SENT, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'erc1155_sent', id: 'a09ff9d1-623a-52ab-a3d4-c7c8c9a58362', created_at: '2023-11-20T20:44:10.110706Z', unread: true, @@ -370,7 +377,8 @@ export function createMockNotificationERC1155Sent(): NormalisedAPINotification { export function createMockNotificationERC1155Received(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC1155_RECEIVED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'erc1155_received', id: 'b6b93c84-e8dc-54ed-9396-7ea50474843a', created_at: '2023-11-20T20:44:10.110706Z', unread: true, @@ -425,7 +433,8 @@ export function createMockNotificationERC1155Received(): NormalisedAPINotificati export function createMockNotificationMetaMaskSwapsCompleted(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'metamask_swap_completed', id: '7ddfe6a1-ac52-5ffe-aa40-f04242db4b8b', created_at: '2023-10-18T13:58:49.854596Z', unread: true, @@ -486,7 +495,8 @@ export function createMockNotificationMetaMaskSwapsCompleted(): NormalisedAPINot export function createMockNotificationRocketPoolStakeCompleted(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'rocketpool_stake_completed', id: 'c2a2f225-b2fb-5d6c-ba56-e27a5c71ffb9', created_at: '2023-11-20T12:02:48.796824Z', unread: true, @@ -546,7 +556,8 @@ export function createMockNotificationRocketPoolStakeCompleted(): NormalisedAPIN export function createMockNotificationRocketPoolUnStakeCompleted(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'rocketpool_unstake_completed', id: '291ec897-f569-4837-b6c0-21001b198dff', created_at: '2023-10-19T13:11:10.623042Z', unread: true, @@ -606,7 +617,8 @@ export function createMockNotificationRocketPoolUnStakeCompleted(): NormalisedAP export function createMockNotificationLidoStakeCompleted(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_STAKE_COMPLETED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'lido_stake_completed', id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', created_at: '2023-11-02T22:28:49.970865Z', unread: true, @@ -666,7 +678,8 @@ export function createMockNotificationLidoStakeCompleted(): NormalisedAPINotific export function createMockNotificationLidoWithdrawalRequested(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'lido_withdrawal_requested', id: 'ef003925-3379-4ba7-9e2d-8218690cadc9', created_at: '2023-10-18T15:04:02.482526Z', unread: true, @@ -726,7 +739,8 @@ export function createMockNotificationLidoWithdrawalRequested(): NormalisedAPINo export function createMockNotificationLidoWithdrawalCompleted(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'lido_withdrawal_completed', id: 'd73df14d-ce73-4f38-bad3-ab028154042f', created_at: '2023-10-18T16:35:03.147606Z', unread: true, @@ -786,7 +800,8 @@ export function createMockNotificationLidoWithdrawalCompleted(): NormalisedAPINo export function createMockNotificationLidoReadyToBeWithdrawn(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN, - notification_type: 'on-chain', + notification_type: 'wallet_activity', + notification_subtype: 'lido_stake_ready_to_be_withdrawn', id: 'd73df14d-ce73-4f38-bad3-ab028154042e', created_at: '2023-10-18T16:35:03.147606Z', unread: true, @@ -833,7 +848,8 @@ export function createMockNotificationLidoReadyToBeWithdrawn(): NormalisedAPINot export function createMockPlatformNotification(): NormalisedAPINotification { const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.PLATFORM, - notification_type: 'platform', + notification_type: 'perps', + notification_subtype: 'position_liquidated', id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', unread: true, created_at: '2025-10-09T09:45:34.202Z', diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts index 3bb9159ef1..4fa599aa9f 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts @@ -1,4 +1,4 @@ -import type { TRIGGER_TYPES } from '../../constants/notification-schema'; +import { TRIGGER_TYPES } from '../../constants/notification-schema'; import type { Compute } from '../type-utils'; // Types derived from external Notification API schema - naming follows API conventions /* eslint-disable @typescript-eslint/naming-convention */ @@ -65,14 +65,11 @@ type NormalizeOnChainNotification< */ type NormalizePlatformNotification< N extends PlatformNotification = PlatformNotification, - NotificationKind extends string = N['notification_type'], -> = { - [K in NotificationKind]: Compute< - N & { - type: ConvertToEnum; - } - >; -}[NotificationKind]; +> = Compute< + N & { + type: TRIGGER_TYPES.PLATFORM; + } +>; export type OnChainRawNotification = Compute< NormalizeOnChainNotification diff --git a/packages/notification-services-controller/src/shared/to-raw-notification.ts b/packages/notification-services-controller/src/shared/to-raw-notification.ts index ed88b96444..820894a8af 100644 --- a/packages/notification-services-controller/src/shared/to-raw-notification.ts +++ b/packages/notification-services-controller/src/shared/to-raw-notification.ts @@ -4,6 +4,7 @@ import type { OnChainRawNotification, PlatformRawNotification, } from 'src/NotificationServicesController/types/notification-api'; +import { TRIGGER_TYPES } from '../NotificationServicesController/constants/notification-schema'; import { isOnChainNotification } from './notification-api-type-guards'; /** @@ -31,6 +32,6 @@ export function toRawAPINotification( return { ...data, - type: data.notification_type, + type: TRIGGER_TYPES.PLATFORM, } as PlatformRawNotification; } From 57bf2d784fcc2c4367eb419cc9e411eed782dc39 Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 10:58:37 +0100 Subject: [PATCH 5/7] fix: lint --- .../notification-services-controller/CHANGELOG.md | 2 +- .../types/notification-api/notification-api.ts | 5 ++--- .../types/notification-api/schema.ts | 8 ++++---- .../src/shared/notification-api-type-guards.ts | 12 +++++++++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index f6b564b4d4..897b2321c2 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Export `isOnChainNotification` and `isPlatformNotification` type guards for discriminating v4 API notification shapes -- Export `Notification`, `PlatformNotification`, and `OnChainNotification` types derived from the v4 Notification API schema +- Export `PlatformNotification` and `OnChainNotification` types derived from the v4 Notification API schema ### Changed diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts index 4fa599aa9f..f09edb9355 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts @@ -27,7 +27,8 @@ export type Data_ERC721Received = components['schemas']['Data_ERC721Received']; export type NetworkMetadata = components['schemas']['NetworkMetadata']; export type BlockExplorer = components['schemas']['BlockExplorer']; -export type Notification = components['schemas']['NotificationOutputV4'][number]; +export type UnprocessedRawNotification = + components['schemas']['NotificationOutputV4'][number]; export type PlatformNotification = components['schemas']['PlatformNotificationV4']; export type OnChainNotification = components['schemas']['OnChainNotificationV4']; @@ -79,8 +80,6 @@ export type PlatformRawNotification = Compute< NormalizePlatformNotification >; -export type UnprocessedRawNotification = Notification; - export type NormalisedAPINotification = | OnChainRawNotification | PlatformRawNotification; diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts index 12276176d1..619a061b26 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts @@ -8,7 +8,7 @@ * Script: `npx openapi-typescript -o ./schema.ts` */ -export interface paths { +export type paths = { '/api/v4/notifications': { parameters: { query?: never; @@ -384,9 +384,9 @@ export interface paths { patch?: never; trace?: never; }; -} +}; export type webhooks = Record; -export interface components { +export type components = { schemas: { /** * @example mobile @@ -787,6 +787,6 @@ export interface components { requestBodies: never; headers: never; pathItems: never; -} +}; export type $defs = Record; export type operations = Record; diff --git a/packages/notification-services-controller/src/shared/notification-api-type-guards.ts b/packages/notification-services-controller/src/shared/notification-api-type-guards.ts index a5981e58ab..f587ef8443 100644 --- a/packages/notification-services-controller/src/shared/notification-api-type-guards.ts +++ b/packages/notification-services-controller/src/shared/notification-api-type-guards.ts @@ -1,23 +1,29 @@ import type { - Notification, + UnprocessedRawNotification, OnChainNotification, PlatformNotification, } from '../NotificationServicesController/types/notification-api'; /** * Narrows a v4 API notification to an on-chain notification. + * + * @param notification - Unprocessed v4 API notification. + * @returns Whether the notification is an on-chain notification. */ export function isOnChainNotification( - notification: Notification, + notification: UnprocessedRawNotification, ): notification is OnChainNotification { return notification.notification_type === 'wallet_activity'; } /** * Narrows a v4 API notification to a platform notification. + * + * @param notification - Unprocessed v4 API notification. + * @returns Whether the notification is a platform notification. */ export function isPlatformNotification( - notification: Notification, + notification: UnprocessedRawNotification, ): notification is PlatformNotification { return !isOnChainNotification(notification); } From e8c6c19243a711e817934b6091d39f28a9bf4cbe Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 11:06:34 +0100 Subject: [PATCH 6/7] chore: formatting --- .../types/notification-api/notification-api.ts | 6 ++++-- .../src/shared/to-raw-notification.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts index f09edb9355..238bc3d574 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts @@ -29,8 +29,10 @@ export type BlockExplorer = components['schemas']['BlockExplorer']; export type UnprocessedRawNotification = components['schemas']['NotificationOutputV4'][number]; -export type PlatformNotification = components['schemas']['PlatformNotificationV4']; -export type OnChainNotification = components['schemas']['OnChainNotificationV4']; +export type PlatformNotification = + components['schemas']['PlatformNotificationV4']; +export type OnChainNotification = + components['schemas']['OnChainNotificationV4']; type ConvertToEnum = { [K in TRIGGER_TYPES]: Kind extends `${K}` ? K : never; diff --git a/packages/notification-services-controller/src/shared/to-raw-notification.ts b/packages/notification-services-controller/src/shared/to-raw-notification.ts index 820894a8af..d19418911d 100644 --- a/packages/notification-services-controller/src/shared/to-raw-notification.ts +++ b/packages/notification-services-controller/src/shared/to-raw-notification.ts @@ -4,6 +4,7 @@ import type { OnChainRawNotification, PlatformRawNotification, } from 'src/NotificationServicesController/types/notification-api'; + import { TRIGGER_TYPES } from '../NotificationServicesController/constants/notification-schema'; import { isOnChainNotification } from './notification-api-type-guards'; From 429f861946d04d76a41b7b5009efdb7b07c0dc01 Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Fri, 3 Jul 2026 11:26:52 +0100 Subject: [PATCH 7/7] chore: add PR links to notification-services-controller changelog entries --- packages/notification-services-controller/CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 897b2321c2..c83ad95d24 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Export `isOnChainNotification` and `isPlatformNotification` type guards for discriminating v4 API notification shapes -- Export `PlatformNotification` and `OnChainNotification` types derived from the v4 Notification API schema +- Export `isOnChainNotification` and `isPlatformNotification` type guards for discriminating v4 API notification shapes ([#9384](https://github.com/MetaMask/core/pull/9384)) +- Export `PlatformNotification` and `OnChainNotification` types derived from the v4 Notification API schema ([#9384](https://github.com/MetaMask/core/pull/9384)) ### Changed -- **BREAKING:** Moved Notification API from v3 to v4 +- **BREAKING:** Moved Notification API from v3 to v4 ([#9384](https://github.com/MetaMask/core/pull/9384)) - API Endpoint Changes: Updated from `/api/v3/notifications` to `/api/v4/notifications` for listing notifications and marking as read - Response Structure: `notification_type` and `notification_subtype` now reflect producer-set database fields instead of fixed enum values - On-chain notifications: `notification_type` is now `"wallet_activity"` (was `"on-chain"`), with `notification_subtype` set to the on-chain kind (e.g. `"metamask_swap_completed"`)