diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 4599034da77..c1a2031f875 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Moved Notification API from v2 to v3 ([#7102](https://github.com/MetaMask/core/pull/7102)) + - API Endpoint Changes: Updated from `/api/v2/notifications` to `/api/v3/notifications` for listing notifications and marking as read + - Request Format: The list notifications endpoint now expects `{ addresses: string[], locale?: string }` instead of `{ address: string }[]` + - Response Structure: Notifications now include a `notification_type` field ('on-chain' or 'platform') and nested payload structure + - On-chain notifications: data moved from root level to `payload.data` + - Platform notifications: new type with `template` containing localized content (`title`, `body`, `image_url`, `cta`) + - Type System Overhaul: + - `OnChainRawNotification` → `NormalisedAPINotification` (union of on-chain and platform) + - `UnprocessedOnChainRawNotification` → `UnprocessedRawNotification` + - Removed specific DeFi notification types (Aave, ENS, Lido rewards, etc.) - now will be handled generically + - Added `TRIGGER_TYPES.PLATFORM` for platform notifications + - Function Signatures: + - `getOnChainNotifications()` → `getAPINotifications()` with new `locale` parameter + - `getOnChainNotificationsConfigCached()` → `getNotificationsApiConfigCached()` + - `processOnChainNotification()` → `processAPINotifications()` + - Service Imports: Update imports from `onchain-notifications` to `api-notifications` + - Auto-expiry: Reduced from 90 days to 30 days for notification auto-expiry + - Locale Support: Added locale parameter to controller constructor for localized server notifications + ## [19.0.0] ### Changed diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts index d5a4cb2d31a..6190ebbbc3a 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts @@ -20,7 +20,7 @@ import { ADDRESS_1, ADDRESS_2 } from './__fixtures__/mockAddresses'; import { mockGetOnChainNotificationsConfig, mockUpdateOnChainNotifications, - mockGetOnChainNotifications, + mockGetAPINotifications, mockFetchFeatureAnnouncementNotifications, mockMarkNotificationsAsRead, mockCreatePerpNotification, @@ -594,7 +594,7 @@ describe('NotificationServicesController', () => { const mockOnChainNotificationsAPIResult = [ createMockNotificationEthSent(), ]; - const mockOnChainNotificationsAPI = mockGetOnChainNotifications({ + const mockOnChainNotificationsAPI = mockGetAPINotifications({ status: 200, body: mockOnChainNotificationsAPIResult, }); @@ -705,7 +705,7 @@ describe('NotificationServicesController', () => { // Mock APIs to fail mockFetchFeatureAnnouncementNotifications({ status: 500 }); - mockGetOnChainNotifications({ status: 500 }); + mockGetAPINotifications({ status: 500 }); const controller = arrangeController(messenger); diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index cb10f433cb0..051e224b1ae 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -21,19 +21,24 @@ import type { AuthenticationController } from '@metamask/profile-sync-controller import { assert } from '@metamask/utils'; import log from 'loglevel'; +import type { NormalisedAPINotification } from '.'; import { TRIGGER_TYPES } from './constants/notification-schema'; import { processAndFilterNotifications, safeProcessNotification, } from './processors/process-notifications'; -import * as FeatureNotifications from './services/feature-announcements'; -import * as OnChainNotifications from './services/onchain-notifications'; +import { + getAPINotifications, + getNotificationsApiConfigCached, + markNotificationsAsRead, + updateOnChainNotifications, +} from './services/api-notifications'; +import { getFeatureAnnouncementNotifications } from './services/feature-announcements'; import { createPerpOrderNotification } from './services/perp-notifications'; import type { INotification, MarkAsReadNotificationsParam, } from './types/notification/notification'; -import type { OnChainRawNotification } from './types/on-chain-notification/on-chain-notification'; import type { OrderInput } from './types/perps'; import type { NotificationServicesPushControllerEnablePushNotificationsAction, @@ -500,6 +505,8 @@ export default class NotificationServicesController extends BaseController< }, }; + readonly #locale: () => string; + readonly #featureAnnouncementEnv: FeatureAnnouncementEnv; /** @@ -510,7 +517,7 @@ export default class NotificationServicesController extends BaseController< * @param args.state - Initial state to set on this controller. * @param args.env - environment variables for a given controller. * @param args.env.featureAnnouncements - env variables for feature announcements. - * @param args.env.isPushIntegrated - toggle push notifications on/off if client has integrated them. + * @param args.env.locale - users locale for better dynamic server notifications */ constructor({ messenger, @@ -521,7 +528,7 @@ export default class NotificationServicesController extends BaseController< state?: Partial; env: { featureAnnouncements: FeatureAnnouncementEnv; - isPushIntegrated?: boolean; + locale?: () => string; }; }) { super({ @@ -532,6 +539,7 @@ export default class NotificationServicesController extends BaseController< }); this.#featureAnnouncementEnv = env.featureAnnouncements; + this.#locale = env.locale ?? (() => 'en'); this.#registerMessageHandlers(); this.#clearLoadingStates(); } @@ -694,11 +702,10 @@ export default class NotificationServicesController extends BaseController< try { const { bearerToken } = await this.#getBearerToken(); const { accounts } = this.#accounts.listAccounts(); - const addressesWithNotifications = - await OnChainNotifications.getOnChainNotificationsConfigCached( - bearerToken, - accounts, - ); + const addressesWithNotifications = await getNotificationsApiConfigCached( + bearerToken, + accounts, + ); const addresses = addressesWithNotifications .filter((a) => Boolean(a.enabled)) .map((a) => a.address); @@ -725,11 +732,10 @@ export default class NotificationServicesController extends BaseController< // Retrieve user storage const { bearerToken } = await this.#getBearerToken(); - const addressesWithNotifications = - await OnChainNotifications.getOnChainNotificationsConfigCached( - bearerToken, - accounts, - ); + const addressesWithNotifications = await getNotificationsApiConfigCached( + bearerToken, + accounts, + ); const result: Record = {}; addressesWithNotifications.forEach((a) => { @@ -788,11 +794,10 @@ export default class NotificationServicesController extends BaseController< const { accounts } = this.#accounts.listAccounts(); // 1. See if has enabled notifications before - const addressesWithNotifications = - await OnChainNotifications.getOnChainNotificationsConfigCached( - bearerToken, - accounts, - ); + const addressesWithNotifications = await getNotificationsApiConfigCached( + bearerToken, + accounts, + ); // Notifications API can return array with addresses set to false // So assert that at least one address is enabled @@ -802,7 +807,7 @@ export default class NotificationServicesController extends BaseController< // 2. Enable Notifications (if no accounts subscribed or we are resetting) if (accountsWithNotifications.length === 0 || opts?.resetNotifications) { - await OnChainNotifications.updateOnChainNotifications( + await updateOnChainNotifications( bearerToken, accounts.map((address) => ({ address, enabled: true })), ); @@ -903,7 +908,7 @@ export default class NotificationServicesController extends BaseController< const { bearerToken } = await this.#getBearerToken(); // Delete these UUIDs (Mutates User Storage) - await OnChainNotifications.updateOnChainNotifications( + await updateOnChainNotifications( bearerToken, accounts.map((address) => ({ address, enabled: false })), ); @@ -935,7 +940,7 @@ export default class NotificationServicesController extends BaseController< this.#updateUpdatingAccountsState(accounts); const { bearerToken } = await this.#getBearerToken(); - await OnChainNotifications.updateOnChainNotifications( + await updateOnChainNotifications( bearerToken, accounts.map((address) => ({ address, enabled: true })), ); @@ -970,31 +975,29 @@ export default class NotificationServicesController extends BaseController< // Raw Feature Notifications const rawAnnouncements = isGlobalNotifsEnabled && this.state.isFeatureAnnouncementsEnabled - ? await FeatureNotifications.getFeatureAnnouncementNotifications( + ? await getFeatureAnnouncementNotifications( this.#featureAnnouncementEnv, previewToken, ).catch(() => []) : []; // Raw On Chain Notifications - const rawOnChainNotifications: OnChainRawNotification[] = []; + const rawOnChainNotifications: NormalisedAPINotification[] = []; if (isGlobalNotifsEnabled) { try { const { bearerToken } = await this.#getBearerToken(); const { accounts } = this.#accounts.listAccounts(); const addressesWithNotifications = ( - await OnChainNotifications.getOnChainNotificationsConfigCached( - bearerToken, - accounts, - ) + await getNotificationsApiConfigCached(bearerToken, accounts) ) .filter((a) => Boolean(a.enabled)) .map((a) => a.address); - const notifications = - await OnChainNotifications.getOnChainNotifications( - bearerToken, - addressesWithNotifications, - ).catch(() => []); + const notifications = await getAPINotifications( + bearerToken, + addressesWithNotifications, + this.#locale(), + this.#featureAnnouncementEnv.platform, + ).catch(() => []); rawOnChainNotifications.push(...notifications); } catch { // Do nothing @@ -1165,7 +1168,7 @@ export default class NotificationServicesController extends BaseController< onchainNotificationIds = onChainNotifications.map( (notification) => notification.id, ); - await OnChainNotifications.markNotificationsAsRead( + await markNotificationsAsRead( bearerToken, onchainNotificationIds, ).catch(() => { diff --git a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts index 97990d77af3..206b6bb5e97 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mockServices.ts @@ -49,7 +49,7 @@ export const mockGetOnChainNotificationsConfig = (mockReply?: MockReply) => { return mockEndpoint; }; -export const mockGetOnChainNotifications = (mockReply?: MockReply) => { +export const mockGetAPINotifications = (mockReply?: MockReply) => { const mockResponse = getMockListNotificationsResponse(); const reply = mockReply ?? { status: 200, body: mockResponse.response }; diff --git a/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts b/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts index 814c5dead04..6a3a2252779 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts @@ -15,16 +15,11 @@ export enum TRIGGER_TYPES { ERC721_RECEIVED = 'erc721_received', ERC1155_SENT = 'erc1155_sent', ERC1155_RECEIVED = 'erc1155_received', - AAVE_V3_HEALTH_FACTOR = 'aave_v3_health_factor', - ENS_EXPIRATION = 'ens_expiration', - LIDO_STAKING_REWARDS = 'lido_staking_rewards', - ROCKETPOOL_STAKING_REWARDS = 'rocketpool_staking_rewards', - NOTIONAL_LOAN_EXPIRATION = 'notional_loan_expiration', - SPARK_FI_HEALTH_FACTOR = 'spark_fi_health_factor', SNAP = 'snap', + PLATFORM = 'platform', } -export const TRIGGER_TYPES_WALLET_SET: Set = new Set([ +export const NOTIFICATION_API_TRIGGER_TYPES_SET: Set = new Set([ TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, TRIGGER_TYPES.ERC20_SENT, TRIGGER_TYPES.ERC20_RECEIVED, @@ -40,13 +35,8 @@ export const TRIGGER_TYPES_WALLET_SET: Set = new Set([ TRIGGER_TYPES.ERC721_RECEIVED, TRIGGER_TYPES.ERC1155_SENT, TRIGGER_TYPES.ERC1155_RECEIVED, -]) satisfies Set>; - -export enum TRIGGER_TYPES_GROUPS { - RECEIVED = 'received', - SENT = 'sent', - DEFI = 'defi', -} + TRIGGER_TYPES.PLATFORM, +]); export const NOTIFICATION_CHAINS_ID = { ETHEREUM: '1', 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 5ed07c66996..89095c29884 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 @@ -1,35 +1,37 @@ import { TRIGGER_TYPES } from '../constants/notification-schema'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; +import type { NormalisedAPINotification } from '../types/notification-api/notification-api'; /** * Mocking Utility - create a mock Eth sent notification * * @returns Mock raw Eth sent notification */ -export function createMockNotificationEthSent(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationEthSent(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ETH_SENT, + notification_type: 'on-chain', id: '3fa85f64-5717-4562-b3fc-2c963f66afa7', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa7', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: - '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', unread: true, created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'eth_sent', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - from: '0x881D40237659C251811CEC9c364ef91dC08D300C', - to: '0x881D40237659C251811CEC9c364ef91dC08D300D', - amount: { - usd: '670.64', - eth: '0.005', + payload: { + chain_id: 1, + block_number: 17485840, + block_timestamp: '2022-03-01T00:00:00Z', + tx_hash: + '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'eth_sent', + network_fee: { + gas_price: '207806259583', + native_token_price_in_usd: '0.83', + }, + from: '0x881D40237659C251811CEC9c364ef91dC08D300C', + to: '0x881D40237659C251811CEC9c364ef91dC08D300D', + amount: { + usd: '670.64', + eth: '0.005', + }, }, }, }; @@ -42,30 +44,32 @@ export function createMockNotificationEthSent(): OnChainRawNotification { * * @returns Mock raw Eth Received notification */ -export function createMockNotificationEthReceived(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationEthReceived(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ETH_RECEIVED, + notification_type: 'on-chain', id: '3fa85f64-5717-4562-b3fc-2c963f66afa8', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa8', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: - '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', unread: true, created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'eth_received', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - from: '0x881D40237659C251811CEC9c364ef91dC08D300C', - to: '0x881D40237659C251811CEC9c364ef91dC08D300D', - amount: { - usd: '670.64', - eth: '808.000000000000000000', + payload: { + chain_id: 1, + block_number: 17485840, + block_timestamp: '2022-03-01T00:00:00Z', + tx_hash: + '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'eth_received', + network_fee: { + gas_price: '207806259583', + native_token_price_in_usd: '0.83', + }, + from: '0x881D40237659C251811CEC9c364ef91dC08D300C', + to: '0x881D40237659C251811CEC9c364ef91dC08D300D', + amount: { + usd: '670.64', + eth: '808.000000000000000000', + }, }, }, }; @@ -78,36 +82,38 @@ export function createMockNotificationEthReceived(): OnChainRawNotification { * * @returns Mock raw ERC20 sent notification */ -export function createMockNotificationERC20Sent(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationERC20Sent(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC20_SENT, + notification_type: 'on-chain', id: '3fa85f64-5717-4562-b3fc-2c963f66afa9', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa9', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: - '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', unread: true, created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'erc20_sent', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - to: '0xecc19e177d24551aa7ed6bc6fe566eca726cc8a9', - from: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', - token: { - usd: '1.00', - name: 'USDC', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/usdc.svg', - amount: '4956250000', - symbol: 'USDC', - address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - decimals: '6', + payload: { + chain_id: 1, + block_number: 17485840, + block_timestamp: '2022-03-01T00:00:00Z', + tx_hash: + '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'erc20_sent', + network_fee: { + gas_price: '207806259583', + native_token_price_in_usd: '0.83', + }, + to: '0xecc19e177d24551aa7ed6bc6fe566eca726cc8a9', + from: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', + token: { + usd: '1.00', + name: 'USDC', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/usdc.svg', + amount: '4956250000', + symbol: 'USDC', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + decimals: '6', + }, }, }, }; @@ -120,36 +126,38 @@ export function createMockNotificationERC20Sent(): OnChainRawNotification { * * @returns Mock raw ERC20 received notification */ -export function createMockNotificationERC20Received(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationERC20Received(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC20_RECEIVED, + notification_type: 'on-chain', id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: - '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', unread: true, created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'erc20_received', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - to: '0xeae7380dd4cef6fbd1144f49e4d1e6964258a4f4', - from: '0x51c72848c68a965f66fa7a88855f9f7784502a7f', - token: { - usd: '0.00', - name: 'SHIBA INU', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/shib.svg', - amount: '8382798736999999457296646144', - symbol: 'SHIB', - address: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', - decimals: '18', + payload: { + chain_id: 1, + block_number: 17485840, + block_timestamp: '2022-03-01T00:00:00Z', + tx_hash: + '0xb2256b183f2fb3872f99294ab55fb03e6a479b0d4aca556a3b27568b712505a6', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'erc20_received', + network_fee: { + gas_price: '207806259583', + native_token_price_in_usd: '0.83', + }, + to: '0xeae7380dd4cef6fbd1144f49e4d1e6964258a4f4', + from: '0x51c72848c68a965f66fa7a88855f9f7784502a7f', + token: { + usd: '0.00', + name: 'SHIBA INU', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/shib.svg', + amount: '8382798736999999457296646144', + symbol: 'SHIB', + address: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', + decimals: '18', + }, }, }, }; @@ -162,41 +170,43 @@ export function createMockNotificationERC20Received(): OnChainRawNotification { * * @returns Mock raw ERC721 sent notification */ -export function createMockNotificationERC721Sent(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationERC721Sent(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC721_SENT, - block_number: 18576643, - block_timestamp: '1700043467', - chain_id: 1, + notification_type: 'on-chain', + id: 'a4193058-9814-537e-9df4-79dcac727fb6', created_at: '2023-11-15T11:08:17.895407Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0xf47f628fe3bd2595e9ab384bfffc3859b448e451', - nft: { - name: 'Captainz #8680', - image: - 'https://i.seadn.io/s/raw/files/ae0fc06714ff7fb40217340d8a242c0e.gif?w=500&auto=format', - token_id: '8680', - collection: { - name: 'The Captainz', + unread: true, + payload: { + block_number: 18576643, + block_timestamp: '1700043467', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + to: '0xf47f628fe3bd2595e9ab384bfffc3859b448e451', + nft: { + name: 'Captainz #8680', image: - 'https://i.seadn.io/gcs/files/6df4d75778066bce740050615bc84e21.png?w=500&auto=format', - symbol: 'Captainz', - address: '0x769272677fab02575e84945f03eca517acc544cc', + 'https://i.seadn.io/s/raw/files/ae0fc06714ff7fb40217340d8a242c0e.gif?w=500&auto=format', + token_id: '8680', + collection: { + name: 'The Captainz', + image: + 'https://i.seadn.io/gcs/files/6df4d75778066bce740050615bc84e21.png?w=500&auto=format', + symbol: 'Captainz', + address: '0x769272677fab02575e84945f03eca517acc544cc', + }, + }, + from: '0x24a0bb54b7e7a8e406e9b28058a9fd6c49e6df4f', + kind: 'erc721_sent', + network_fee: { + gas_price: '24550653274', + native_token_price_in_usd: '1986.61', }, }, - from: '0x24a0bb54b7e7a8e406e9b28058a9fd6c49e6df4f', - kind: 'erc721_sent', - network_fee: { - gas_price: '24550653274', - native_token_price_in_usd: '1986.61', - }, + tx_hash: + '0x0833c69fb41cf972a0f031fceca242939bc3fcf82b964b74606649abcad371bd', }, - id: 'a4193058-9814-537e-9df4-79dcac727fb6', - trigger_id: '028485be-b994-422b-a93b-03fcc01ab715', - tx_hash: - '0x0833c69fb41cf972a0f031fceca242939bc3fcf82b964b74606649abcad371bd', - unread: true, }; return mockNotification; @@ -207,41 +217,43 @@ export function createMockNotificationERC721Sent(): OnChainRawNotification { * * @returns Mock raw ERC721 received notification */ -export function createMockNotificationERC721Received(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationERC721Received(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC721_RECEIVED, - block_number: 18571446, - block_timestamp: '1699980623', - chain_id: 1, + notification_type: 'on-chain', + id: '00a79d24-befa-57ed-a55a-9eb8696e1654', created_at: '2023-11-14T17:40:52.319281Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0xba7f3daa8adfdad686574406ab9bd5d2f0a49d2e', - nft: { - name: 'The Plague #2722', - image: - 'https://i.seadn.io/s/raw/files/a96f90ec8ebf55a2300c66a0c46d6a16.png?w=500&auto=format', - token_id: '2722', - collection: { - name: 'The Plague NFT', + unread: true, + payload: { + block_number: 18571446, + block_timestamp: '1699980623', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + to: '0xba7f3daa8adfdad686574406ab9bd5d2f0a49d2e', + nft: { + name: 'The Plague #2722', image: - 'https://i.seadn.io/gcs/files/4577987a5ca45ca5118b2e31559ee4d1.jpg?w=500&auto=format', - symbol: 'FROG', - address: '0xc379e535caff250a01caa6c3724ed1359fe5c29b', + 'https://i.seadn.io/s/raw/files/a96f90ec8ebf55a2300c66a0c46d6a16.png?w=500&auto=format', + token_id: '2722', + collection: { + name: 'The Plague NFT', + image: + 'https://i.seadn.io/gcs/files/4577987a5ca45ca5118b2e31559ee4d1.jpg?w=500&auto=format', + symbol: 'FROG', + address: '0xc379e535caff250a01caa6c3724ed1359fe5c29b', + }, + }, + from: '0x24a0bb54b7e7a8e406e9b28058a9fd6c49e6df4f', + kind: 'erc721_received', + network_fee: { + gas_price: '53701898538', + native_token_price_in_usd: '2047.01', }, }, - from: '0x24a0bb54b7e7a8e406e9b28058a9fd6c49e6df4f', - kind: 'erc721_received', - network_fee: { - gas_price: '53701898538', - native_token_price_in_usd: '2047.01', - }, + tx_hash: + '0xe554c9e29e6eeca8ba94da4d047334ba08b8eb9ca3b801dd69cec08dfdd4ae43', }, - id: '00a79d24-befa-57ed-a55a-9eb8696e1654', - trigger_id: 'd24ac26a-8579-49ec-9947-d04d63592ebd', - tx_hash: - '0xe554c9e29e6eeca8ba94da4d047334ba08b8eb9ca3b801dd69cec08dfdd4ae43', - unread: true, }; return mockNotification; @@ -252,41 +264,43 @@ export function createMockNotificationERC721Received(): OnChainRawNotification { * * @returns Mock raw ERC1155 sent notification */ -export function createMockNotificationERC1155Sent(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationERC1155Sent(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC1155_SENT, - block_number: 18615206, - block_timestamp: '1700510003', - chain_id: 1, + notification_type: 'on-chain', + id: 'a09ff9d1-623a-52ab-a3d4-c7c8c9a58362', created_at: '2023-11-20T20:44:10.110706Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0x15bd77ccacf2da39b84f0c31fee2e451225bb190', - nft: { - name: 'IlluminatiNFT DAO', - image: - 'https://i.seadn.io/gcs/files/79a77cb37c7b2f1069f752645d29fea7.jpg?w=500&auto=format', - token_id: '1', - collection: { + unread: true, + payload: { + block_number: 18615206, + block_timestamp: '1700510003', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + to: '0x15bd77ccacf2da39b84f0c31fee2e451225bb190', + nft: { name: 'IlluminatiNFT DAO', image: - 'https://i.seadn.io/gae/LTKz3om2eCQfn3M6PkqEmY7KhLtdMCOm0QVch2318KJq7-KyToCH7NBTMo4UuJ0AZI-oaBh1HcgrAEIEWYbXY3uMcYpuGXunaXEh?w=500&auto=format', - symbol: 'TRUTH', - address: '0xe25f0fe686477f9df3c2876c4902d3b85f75f33a', + 'https://i.seadn.io/gcs/files/79a77cb37c7b2f1069f752645d29fea7.jpg?w=500&auto=format', + token_id: '1', + collection: { + name: 'IlluminatiNFT DAO', + image: + 'https://i.seadn.io/gae/LTKz3om2eCQfn3M6PkqEmY7KhLtdMCOm0QVch2318KJq7-KyToCH7NBTMo4UuJ0AZI-oaBh1HcgrAEIEWYbXY3uMcYpuGXunaXEh?w=500&auto=format', + symbol: 'TRUTH', + address: '0xe25f0fe686477f9df3c2876c4902d3b85f75f33a', + }, + }, + from: '0x0000000000000000000000000000000000000000', + kind: 'erc1155_sent', + network_fee: { + gas_price: '33571446596', + native_token_price_in_usd: '2038.88', }, }, - from: '0x0000000000000000000000000000000000000000', - kind: 'erc1155_sent', - network_fee: { - gas_price: '33571446596', - native_token_price_in_usd: '2038.88', - }, + tx_hash: + '0x03381aba290facbaf71c123e263c8dc3dd550aac00ef589cce395182eaeff76f', }, - id: 'a09ff9d1-623a-52ab-a3d4-c7c8c9a58362', - trigger_id: 'e2130f7d-78b8-4c34-999a-3f3d3bb5b03c', - tx_hash: - '0x03381aba290facbaf71c123e263c8dc3dd550aac00ef589cce395182eaeff76f', - unread: true, }; return mockNotification; @@ -297,41 +311,43 @@ export function createMockNotificationERC1155Sent(): OnChainRawNotification { * * @returns Mock raw ERC1155 received notification */ -export function createMockNotificationERC1155Received(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationERC1155Received(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ERC1155_RECEIVED, - block_number: 18615206, - block_timestamp: '1700510003', - chain_id: 1, + notification_type: 'on-chain', + id: 'b6b93c84-e8dc-54ed-9396-7ea50474843a', created_at: '2023-11-20T20:44:10.110706Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0x15bd77ccacf2da39b84f0c31fee2e451225bb190', - nft: { - name: 'IlluminatiNFT DAO', - image: - 'https://i.seadn.io/gcs/files/79a77cb37c7b2f1069f752645d29fea7.jpg?w=500&auto=format', - token_id: '1', - collection: { + unread: true, + payload: { + block_number: 18615206, + block_timestamp: '1700510003', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + to: '0x15bd77ccacf2da39b84f0c31fee2e451225bb190', + nft: { name: 'IlluminatiNFT DAO', image: - 'https://i.seadn.io/gae/LTKz3om2eCQfn3M6PkqEmY7KhLtdMCOm0QVch2318KJq7-KyToCH7NBTMo4UuJ0AZI-oaBh1HcgrAEIEWYbXY3uMcYpuGXunaXEh?w=500&auto=format', - symbol: 'TRUTH', - address: '0xe25f0fe686477f9df3c2876c4902d3b85f75f33a', + 'https://i.seadn.io/gcs/files/79a77cb37c7b2f1069f752645d29fea7.jpg?w=500&auto=format', + token_id: '1', + collection: { + name: 'IlluminatiNFT DAO', + image: + 'https://i.seadn.io/gae/LTKz3om2eCQfn3M6PkqEmY7KhLtdMCOm0QVch2318KJq7-KyToCH7NBTMo4UuJ0AZI-oaBh1HcgrAEIEWYbXY3uMcYpuGXunaXEh?w=500&auto=format', + symbol: 'TRUTH', + address: '0xe25f0fe686477f9df3c2876c4902d3b85f75f33a', + }, + }, + from: '0x0000000000000000000000000000000000000000', + kind: 'erc1155_received', + network_fee: { + gas_price: '33571446596', + native_token_price_in_usd: '2038.88', }, }, - from: '0x0000000000000000000000000000000000000000', - kind: 'erc1155_received', - network_fee: { - gas_price: '33571446596', - native_token_price_in_usd: '2038.88', - }, + tx_hash: + '0x03381aba290facbaf71c123e263c8dc3dd550aac00ef589cce395182eaeff76f', }, - id: 'b6b93c84-e8dc-54ed-9396-7ea50474843a', - trigger_id: '710c8abb-43a9-42a5-9d86-9dd258726c82', - tx_hash: - '0x03381aba290facbaf71c123e263c8dc3dd550aac00ef589cce395182eaeff76f', - unread: true, }; return mockNotification; @@ -342,47 +358,49 @@ export function createMockNotificationERC1155Received(): OnChainRawNotification * * @returns Mock raw MetaMask Swaps notification */ -export function createMockNotificationMetaMaskSwapsCompleted(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationMetaMaskSwapsCompleted(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, - block_number: 18377666, - block_timestamp: '1697637275', - chain_id: 1, + notification_type: 'on-chain', + id: '7ddfe6a1-ac52-5ffe-aa40-f04242db4b8b', created_at: '2023-10-18T13:58:49.854596Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'metamask_swap_completed', - rate: '1558.27', - token_in: { - usd: '1576.73', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '9000000000000000', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - token_out: { - usd: '1.00', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/usdt.svg', - amount: '14024419', - symbol: 'USDT', - address: '0xdac17f958d2ee523a2206206994597c13d831ec7', - decimals: '6', - name: 'USDT', - }, - network_fee: { - gas_price: '15406129273', - native_token_price_in_usd: '1576.73', + unread: true, + payload: { + block_number: 18377666, + block_timestamp: '1697637275', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'metamask_swap_completed', + rate: '1558.27', + token_in: { + usd: '1576.73', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + amount: '9000000000000000', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: '18', + name: 'Ethereum', + }, + token_out: { + usd: '1.00', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/usdt.svg', + amount: '14024419', + symbol: 'USDT', + address: '0xdac17f958d2ee523a2206206994597c13d831ec7', + decimals: '6', + name: 'USDT', + }, + network_fee: { + gas_price: '15406129273', + native_token_price_in_usd: '1576.73', + }, }, + tx_hash: + '0xf69074290f3aa11bce567aabc9ca0df7a12559dfae1b80ba1a124e9dfe19ecc5', }, - id: '7ddfe6a1-ac52-5ffe-aa40-f04242db4b8b', - trigger_id: 'd2eaa2eb-2e6e-4fd5-8763-b70ea571b46c', - tx_hash: - '0xf69074290f3aa11bce567aabc9ca0df7a12559dfae1b80ba1a124e9dfe19ecc5', - unread: true, }; return mockNotification; @@ -393,46 +411,48 @@ export function createMockNotificationMetaMaskSwapsCompleted(): OnChainRawNotifi * * @returns Mock raw RocketPool Stake Completed notification */ -export function createMockNotificationRocketPoolStakeCompleted(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationRocketPoolStakeCompleted(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED, - block_number: 18585057, - block_timestamp: '1700145059', - chain_id: 1, + notification_type: 'on-chain', + id: 'c2a2f225-b2fb-5d6c-ba56-e27a5c71ffb9', created_at: '2023-11-20T12:02:48.796824Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'rocketpool_stake_completed', - stake_in: { - usd: '2031.86', - name: 'Ethereum', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '190690478063438272', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - }, - stake_out: { - usd: '2226.49', - name: 'Rocket Pool ETH', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg', - amount: '175024360778165879', - symbol: 'RETH', - address: '0xae78736Cd615f374D3085123A210448E74Fc6393', - decimals: '18', - }, - network_fee: { - gas_price: '36000000000', - native_token_price_in_usd: '2031.86', + unread: true, + payload: { + block_number: 18585057, + block_timestamp: '1700145059', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'rocketpool_stake_completed', + stake_in: { + usd: '2031.86', + name: 'Ethereum', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + amount: '190690478063438272', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: '18', + }, + stake_out: { + usd: '2226.49', + name: 'Rocket Pool ETH', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg', + amount: '175024360778165879', + symbol: 'RETH', + address: '0xae78736Cd615f374D3085123A210448E74Fc6393', + decimals: '18', + }, + network_fee: { + gas_price: '36000000000', + native_token_price_in_usd: '2031.86', + }, }, + tx_hash: + '0xcfc0693bf47995907b0f46ef0644cf16dd9a0de797099b2e00fd481e1b2117d3', }, - id: 'c2a2f225-b2fb-5d6c-ba56-e27a5c71ffb9', - trigger_id: '5110ff97-acff-40c0-83b4-11d487b8c7b0', - tx_hash: - '0xcfc0693bf47995907b0f46ef0644cf16dd9a0de797099b2e00fd481e1b2117d3', - unread: true, }; return mockNotification; @@ -443,46 +463,48 @@ export function createMockNotificationRocketPoolStakeCompleted(): OnChainRawNoti * * @returns Mock raw RocketPool Un-staked notification */ -export function createMockNotificationRocketPoolUnStakeCompleted(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationRocketPoolUnStakeCompleted(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED, - block_number: 18384336, - block_timestamp: '1697718011', - chain_id: 1, + notification_type: 'on-chain', + id: '291ec897-f569-4837-b6c0-21001b198dff', created_at: '2023-10-19T13:11:10.623042Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'rocketpool_unstake_completed', - stake_in: { - usd: '1686.34', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg', - amount: '66608041413696770', - symbol: 'RETH', - address: '0xae78736Cd615f374D3085123A210448E74Fc6393', - decimals: '18', - name: 'Rocketpool Eth', - }, - stake_out: { - usd: '1553.75', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '72387843427700824', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - network_fee: { - gas_price: '5656322987', - native_token_price_in_usd: '1553.75', + unread: true, + payload: { + block_number: 18384336, + block_timestamp: '1697718011', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'rocketpool_unstake_completed', + stake_in: { + usd: '1686.34', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg', + amount: '66608041413696770', + symbol: 'RETH', + address: '0xae78736Cd615f374D3085123A210448E74Fc6393', + decimals: '18', + name: 'Rocketpool Eth', + }, + stake_out: { + usd: '1553.75', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + amount: '72387843427700824', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: '18', + name: 'Ethereum', + }, + network_fee: { + gas_price: '5656322987', + native_token_price_in_usd: '1553.75', + }, }, + tx_hash: + '0xc7972a7e409abfc62590ec90e633acd70b9b74e76ad02305be8bf133a0e22d5f', }, - id: '291ec897-f569-4837-b6c0-21001b198dff', - trigger_id: '291ec897-f569-4837-b6c0-21001b198dff', - tx_hash: - '0xc7972a7e409abfc62590ec90e633acd70b9b74e76ad02305be8bf133a0e22d5f', - unread: true, }; return mockNotification; @@ -493,46 +515,48 @@ export function createMockNotificationRocketPoolUnStakeCompleted(): OnChainRawNo * * @returns Mock raw Lido Stake Completed notification */ -export function createMockNotificationLidoStakeCompleted(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationLidoStakeCompleted(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_STAKE_COMPLETED, - block_number: 18487118, - block_timestamp: '1698961091', - chain_id: 1, + notification_type: 'on-chain', + id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', created_at: '2023-11-02T22:28:49.970865Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_stake_completed', - stake_in: { - usd: '1806.33', - name: 'Ethereum', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '330303634023928032', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - }, - stake_out: { - usd: '1801.30', - name: 'Liquid staked Ether 2.0', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', - amount: '330303634023928032', - symbol: 'STETH', - address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', - decimals: '18', - }, - network_fee: { - gas_price: '26536359866', - native_token_price_in_usd: '1806.33', + unread: true, + payload: { + block_number: 18487118, + block_timestamp: '1698961091', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'lido_stake_completed', + stake_in: { + usd: '1806.33', + name: 'Ethereum', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + amount: '330303634023928032', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: '18', + }, + stake_out: { + usd: '1801.30', + name: 'Liquid staked Ether 2.0', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', + amount: '330303634023928032', + symbol: 'STETH', + address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', + decimals: '18', + }, + network_fee: { + gas_price: '26536359866', + native_token_price_in_usd: '1806.33', + }, }, + tx_hash: + '0x8cc0fa805f7c3b1743b14f3b91c6b824113b094f26d4ccaf6a71ad8547ce6a0f', }, - id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', - trigger_id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', - tx_hash: - '0x8cc0fa805f7c3b1743b14f3b91c6b824113b094f26d4ccaf6a71ad8547ce6a0f', - unread: true, }; return mockNotification; @@ -543,46 +567,48 @@ export function createMockNotificationLidoStakeCompleted(): OnChainRawNotificati * * @returns Mock raw Lido Withdrawal Requested notification */ -export function createMockNotificationLidoWithdrawalRequested(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationLidoWithdrawalRequested(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED, - block_number: 18377760, - block_timestamp: '1697638415', - chain_id: 1, + notification_type: 'on-chain', + id: 'ef003925-3379-4ba7-9e2d-8218690cadc9', created_at: '2023-10-18T15:04:02.482526Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_withdrawal_requested', - stake_in: { - usd: '1568.54', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', - amount: '97180668792218669859', - symbol: 'STETH', - address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', - decimals: '18', - name: 'Staked Eth', - }, - stake_out: { - usd: '1576.73', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '97180668792218669859', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - network_fee: { - gas_price: '11658906980', - native_token_price_in_usd: '1576.73', + unread: true, + payload: { + block_number: 18377760, + block_timestamp: '1697638415', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'lido_withdrawal_requested', + stake_in: { + usd: '1568.54', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', + amount: '97180668792218669859', + symbol: 'STETH', + address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', + decimals: '18', + name: 'Staked Eth', + }, + stake_out: { + usd: '1576.73', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + amount: '97180668792218669859', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: '18', + name: 'Ethereum', + }, + network_fee: { + gas_price: '11658906980', + native_token_price_in_usd: '1576.73', + }, }, + tx_hash: + '0x58b5f82e084cb750ea174e02b20fbdfd2ba8d78053deac787f34fc38e5d427aa', }, - id: 'ef003925-3379-4ba7-9e2d-8218690cadc9', - trigger_id: 'ef003925-3379-4ba7-9e2d-8218690cadc9', - tx_hash: - '0x58b5f82e084cb750ea174e02b20fbdfd2ba8d78053deac787f34fc38e5d427aa', - unread: true, }; return mockNotification; @@ -593,46 +619,48 @@ export function createMockNotificationLidoWithdrawalRequested(): OnChainRawNotif * * @returns Mock raw Lido Withdrawal Completed notification */ -export function createMockNotificationLidoWithdrawalCompleted(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationLidoWithdrawalCompleted(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED, - block_number: 18378208, - block_timestamp: '1697643851', - chain_id: 1, + notification_type: 'on-chain', + id: 'd73df14d-ce73-4f38-bad3-ab028154042f', created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_withdrawal_completed', - stake_in: { - usd: '1570.23', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', - amount: '35081997661451346', - symbol: 'STETH', - address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', - decimals: '18', - name: 'Staked Eth', - }, - stake_out: { - usd: '1571.74', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '35081997661451346', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - network_fee: { - gas_price: '12699495150', - native_token_price_in_usd: '1571.74', + unread: true, + payload: { + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'lido_withdrawal_completed', + stake_in: { + usd: '1570.23', + image: + 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', + amount: '35081997661451346', + symbol: 'STETH', + address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', + decimals: '18', + name: 'Staked Eth', + }, + stake_out: { + usd: '1571.74', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + amount: '35081997661451346', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: '18', + name: 'Ethereum', + }, + network_fee: { + gas_price: '12699495150', + native_token_price_in_usd: '1571.74', + }, }, + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', }, - id: 'd73df14d-ce73-4f38-bad3-ab028154042f', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042f', - tx_hash: - '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', - unread: true, }; return mockNotification; @@ -643,196 +671,62 @@ export function createMockNotificationLidoWithdrawalCompleted(): OnChainRawNotif * * @returns Mock raw Lido Withdrawal Ready notification */ -export function createMockNotificationLidoReadyToBeWithdrawn(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { +export function createMockNotificationLidoReadyToBeWithdrawn(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { type: TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN, - block_number: 18378208, - block_timestamp: '1697643851', - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_stake_ready_to_be_withdrawn', - request_id: '123456789', - staked_eth: { - address: '0x881D40237659C251811CEC9c364ef91dC08D300F', - symbol: 'ETH', - name: 'Ethereum', - amount: '2.5', - decimals: '18', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - usd: '10000.00', - }, - }, + notification_type: 'on-chain', id: 'd73df14d-ce73-4f38-bad3-ab028154042e', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042e', - tx_hash: - '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', - unread: true, - }; - - return mockNotification; -} - -/** - * Mocking Utility - create a mock Aave V3 Health Factor notification - * - * @returns Mock raw Aave V3 Health Factor notification - */ -export function createMockNotificationAaveV3HealthFactor(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.AAVE_V3_HEALTH_FACTOR, - chain_id: 1, created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'aave_v3_health_factor', - chainId: 1, - healthFactor: '3.4', - threshold: '5.5', - }, - id: 'd73df14d-ce73-4f38-bad3-ab028154042b', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042b', unread: true, - }; - - return mockNotification; -} - -/** - * Mocking Utility - create a mock ENS Expiration notification - * - * @returns Mock raw ENS Expiration notification - */ -export function createMockNotificationEnsExpiration(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ENS_EXPIRATION, - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'ens_expiration', - chainId: 1, - reverseEnsName: 'vitalik.eth', - expirationDateIso: '2024-01-01T00:00:00Z', - reminderDelayInSeconds: 86400, - }, - id: 'f4ef0b7f-5612-537f-9144-0b5c63ae5391', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042c', - unread: true, - }; - - return mockNotification; -} - -/** - * Mocking Utility - create a mock Lido Staking Rewards notification - * - * @returns Mock raw Lido Staking Rewards notification - */ -export function createMockNotificationLidoStakingRewards(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.LIDO_STAKING_REWARDS, - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_staking_rewards', - chainId: 1, - currentStethBalance: '10', - currentEthValue: '10.5', - estimatedTotalRewardInPeriod: '0.5', - daysSinceLastNotification: 30, - notificationIntervalDays: 30, - }, - id: 'd73df14d-ce73-4f38-bad3-ab028154042l', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042l', - unread: true, - }; - - return mockNotification; -} - -/** - * Mocking Utility - create a mock Notional Loan Expiration notification - * - * @returns Mock raw Notional Loan Expiration notification - */ -export function createMockNotificationNotionalLoanExpiration(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.NOTIONAL_LOAN_EXPIRATION, - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'notional_loan_expiration', - chainId: 1, - loans: [ - { - amount: '1.1234', + payload: { + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'lido_stake_ready_to_be_withdrawn', + request_id: '123456789', + staked_eth: { + address: '0x881D40237659C251811CEC9c364ef91dC08D300F', symbol: 'ETH', - maturityDateIso: '2024-01-01T00:00:00Z', + name: 'Ethereum', + amount: '2.5', + decimals: '18', + image: + 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', + usd: '10000.00', }, - ], - reminderDelayInSeconds: 86400, + }, + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', }, - id: 'd73df14d-ce73-4f38-bad3-ab028154042n', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042n', - unread: true, }; return mockNotification; } /** - * Mocking Utility - create a mock Rocketpool Staking Rewards notification + * Mocking Utility - create a mock Generic Platform notification * - * @returns Mock raw Rocketpool Staking Rewards notification + * @returns Mock raw Generic Platform notification */ -export function createMockNotificationRocketpoolStakingRewards(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ROCKETPOOL_STAKING_REWARDS, - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'rocketpool_staking_rewards', - chainId: 1, - currentRethBalance: '10', - currentEthValue: '10.5', - estimatedTotalRewardInPeriod: '0.5', - daysSinceLastNotification: 30, - notificationIntervalDays: 30, - }, - id: 'd73df14d-ce73-4f38-bad3-ab028154042r', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042r', +export function createMockPlatformNotification(): NormalisedAPINotification { + const mockNotification: NormalisedAPINotification = { + type: TRIGGER_TYPES.PLATFORM, + notification_type: 'platform', + id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', unread: true, - }; - - return mockNotification; -} - -/** - * Mocking Utility - create a mock SparkFi Health Factor notification - * - * @returns Mock raw SparkFi Health Factor notification - */ -export function createMockNotificationSparkFiHealthFactor(): OnChainRawNotification { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.SPARK_FI_HEALTH_FACTOR, - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'spark_fi_health_factor', - chainId: 1, - healthFactor: '3.4', - threshold: '5.5', + created_at: '2025-10-09T09:45:34.202Z', + template: { + image_url: + 'https://images.ctfassets.net/clixtyxoaeas/4rnpEzy1ATWRKVBOLxZ1Fm/a74dc1eed36d23d7ea6030383a4d5163/MetaMask-icon-fox.svg', + title: 'This is a Platform Notification!', + body: 'Teams can now build out their own notifications, and add an optional CTA (like this one below).', + cta: { + content: 'Get Started', + link: 'https://metamask.io/get-started', + }, }, - id: 'd73df14d-ce73-4f38-bad3-ab028154042s', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042s', - unread: true, }; return mockNotification; @@ -843,7 +737,7 @@ export function createMockNotificationSparkFiHealthFactor(): OnChainRawNotificat * * @returns Array of raw on-chain notifications */ -export function createMockRawOnChainNotifications(): OnChainRawNotification[] { +export function createMockRawOnChainNotifications(): NormalisedAPINotification[] { return [1, 2, 3].map((id) => { const notification = createMockNotificationEthSent(); notification.id += `-${id}`; diff --git a/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts b/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts index e3f4fbcb3cc..47129f4133d 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/mocks/mockResponses.ts @@ -1,12 +1,12 @@ import { createMockFeatureAnnouncementAPIResult } from './mock-feature-announcements'; import { createMockRawOnChainNotifications } from './mock-raw-notifications'; -import { FEATURE_ANNOUNCEMENT_API } from '../services/feature-announcements'; import { NOTIFICATION_API_LIST_ENDPOINT, NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, TRIGGER_API_NOTIFICATIONS_ENDPOINT, TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT, -} from '../services/onchain-notifications'; +} from '../services/api-notifications'; +import { FEATURE_ANNOUNCEMENT_API } from '../services/feature-announcements'; import { PERPS_API_CREATE_ORDERS } from '../services/perp-notifications'; type MockResponse = { diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/index.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/index.ts index 9ce54fad42e..fdd06a7c4b9 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/index.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/index.ts @@ -1,3 +1,3 @@ export * from './process-feature-announcement'; export * from './process-notifications'; -export * from './process-onchain-notifications'; +export * from './process-api-notifications'; diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-onchain-notifications.test.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.test.ts similarity index 82% rename from packages/notification-services-controller/src/NotificationServicesController/processors/process-onchain-notifications.test.ts rename to packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.test.ts index bc6ba3b8c86..4fe53b68a8d 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-onchain-notifications.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.test.ts @@ -1,4 +1,4 @@ -import { processOnChainNotification } from './process-onchain-notifications'; +import { processAPINotifications } from './process-api-notifications'; import { createMockNotificationEthSent, createMockNotificationEthReceived, @@ -15,8 +15,8 @@ import { createMockNotificationLidoWithdrawalRequested, createMockNotificationLidoWithdrawalCompleted, createMockNotificationLidoReadyToBeWithdrawn, + createMockPlatformNotification, } from '../mocks/mock-raw-notifications'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; const rawNotifications = [ createMockNotificationEthSent(), @@ -34,17 +34,18 @@ const rawNotifications = [ createMockNotificationLidoWithdrawalRequested(), createMockNotificationLidoWithdrawalCompleted(), createMockNotificationLidoReadyToBeWithdrawn(), + createMockPlatformNotification(), ]; const rawNotificationTestSuite = rawNotifications.map( - (n): [string, OnChainRawNotification] => [n.type, n], + (n) => [n.type, n] as const, ); describe('process-onchain-notifications - processOnChainNotification()', () => { it.each(rawNotificationTestSuite)( 'converts Raw On-Chain Notification (%s) to a shared Notification Type', - (_: string, rawNotification: OnChainRawNotification) => { - const result = processOnChainNotification(rawNotification); + (_, rawNotification) => { + const result = processAPINotifications(rawNotification); expect(result.id).toBe(rawNotification.id); expect(result.type).toBe(rawNotification.type); }, diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts new file mode 100644 index 00000000000..ac2e0400111 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts @@ -0,0 +1,23 @@ +import type { INotification } from '../types/notification/notification'; +import type { NormalisedAPINotification } from '../types/notification-api/notification-api'; +import { shouldAutoExpire } from '../utils/should-auto-expire'; + +/** + * Processes API notifications to a normalized INotification shape + * + * @param notification - API Notification (On-Chain or Platform Notification) + * @returns Normalized Notification + */ +export function processAPINotifications( + notification: NormalisedAPINotification, +): INotification { + const createdAtDate = new Date(notification.created_at); + const expired = shouldAutoExpire(createdAtDate); + + return { + ...notification, + id: notification.id, + createdAt: createdAtDate.toISOString(), + isRead: expired || !notification.unread, + }; +} diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.test.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.test.ts index 99ed5ed9ad4..e212feb983c 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.test.ts @@ -2,6 +2,7 @@ import { isFeatureAnnouncementRead, processFeatureAnnouncement, } from './process-feature-announcement'; +import type { INotification } from '..'; import { TRIGGER_TYPES } from '../constants/notification-schema'; import { createMockFeatureAnnouncementRaw } from '../mocks/mock-feature-announcements'; @@ -42,7 +43,10 @@ describe('process-feature-announcement - isFeatureAnnouncementRead()', () => { describe('process-feature-announcement - processFeatureAnnouncement()', () => { it('processes a Raw Feature Announcement to a shared Notification Type', () => { const rawNotification = createMockFeatureAnnouncementRaw(); - const result = processFeatureAnnouncement(rawNotification); + const result = processFeatureAnnouncement(rawNotification) as Extract< + INotification, + { type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT } + >; expect(result.id).toBe(rawNotification.data.id); expect(result.type).toBe(TRIGGER_TYPES.FEATURES_ANNOUNCEMENT); diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts index d8fe8bc54a6..7033e0451f6 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts @@ -1,13 +1,6 @@ import type { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; import type { INotification } from '../types/notification/notification'; - -const ONE_DAY_MS = 1000 * 60 * 60 * 24; - -const shouldAutoExpire = (oldDate: Date) => { - const differenceInTime = Date.now() - oldDate.getTime(); - const differenceInDays = differenceInTime / ONE_DAY_MS; - return differenceInDays >= 90; -}; +import { shouldAutoExpire } from '../utils/should-auto-expire'; /** * Checks if a feature announcement should be read. diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.test.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.test.ts index b228cdd4a6d..f91a0b36a82 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.test.ts @@ -4,7 +4,10 @@ import { } from './process-notifications'; import type { TRIGGER_TYPES } from '../constants/notification-schema'; import { createMockFeatureAnnouncementRaw } from '../mocks/mock-feature-announcements'; -import { createMockNotificationEthSent } from '../mocks/mock-raw-notifications'; +import { + createMockNotificationEthSent, + createMockPlatformNotification, +} from '../mocks/mock-raw-notifications'; import { createMockSnapNotification } from '../mocks/mock-snap-notification'; describe('process-notifications - processNotification()', () => { @@ -20,6 +23,12 @@ describe('process-notifications - processNotification()', () => { expect(result).toBeDefined(); }); + // More thorough tests are found in the specific process + it('maps Platform Notification to a shared Notification Type', () => { + const result = processNotification(createMockPlatformNotification()); + expect(result).toBeDefined(); + }); + // More thorough tests are found in the specific process it('maps Snap Notification to shared Notification Type', () => { const result = processNotification(createMockSnapNotification()); diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.ts index 69d053f5972..b1621e739db 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.ts @@ -1,21 +1,25 @@ +import { processAPINotifications } from './process-api-notifications'; import { isFeatureAnnouncementRead, processFeatureAnnouncement, } from './process-feature-announcement'; -import { processOnChainNotification } from './process-onchain-notifications'; import { processSnapNotification } from './process-snap-notifications'; -import { TRIGGER_TYPES } from '../constants/notification-schema'; +import { + TRIGGER_TYPES, + NOTIFICATION_API_TRIGGER_TYPES_SET, +} from '../constants/notification-schema'; import type { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; import type { INotification, RawNotificationUnion, } from '../types/notification/notification'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; +import type { NormalisedAPINotification } from '../types/notification-api/notification-api'; import type { RawSnapNotification } from '../types/snaps'; const isOnChainNotification = ( n: RawNotificationUnion, -): n is OnChainRawNotification => Object.values(TRIGGER_TYPES).includes(n.type); +): n is NormalisedAPINotification => + NOTIFICATION_API_TRIGGER_TYPES_SET.has(n.type); const isFeatureAnnouncement = ( n: RawNotificationUnion, @@ -56,7 +60,7 @@ export function processNotification( } if (isOnChainNotification(notification)) { - return processOnChainNotification(notification); + return processAPINotifications(notification); } return exhaustedAllCases(notification as never); diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-onchain-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-onchain-notifications.ts deleted file mode 100644 index 8d135c50ad1..00000000000 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-onchain-notifications.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { INotification } from '../types/notification/notification'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; - -/** - * Processes On-Chain notifications to a normalized notification - * - * @param notification - On-Chain Notification - * @returns Normalized Notification - */ -export function processOnChainNotification( - notification: OnChainRawNotification, -): INotification { - return { - ...notification, - id: notification.id, - createdAt: new Date(notification.created_at).toISOString(), - isRead: !notification.unread, - }; -} diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.test.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.test.ts index d6014a2cfec..5e9a0bf87d7 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.test.ts @@ -1,17 +1,19 @@ import { processSnapNotification } from './process-snap-notifications'; +import type { INotification } from '..'; import { TRIGGER_TYPES } from '../constants'; import { createMockSnapNotification } from '../mocks'; describe('process-snap-notifications - processSnapNotification()', () => { it('processes a Raw Snap Notification to a shared Notification Type', () => { const rawNotification = createMockSnapNotification(); - const result = processSnapNotification(rawNotification); + const result = processSnapNotification(rawNotification) as Extract< + INotification, + { type: TRIGGER_TYPES.SNAP } + >; expect(result.type).toBe(TRIGGER_TYPES.SNAP); expect(result.isRead).toBe(false); expect(result.data).toBeDefined(); - // @ts-expect-error readDate property is guaranteed to exist - // as we're dealing with a snap notification expect(result.readDate).toBeNull(); }); }); diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/onchain-notifications.test.ts b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts similarity index 73% rename from packages/notification-services-controller/src/NotificationServicesController/services/onchain-notifications.test.ts rename to packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts index e8b9f2e2793..b87260dc4e5 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/onchain-notifications.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.test.ts @@ -1,26 +1,29 @@ -import * as OnChainNotifications from './onchain-notifications'; +import * as OnChainNotifications from './api-notifications'; import { mockGetOnChainNotificationsConfig, mockUpdateOnChainNotifications, - mockGetOnChainNotifications, + mockGetAPINotifications, mockMarkNotificationsAsRead, } from '../__fixtures__/mockServices'; +import { + createMockNotificationERC20Sent, + createMockPlatformNotification, +} from '../mocks'; const MOCK_BEARER_TOKEN = 'MOCK_BEARER_TOKEN'; const MOCK_ADDRESSES = ['0x123', '0x456', '0x789']; -describe('On Chain Notifications - getOnChainNotificationsConfig()', () => { +describe('On Chain Notifications - getAPINotificationsConfig()', () => { it('should return notification config for addresses', async () => { const mockEndpoint = mockGetOnChainNotificationsConfig({ status: 200, body: [{ address: '0xTestAddress', enabled: true }], }); - const result = - await OnChainNotifications.getOnChainNotificationsConfigCached( - MOCK_BEARER_TOKEN, - MOCK_ADDRESSES, - ); + const result = await OnChainNotifications.getNotificationsApiConfigCached( + MOCK_BEARER_TOKEN, + MOCK_ADDRESSES, + ); expect(mockEndpoint.isDone()).toBe(true); expect(result).toStrictEqual([{ address: '0xTestAddress', enabled: true }]); @@ -29,11 +32,10 @@ describe('On Chain Notifications - getOnChainNotificationsConfig()', () => { it('should bail early if given a list of empty addresses', async () => { const mockEndpoint = mockGetOnChainNotificationsConfig(); - const result = - await OnChainNotifications.getOnChainNotificationsConfigCached( - MOCK_BEARER_TOKEN, - [], - ); + const result = await OnChainNotifications.getNotificationsApiConfigCached( + MOCK_BEARER_TOKEN, + [], + ); expect(mockEndpoint.isDone()).toBe(false); // bailed early before API was called expect(result).toStrictEqual([]); @@ -45,11 +47,10 @@ describe('On Chain Notifications - getOnChainNotificationsConfig()', () => { body: { error: 'mock api failure' }, }); - const result = - await OnChainNotifications.getOnChainNotificationsConfigCached( - MOCK_BEARER_TOKEN, - MOCK_ADDRESSES, - ); + const result = await OnChainNotifications.getNotificationsApiConfigCached( + MOCK_BEARER_TOKEN, + MOCK_ADDRESSES, + ); expect(mockBadEndpoint.isDone()).toBe(true); expect(result).toStrictEqual([]); @@ -112,13 +113,15 @@ describe('On Chain Notifications - updateOnChainNotifications()', () => { }); }); -describe('On Chain Notifications - getOnChainNotifications()', () => { +describe('On Chain Notifications - getAPINotifications()', () => { it('should return a list of notifications', async () => { - const mockEndpoint = mockGetOnChainNotifications(); + const mockEndpoint = mockGetAPINotifications(); - const result = await OnChainNotifications.getOnChainNotifications( + const result = await OnChainNotifications.getAPINotifications( MOCK_BEARER_TOKEN, MOCK_ADDRESSES, + 'en', + 'extension', ); expect(mockEndpoint.isDone()).toBe(true); @@ -126,10 +129,12 @@ describe('On Chain Notifications - getOnChainNotifications()', () => { }); it('should bail early when a list of empty addresses is provided', async () => { - const mockEndpoint = mockGetOnChainNotifications(); - const result = await OnChainNotifications.getOnChainNotifications( + const mockEndpoint = mockGetAPINotifications(); + const result = await OnChainNotifications.getAPINotifications( MOCK_BEARER_TOKEN, [], + 'en', + 'extension', ); expect(mockEndpoint.isDone()).toBe(false); // API was not called @@ -137,14 +142,16 @@ describe('On Chain Notifications - getOnChainNotifications()', () => { }); it('should return an empty array if endpoint fails', async () => { - const mockBadEndpoint = mockGetOnChainNotifications({ + const mockBadEndpoint = mockGetAPINotifications({ status: 500, body: { error: 'mock api failure' }, }); - const result = await OnChainNotifications.getOnChainNotifications( + const result = await OnChainNotifications.getAPINotifications( MOCK_BEARER_TOKEN, MOCK_ADDRESSES, + 'en', + 'extension', ); expect(mockBadEndpoint.isDone()).toBe(true); @@ -153,43 +160,41 @@ describe('On Chain Notifications - getOnChainNotifications()', () => { }); it('should send correct request body format with addresses', async () => { - const mockEndpoint = mockGetOnChainNotifications(); + const mockEndpoint = mockGetAPINotifications(); - const result = await OnChainNotifications.getOnChainNotifications( + const result = await OnChainNotifications.getAPINotifications( MOCK_BEARER_TOKEN, MOCK_ADDRESSES, + 'en', + 'extension', ); expect(mockEndpoint.isDone()).toBe(true); expect(result.length > 0).toBe(true); }); - it('should filter out notifications without data.kind', async () => { - const mockEndpoint = mockGetOnChainNotifications({ + it('should filter out notifications invalid notifications', async () => { + const mockEndpoint = mockGetAPINotifications({ status: 200, body: [ - { - id: '1', - data: { kind: 'eth_sent' }, - }, + createMockNotificationERC20Sent(), { id: '2', data: {}, // missing kind }, - { - id: '3', - data: { kind: 'eth_received' }, - }, + createMockPlatformNotification(), ], }); - const result = await OnChainNotifications.getOnChainNotifications( + const result = await OnChainNotifications.getAPINotifications( MOCK_BEARER_TOKEN, MOCK_ADDRESSES, + 'en', + 'extension', ); expect(mockEndpoint.isDone()).toBe(true); - expect(result).toHaveLength(2); // Should filter out the one without kind + expect(result).toHaveLength(2); // Should filter out the invalid notification }); }); diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/onchain-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts similarity index 72% rename from packages/notification-services-controller/src/NotificationServicesController/services/onchain-notifications.ts rename to packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts index 40bb2f93d39..a3589d9766e 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/onchain-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/api-notifications.ts @@ -1,11 +1,12 @@ import log from 'loglevel'; import { notificationsConfigCache } from './notification-config-cache'; -import { toRawOnChainNotification } from '../../shared/to-raw-notification'; +import { toRawAPINotification } from '../../shared/to-raw-notification'; import type { - OnChainRawNotification, - UnprocessedOnChainRawNotification, -} from '../types/on-chain-notification/on-chain-notification'; + NormalisedAPINotification, + Schema, + UnprocessedRawNotification, +} from '../types/notification-api'; import { makeApiCall } from '../utils/utils'; export type NotificationTrigger = { @@ -25,10 +26,10 @@ export const TRIGGER_API_NOTIFICATIONS_QUERY_ENDPOINT = `${TRIGGER_API}/api/v2/n export const TRIGGER_API_NOTIFICATIONS_ENDPOINT = `${TRIGGER_API}/api/v2/notifications`; // Lists notifications for each account provided -export const NOTIFICATION_API_LIST_ENDPOINT = `${NOTIFICATION_API}/api/v2/notifications`; +export const NOTIFICATION_API_LIST_ENDPOINT = `${NOTIFICATION_API}/api/v3/notifications`; // Makrs notifications as read -export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = `${NOTIFICATION_API}/api/v2/notifications/mark-as-read`; +export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = `${NOTIFICATION_API}/api/v3/notifications/mark-as-read`; /** * fetches notification config (accounts enabled vs disabled) @@ -39,7 +40,7 @@ export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = `${NOTIFICATION_API}/a * NOTE this is cached for 1s to prevent multiple update calls * @returns object of notification config, or null if missing */ -export async function getOnChainNotificationsConfigCached( +export async function getNotificationsApiConfigCached( bearerToken: string, addresses: string[], ) { @@ -112,41 +113,53 @@ export async function updateOnChainNotifications( * * @param bearerToken - The JSON Web Token used for authentication in the API call. * @param addresses - List of addresses - * @returns A promise that resolves to an array of OnChainRawNotification objects. If no triggers are enabled or an error occurs, it may return an empty array. + * @param locale - to generate translated notifications + * @param platform - filter notifications for specific platforms ('extension' | 'mobile') + * @returns A promise that resolves to an array of NormalisedAPINotification objects. If no notifications are enabled or an error occurs, it may return an empty array. */ -export async function getOnChainNotifications( +export async function getAPINotifications( bearerToken: string, addresses: string[], -): Promise { + locale: string, + platform: 'extension' | 'mobile', +): Promise { if (addresses.length === 0) { return []; } - addresses = addresses.map((a) => a.toLowerCase()); + type RequestBody = + Schema.paths['/api/v3/notifications']['post']['requestBody']['content']['application/json']; + type APIResponse = + Schema.paths['/api/v3/notifications']['post']['responses']['200']['content']['application/json']; - type RequestBody = { address: string }[]; - const body: RequestBody = addresses.map((address) => ({ address })); + const body: RequestBody = { + addresses: addresses.map((a) => a.toLowerCase()), + locale, + platform, + }; const notifications = await makeApiCall( bearerToken, NOTIFICATION_API_LIST_ENDPOINT, 'POST', body, ) - .then((r) => - r.ok ? r.json() : null, - ) + .then((r) => (r.ok ? r.json() : null)) .catch(() => null); // Transform and sort notifications const transformedNotifications = notifications - ?.map((n): OnChainRawNotification | undefined => { - if (!n.data?.kind) { + ?.map((n): UnprocessedRawNotification | undefined => { + if (!n.notification_type) { return undefined; } - return toRawOnChainNotification(n); + try { + return toRawAPINotification(n); + } catch { + return undefined; + } }) - .filter((n): n is OnChainRawNotification => Boolean(n)); + .filter((n): n is NormalisedAPINotification => Boolean(n)); return transformedNotifications ?? []; } @@ -168,12 +181,18 @@ export async function markNotificationsAsRead( return; } + type ResponseBody = + Schema.paths['/api/v3/notifications/mark-as-read']['post']['requestBody']['content']['application/json']; + const body: ResponseBody = { + ids: notificationIds, + }; + try { await makeApiCall( bearerToken, NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, 'POST', - { ids: notificationIds }, + body, ); } catch (err) { log.error('Error marking notifications as read:', err); diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts b/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts index 1375e397765..5352bbbe3c3 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts @@ -2,6 +2,7 @@ import { getFeatureAnnouncementNotifications, getFeatureAnnouncementUrl, } from './feature-announcements'; +import type { INotification } from '..'; import { mockFetchFeatureAnnouncementNotifications } from '../__fixtures__/mockServices'; import { TRIGGER_TYPES } from '../constants/notification-schema'; import { createMockFeatureAnnouncementAPIResult } from '../mocks/mock-feature-announcements'; @@ -85,7 +86,10 @@ describe('Feature Announcement Notifications', () => { expect(notifications).toHaveLength(1); mockEndpoint.done(); - const resultNotification = notifications[0]; + const resultNotification = notifications[0] as Extract< + INotification, + { type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT } + >; expect(resultNotification).toStrictEqual( expect.objectContaining({ id: 'dont-miss-out-on-airdrops-and-new-nft-mints', diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/perp-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/services/perp-notifications.ts index a74eb91d362..1b28216776d 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/perp-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/perp-notifications.ts @@ -20,7 +20,6 @@ export async function createPerpOrderNotification( ) { try { await createServicePolicy().execute(async () => { - // console.log('called'); return successfulFetch(PERPS_API_CREATE_ORDERS, { method: 'POST', headers: { diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/index.ts b/packages/notification-services-controller/src/NotificationServicesController/types/index.ts index bbb32e4a58b..d2e18d1c996 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/index.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/index.ts @@ -1,5 +1,5 @@ export type * from './feature-announcement'; +export type * from './notification-api'; export type * from './notification'; -export type * from './on-chain-notification'; export type * from './snaps/snaps'; export type * from './perps'; diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/index.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/index.ts new file mode 100644 index 00000000000..1eea6328c95 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/index.ts @@ -0,0 +1,2 @@ +export type * from './notification-api'; +export type * as Schema from './schema'; diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts similarity index 52% rename from packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts rename to packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts index 844bd95d837..2f7dd6b7b0d 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts @@ -23,52 +23,74 @@ export type Data_ERC20Received = components['schemas']['Data_ERC20Received']; export type Data_ERC721Sent = components['schemas']['Data_ERC721Sent']; export type Data_ERC721Received = components['schemas']['Data_ERC721Received']; -// Web3Notifications -export type Data_AaveV3HealthFactor = - components['schemas']['Data_AaveV3HealthFactor']; -export type Data_EnsExpiration = components['schemas']['Data_EnsExpiration']; -export type Data_LidoStakingRewards = - components['schemas']['Data_LidoStakingRewards']; -export type Data_RocketpoolStakingRewards = - components['schemas']['Data_RocketpoolStakingRewards']; -export type Data_NotionalLoanExpiration = - components['schemas']['Data_NotionalLoanExpiration']; -export type Data_SparkFiHealthFactor = - components['schemas']['Data_SparkFiHealthFactor']; +type Notification = components['schemas']['NotificationOutputV3'][number]; +type PlatformNotification = Extract< + Notification, + { notification_type: 'platform' } +>; +type OnChainNotification = Extract< + Notification, + { notification_type: 'on-chain' } +>; -type Notification = - | components['schemas']['WalletNotification'] - | components['schemas']['Web3Notification']; type ConvertToEnum = { [K in TRIGGER_TYPES]: Kind extends `${K}` ? K : never; }[TRIGGER_TYPES]; /** * Type-Computation. - * 1. Adds a `type` field to the notification, it converts the schema type into the ENUM we use. - * 2. It ensures that the `data` field is the correct Notification data for this `type` - * - The `Compute` utility merges the intersections (`&`) for a prettier type. + * Adds a `type` field to on-chain notifications for easier enum checking. + * Preserves the original nested payload structure. */ -type NormalizeNotification< - N extends Notification, - NotificationDataKinds extends string = NonNullable['kind'], +type NormalizeOnChainNotification< + N extends OnChainNotification = OnChainNotification, + NotificationDataKinds extends string = NonNullable< + N['payload']['data'] + >['kind'], > = { [K in NotificationDataKinds]: Compute< - Omit & { + Omit & { type: ConvertToEnum; - data: Extract; + payload: Compute< + Omit & { + data: Extract, { kind: K }>; + } + >; } >; }[NotificationDataKinds]; +/** + * Type-Computation. + * Adds a `type` field to platform notifications for easier enum checking. + * Preserves the original nested payload structure. + */ +type NormalizePlatformNotification< + N extends PlatformNotification = PlatformNotification, + NotificationKind extends string = N['notification_type'], +> = { + [K in NotificationKind]: Compute< + N & { + type: ConvertToEnum; + } + >; +}[NotificationKind]; + export type OnChainRawNotification = Compute< - | NormalizeNotification - | NormalizeNotification + NormalizeOnChainNotification >; -export type UnprocessedOnChainRawNotification = Notification; +export type PlatformRawNotification = Compute< + NormalizePlatformNotification +>; + +export type UnprocessedRawNotification = Notification; + +export type NormalisedAPINotification = + | OnChainRawNotification + | PlatformRawNotification; export type OnChainRawNotificationsWithNetworkFields = Extract< OnChainRawNotification, - { data: { network_fee: unknown } } + { payload: { data: { network_fee: unknown } } } >; 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 new file mode 100644 index 00000000000..5ca867a94c9 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts @@ -0,0 +1,385 @@ +/* eslint-disable jsdoc/tag-lines */ +/* eslint-disable jsdoc/check-tag-names */ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + * Script: `npx openapi-typescript -o ./schema.d.ts` + */ + +export type paths = { + '/api/v3/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) */ + 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']['NotificationOutputV3']; + }; + }; + /** @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/v3/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 = { + schemas: { + NotificationInputV3: { + /** @example en-US */ + locale: string; + addresses: string[]; + platform: 'extension' | 'mobile'; + }; + NotificationOutputV3: ( + | components['schemas']['PlatformNotification'] + | components['schemas']['OnChainNotification'] + )[]; + PlatformNotification: { + /** + * Format: uuid + * @example 3fa85f64-5717-4562-b3fc-2c963f66afa6 + */ + id: string; + /** @enum {string} */ + notification_type: 'platform'; + /** @example false */ + unread: boolean; + template: components['schemas']['LocalizedNotification']; + /** + * Format: date-time + * @example 2025-10-09T09:45:34.202Z + */ + created_at: string; + }; + LocalizedNotification: { + image_url: string; + cta?: components['schemas']['LocalizedNotificationCTA']; + title: string; + body: string; + }; + LocalizedNotificationCTA: { + content: string; + link: string; + }; + OnChainNotification: { + /** + * Format: uuid + * @example 3fa85f64-5717-4562-b3fc-2c963f66afa6 + */ + id: string; + /** @enum {string} */ + notification_type: 'on-chain'; + /** @example false */ + unread: boolean; + /** + * Format: date-time + * @example 2025-10-09T09:45:34.202Z + */ + created_at: string; + payload: components['schemas']['OnChainPayload']; + }; + OnChainPayload: { + /** @example 1 */ + chain_id: number; + /** @example 17485840 */ + block_number: number; + block_timestamp: string; + /** @example 0x881D40237659C251811CEC9c364ef91dC08D300C */ + tx_hash: string; + 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'; + network_fee: components['schemas']['NetworkFee']; + /** Format: decimal */ + rate: string; + token_in: components['schemas']['Token']; + token_out: components['schemas']['Token']; + }; + Data_LidoStakeCompleted: { + /** @enum {string} */ + kind: 'lido_stake_completed'; + network_fee: components['schemas']['NetworkFee']; + stake_in: components['schemas']['Stake']; + stake_out: components['schemas']['Stake']; + }; + Data_LidoWithdrawalRequested: { + /** @enum {string} */ + kind: 'lido_withdrawal_requested'; + network_fee: components['schemas']['NetworkFee']; + stake_in: components['schemas']['Stake']; + stake_out: components['schemas']['Stake']; + }; + Data_LidoStakeReadyToBeWithdrawn: { + /** @enum {string} */ + kind: 'lido_stake_ready_to_be_withdrawn'; + /** Format: decimal */ + request_id: string; + staked_eth: components['schemas']['Stake']; + }; + Data_LidoWithdrawalCompleted: { + /** @enum {string} */ + kind: 'lido_withdrawal_completed'; + network_fee: components['schemas']['NetworkFee']; + stake_in: components['schemas']['Stake']; + stake_out: components['schemas']['Stake']; + }; + Data_RocketPoolStakeCompleted: { + /** @enum {string} */ + kind: 'rocketpool_stake_completed'; + network_fee: components['schemas']['NetworkFee']; + stake_in: components['schemas']['Stake']; + stake_out: components['schemas']['Stake']; + }; + Data_RocketPoolUnstakeCompleted: { + /** @enum {string} */ + kind: 'rocketpool_unstake_completed'; + network_fee: components['schemas']['NetworkFee']; + stake_in: components['schemas']['Stake']; + stake_out: components['schemas']['Stake']; + }; + Data_ETHSent: { + /** @enum {string} */ + kind: 'eth_sent'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + amount: { + /** Format: decimal */ + usd: string; + /** Format: decimal */ + eth: string; + }; + }; + Data_ETHReceived: { + /** @enum {string} */ + kind: 'eth_received'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + amount: { + /** Format: decimal */ + usd: string; + /** Format: decimal */ + eth: string; + }; + }; + Data_ERC20Sent: { + /** @enum {string} */ + kind: 'erc20_sent'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + token: components['schemas']['Token']; + }; + Data_ERC20Received: { + /** @enum {string} */ + kind: 'erc20_received'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + token: components['schemas']['Token']; + }; + Data_ERC721Sent: { + /** @enum {string} */ + kind: 'erc721_sent'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + nft: components['schemas']['NFT']; + }; + Data_ERC721Received: { + /** @enum {string} */ + kind: 'erc721_received'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + nft: components['schemas']['NFT']; + }; + Data_ERC1155Sent: { + /** @enum {string} */ + kind: 'erc1155_sent'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + nft?: components['schemas']['NFT']; + }; + Data_ERC1155Received: { + /** @enum {string} */ + kind: 'erc1155_received'; + network_fee: components['schemas']['NetworkFee']; + /** Format: address */ + from: string; + /** Format: address */ + to: string; + nft?: components['schemas']['NFT']; + }; + NetworkFee: { + /** Format: decimal */ + gas_price: string; + /** Format: decimal */ + native_token_price_in_usd: string; + }; + Token: { + /** Format: address */ + address: string; + symbol: string; + name: string; + /** Format: decimal */ + amount: string; + /** Format: int32 */ + decimals: string; + /** Format: uri */ + image: string; + /** Format: decimal */ + usd: string; + }; + NFT: { + name: string; + token_id: string; + /** Format: uri */ + image: string; + collection: { + /** Format: address */ + address: string; + name: string; + symbol: string; + /** Format: uri */ + image: string; + }; + }; + Stake: { + /** Format: address */ + address: string; + symbol: string; + name: string; + /** Format: decimal */ + amount: string; + /** Format: int32 */ + decimals: string; + /** Format: uri */ + image: string; + /** Format: decimal */ + usd: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +}; +export type $defs = Record; +export type operations = Record; diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts index 991e4f627c8..aa7102aa278 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts @@ -1,5 +1,5 @@ import type { FeatureAnnouncementRawNotification } from '../feature-announcement/feature-announcement'; -import type { OnChainRawNotification } from '../on-chain-notification/on-chain-notification'; +import type { NormalisedAPINotification } from '../notification-api/notification-api'; import type { RawSnapNotification } from '../snaps'; import type { Compute } from '../type-utils'; @@ -10,7 +10,7 @@ export type BaseNotification = { }; export type RawNotificationUnion = - | OnChainRawNotification + | NormalisedAPINotification | FeatureAnnouncementRawNotification | RawSnapNotification; @@ -22,20 +22,10 @@ export type RawNotificationUnion = */ export type INotification = Compute< | (FeatureAnnouncementRawNotification & BaseNotification) - | (OnChainRawNotification & BaseNotification) + | (NormalisedAPINotification & BaseNotification) | (RawSnapNotification & BaseNotification & { readDate?: string | null }) >; -// NFT -export type NFT = { - token_id: string; - image: string; - collection?: { - name: string; - image: string; - }; -}; - export type MarkAsReadNotificationsParam = Pick< INotification, 'id' | 'type' | 'isRead' diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/index.ts b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/index.ts deleted file mode 100644 index 47a00df68a7..00000000000 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type * from './on-chain-notification'; diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts deleted file mode 100644 index 8f51dcde380..00000000000 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts +++ /dev/null @@ -1,708 +0,0 @@ -/* eslint-disable jsdoc/tag-lines */ -/* eslint-disable jsdoc/check-tag-names */ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - * Script: `npx openapi-typescript -o ./schema.d.ts` - */ - -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export type paths = { - '/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'] - | components['schemas']['Web3Notification'] - )[]; - }; - }; - }; - }; - 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; - }; - '/api/internal/v1/topics': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all topics created (internal) */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully fetched all topics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Topic'][]; - }; - }; - }; - }; - put?: never; - /** Create a new topic (internal) */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - name: string; - desc?: string; - }; - }; - }; - responses: { - /** @description Successfully created a new topic */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/internal/v1/subtopics': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all sub-topics created (internal) */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully fetched all subtopics */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SubTopic'][]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/internal/v1/global-notifications': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Insert a new Global Notification (internal) */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['GlobalNotificationWrite']; - }; - }; - responses: { - /** @description Successfully created a new global notification */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/global-notifications': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all Global Notifications for a UserID */ - get: { - parameters: { - query: { - /** @description Platform(s) to filter notifications by */ - platform: ('portfolio' | 'extension' | 'mobile')[]; - /** @description Delivery channel(s) to filter notifications by */ - deliveryChannel: ('inbox' | 'push')[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully fetched global notifications */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GlobalNotification'][]; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/user-preferences': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all preferences for a UserID */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully fetched preferences */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Topic'][]; - }; - }; - /** @description User not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - /** Update Preferences for a UserID */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - topics: string[]; - }; - }; - }; - responses: { - /** @description Successfully updated topics preferences */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -}; -export type webhooks = Record; -export type components = { - schemas: { - GlobalNotification: { - title: string; - body: string; - /** Format: date-time */ - created_at: string; - }; - GlobalNotificationWrite: { - title: string; - body: string; - 'sub-topic': string; - platforms: ('portfolio' | 'extension' | 'mobile')[]; - delivery_channels: ('inbox' | 'push')[]; - }; - Topic: { - name: string; - description?: string; - /** Format: date-time */ - created_at?: string; - }; - SubTopic: { - name: string; - /** Format: date-time */ - created_at?: 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']; - }; - Web3Notification: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - trigger_id: string; - /** @example 1 */ - chain_id: number; - /** @example false */ - unread: boolean; - /** Format: date-time */ - created_at: string; - /** Format: address */ - address: string; - data?: - | components['schemas']['Data_AaveV3HealthFactor'] - | components['schemas']['Data_EnsExpiration'] - | components['schemas']['Data_LidoStakingRewards'] - | components['schemas']['Data_RocketpoolStakingRewards'] - | components['schemas']['Data_NotionalLoanExpiration'] - | components['schemas']['Data_SparkFiHealthFactor']; - }; - Data_MetamaskSwapCompleted: { - /** @enum {string} */ - kind: 'metamask_swap_completed'; - network_fee: components['schemas']['NetworkFee']; - /** Format: decimal */ - rate: string; - token_in: components['schemas']['Token']; - token_out: components['schemas']['Token']; - }; - Data_LidoStakeCompleted: { - /** @enum {string} */ - kind: 'lido_stake_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_LidoWithdrawalRequested: { - /** @enum {string} */ - kind: 'lido_withdrawal_requested'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_LidoStakeReadyToBeWithdrawn: { - /** @enum {string} */ - kind: 'lido_stake_ready_to_be_withdrawn'; - /** Format: decimal */ - request_id: string; - staked_eth: components['schemas']['Stake']; - }; - Data_LidoWithdrawalCompleted: { - /** @enum {string} */ - kind: 'lido_withdrawal_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_RocketPoolStakeCompleted: { - /** @enum {string} */ - kind: 'rocketpool_stake_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_RocketPoolUnstakeCompleted: { - /** @enum {string} */ - kind: 'rocketpool_unstake_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_ETHSent: { - /** @enum {string} */ - kind: 'eth_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - amount: { - /** Format: decimal */ - usd: string; - /** Format: decimal */ - eth: string; - }; - }; - Data_ETHReceived: { - /** @enum {string} */ - kind: 'eth_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - amount: { - /** Format: decimal */ - usd: string; - /** Format: decimal */ - eth: string; - }; - }; - Data_ERC20Sent: { - /** @enum {string} */ - kind: 'erc20_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - token: components['schemas']['Token']; - }; - Data_ERC20Received: { - /** @enum {string} */ - kind: 'erc20_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - token: components['schemas']['Token']; - }; - Data_ERC721Sent: { - /** @enum {string} */ - kind: 'erc721_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft: components['schemas']['NFT']; - }; - Data_ERC721Received: { - /** @enum {string} */ - kind: 'erc721_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft: components['schemas']['NFT']; - }; - Data_ERC1155Sent: { - /** @enum {string} */ - kind: 'erc1155_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft?: components['schemas']['NFT']; - }; - Data_ERC1155Received: { - /** @enum {string} */ - kind: 'erc1155_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft?: components['schemas']['NFT']; - }; - Data_AaveV3HealthFactor: { - /** @enum {string} */ - kind: 'aave_v3_health_factor'; - /** @example 1 */ - chainId: number; - /** Format: decimal */ - healthFactor: string; - /** Format: decimal */ - threshold: string; - }; - Data_EnsExpiration: { - /** @enum {string} */ - kind: 'ens_expiration'; - chainId: number; - reverseEnsName: string; - /** Format: date-time */ - expirationDateIso: string; - /** @example 86400 */ - reminderDelayInSeconds: number; - }; - Data_LidoStakingRewards: { - /** @enum {string} */ - kind: 'lido_staking_rewards'; - chainId: number; - /** Format: decimal */ - currentStethBalance: string; - /** Format: decimal */ - currentEthValue: string; - /** Format: decimal */ - estimatedTotalRewardInPeriod: string; - /** @example 1 */ - daysSinceLastNotification: number; - /** @example 1 */ - notificationIntervalDays: number; - }; - Data_NotionalLoanExpiration: { - /** @enum {string} */ - kind: 'notional_loan_expiration'; - chainId: number; - loans: { - /** Format: decimal */ - amount: string; - symbol: string; - /** Format: date-time */ - maturityDateIso: string; - }[]; - /** @example 86400 */ - reminderDelayInSeconds: number; - }; - Data_RocketpoolStakingRewards: { - /** @enum {string} */ - kind: 'rocketpool_staking_rewards'; - chainId: number; - /** Format: decimal */ - currentRethBalance: string; - /** Format: decimal */ - currentEthValue: string; - /** Format: decimal */ - estimatedTotalRewardInPeriod: string; - /** @example 1 */ - daysSinceLastNotification: number; - /** @example 1 */ - notificationIntervalDays: number; - }; - Data_SparkFiHealthFactor: { - /** @enum {string} */ - kind: 'spark_fi_health_factor'; - chainId: number; - /** Format: decimal */ - healthFactor: string; - /** Format: decimal */ - threshold: string; - }; - NetworkFee: { - /** Format: decimal */ - gas_price: string; - /** Format: decimal */ - native_token_price_in_usd: string; - }; - Token: { - /** Format: address */ - address: string; - symbol: string; - name: string; - /** Format: decimal */ - amount: string; - /** Format: int32 */ - decimals: string; - /** Format: uri */ - image: string; - /** Format: decimal */ - usd: string; - }; - NFT: { - name: string; - token_id: string; - /** Format: uri */ - image: string; - collection: { - /** Format: address */ - address: string; - name: string; - symbol: string; - /** Format: uri */ - image: string; - }; - }; - Stake: { - /** Format: address */ - address: string; - symbol: string; - name: string; - /** Format: decimal */ - amount: string; - /** Format: int32 */ - decimals: string; - /** Format: uri */ - image: string; - /** Format: decimal */ - usd: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -}; -export type $defs = Record; -export type operations = Record; diff --git a/packages/notification-services-controller/src/NotificationServicesController/utils/should-auto-expire.ts b/packages/notification-services-controller/src/NotificationServicesController/utils/should-auto-expire.ts new file mode 100644 index 00000000000..c6316032055 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesController/utils/should-auto-expire.ts @@ -0,0 +1,8 @@ +const ONE_DAY_MS = 1000 * 60 * 60 * 24; +const MAX_DAYS = 30; + +export const shouldAutoExpire = (oldDate: Date) => { + const differenceInTime = Date.now() - oldDate.getTime(); + const differenceInDays = differenceInTime / ONE_DAY_MS; + return differenceInDays >= MAX_DAYS; +}; diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts b/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts index 978bf7cfdee..125487b9a60 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts @@ -14,7 +14,7 @@ import log from 'loglevel'; import type { Types } from '../../../NotificationServicesController'; import { Processors } from '../../../NotificationServicesController'; -import { toRawOnChainNotification } from '../../../shared/to-raw-notification'; +import { toRawAPINotification } from '../../../shared/to-raw-notification'; import type { PushNotificationEnv } from '../../types/firebase'; declare const self: ServiceWorkerGlobalScope; @@ -127,14 +127,16 @@ export async function listenToPushNotificationsReceived( // eslint-disable-next-line @typescript-eslint/no-misused-promises async (payload: MessagePayload) => { try { - const data: Types.UnprocessedOnChainRawNotification | undefined = - payload?.data?.data ? JSON.parse(payload?.data?.data) : undefined; + const data: Types.UnprocessedRawNotification | undefined = payload?.data + ?.data + ? JSON.parse(payload?.data?.data) + : undefined; if (!data) { return; } - const notificationData = toRawOnChainNotification(data); + const notificationData = toRawAPINotification(data); const notification = Processors.processNotification(notificationData); await handler(notification); } catch (error) { diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/utils/get-notification-message.ts b/packages/notification-services-controller/src/NotificationServicesPushController/utils/get-notification-message.ts index f22e56b0930..56e439fba07 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/utils/get-notification-message.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/utils/get-notification-message.ts @@ -40,12 +40,14 @@ export type TranslationKeys = { type PushNotificationMessage = { title: string; description: string; + ctaLink?: string; }; type NotificationMessage = { - title: string | null; - defaultDescription: string | null; + title: (n: N) => string | null; + defaultDescription: (n: N) => string | null; getDescription?: (n: N) => string | null; + link?: (n: N) => string | null; }; type NotificationMessageDict = { @@ -78,14 +80,13 @@ export const createOnChainPushNotificationMessages = ( return { erc20_sent: { - title: t('pushPlatformNotificationsFundsSentTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsSentDescriptionDefault', - ), + title: () => t('pushPlatformNotificationsFundsSentTitle'), + defaultDescription: () => + t('pushPlatformNotificationsFundsSentDescriptionDefault'), getDescription: (n) => { - const symbol = n?.data?.token?.symbol; - const tokenAmount = n?.data?.token?.amount; - const tokenDecimals = n?.data?.token?.decimals; + const symbol = n?.payload?.data?.token?.symbol; + const tokenAmount = n?.payload?.data?.token?.amount; + const tokenDecimals = n?.payload?.data?.token?.decimals; if (!symbol || !tokenAmount || !tokenDecimals) { return null; } @@ -101,13 +102,12 @@ export const createOnChainPushNotificationMessages = ( }, }, eth_sent: { - title: t('pushPlatformNotificationsFundsSentTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsSentDescriptionDefault', - ), + title: () => t('pushPlatformNotificationsFundsSentTitle'), + defaultDescription: () => + t('pushPlatformNotificationsFundsSentDescriptionDefault'), getDescription: (n) => { - const symbol = getChainSymbol(n?.chain_id); - const tokenAmount = n?.data?.amount?.eth; + const symbol = getChainSymbol(n?.payload?.chain_id); + const tokenAmount = n?.payload?.data?.amount?.eth; if (!symbol || !tokenAmount) { return null; } @@ -123,14 +123,13 @@ export const createOnChainPushNotificationMessages = ( }, }, erc20_received: { - title: t('pushPlatformNotificationsFundsReceivedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsReceivedDescriptionDefault', - ), + title: () => t('pushPlatformNotificationsFundsReceivedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsFundsReceivedDescriptionDefault'), getDescription: (n) => { - const symbol = n?.data?.token?.symbol; - const tokenAmount = n?.data?.token?.amount; - const tokenDecimals = n?.data?.token?.decimals; + const symbol = n?.payload?.data?.token?.symbol; + const tokenAmount = n?.payload?.data?.token?.amount; + const tokenDecimals = n?.payload?.data?.token?.decimals; if (!symbol || !tokenAmount || !tokenDecimals) { return null; } @@ -146,13 +145,12 @@ export const createOnChainPushNotificationMessages = ( }, }, eth_received: { - title: t('pushPlatformNotificationsFundsReceivedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsReceivedDescriptionDefault', - ), + title: () => t('pushPlatformNotificationsFundsReceivedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsFundsReceivedDescriptionDefault'), getDescription: (n) => { - const symbol = getChainSymbol(n?.chain_id); - const tokenAmount = n?.data?.amount?.eth; + const symbol = getChainSymbol(n?.payload?.chain_id); + const tokenAmount = n?.payload?.data?.amount?.eth; if (!symbol || !tokenAmount) { return null; } @@ -168,66 +166,75 @@ export const createOnChainPushNotificationMessages = ( }, }, metamask_swap_completed: { - title: t('pushPlatformNotificationsSwapCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsSwapCompletedDescription', - ), + title: () => t('pushPlatformNotificationsSwapCompletedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsSwapCompletedDescription'), }, erc721_sent: { - title: t('pushPlatformNotificationsNftSentTitle'), - defaultDescription: t('pushPlatformNotificationsNftSentDescription'), + title: () => t('pushPlatformNotificationsNftSentTitle'), + defaultDescription: () => + t('pushPlatformNotificationsNftSentDescription'), }, erc1155_sent: { - title: t('pushPlatformNotificationsNftSentTitle'), - defaultDescription: t('pushPlatformNotificationsNftSentDescription'), + title: () => t('pushPlatformNotificationsNftSentTitle'), + defaultDescription: () => + t('pushPlatformNotificationsNftSentDescription'), }, erc721_received: { - title: t('pushPlatformNotificationsNftReceivedTitle'), - defaultDescription: t('pushPlatformNotificationsNftReceivedDescription'), + title: () => t('pushPlatformNotificationsNftReceivedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsNftReceivedDescription'), }, erc1155_received: { - title: t('pushPlatformNotificationsNftReceivedTitle'), - defaultDescription: t('pushPlatformNotificationsNftReceivedDescription'), + title: () => t('pushPlatformNotificationsNftReceivedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsNftReceivedDescription'), }, rocketpool_stake_completed: { - title: t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription', - ), + title: () => + t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), + defaultDescription: () => + t( + 'pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription', + ), }, rocketpool_unstake_completed: { - title: t( - 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle', - ), - defaultDescription: t( - 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription', - ), + title: () => + t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle'), + defaultDescription: () => + t( + 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription', + ), }, lido_stake_completed: { - title: t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoStakeCompletedDescription', - ), + title: () => t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsStakingLidoStakeCompletedDescription'), }, lido_stake_ready_to_be_withdrawn: { - title: t( - 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle', - ), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription', - ), + title: () => + t('pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle'), + defaultDescription: () => + t( + 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription', + ), }, lido_withdrawal_requested: { - title: t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription', - ), + title: () => + t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription'), }, lido_withdrawal_completed: { - title: t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription', - ), + title: () => + t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), + defaultDescription: () => + t('pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription'), + }, + platform: { + title: (n) => n.template.title, + defaultDescription: (n) => n.template.body, + getDescription: (n) => n.template.body, }, }; }; @@ -272,14 +279,17 @@ export function createOnChainPushNotificationMessage( description = // eslint-disable-next-line @typescript-eslint/no-explicit-any notificationMessage?.getDescription?.(n as any) ?? - notificationMessage.defaultDescription ?? + // eslint-disable-next-line @typescript-eslint/no-explicit-any + notificationMessage.defaultDescription?.(n as any) ?? null; } catch { - description = notificationMessage.defaultDescription ?? null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + description = notificationMessage.defaultDescription?.(n as any) ?? null; } return { - title: notificationMessage.title ?? '', // Ensure title is always a string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + title: notificationMessage?.title?.(n as any) ?? '', // Ensure title is always a string description: description ?? '', // Fallback to empty string if null }; } diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts b/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts index c2d26024164..f98b99a6dfe 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts @@ -14,7 +14,7 @@ import log from 'loglevel'; import type { Types } from '../../NotificationServicesController'; import { Processors } from '../../NotificationServicesController'; -import { toRawOnChainNotification } from '../../shared/to-raw-notification'; +import { toRawAPINotification } from '../../shared/to-raw-notification'; import type { NotificationServicesPushControllerMessenger } from '../NotificationServicesPushController'; import type { PushNotificationEnv } from '../types/firebase'; @@ -128,14 +128,16 @@ async function listenToPushNotificationsReceived( // eslint-disable-next-line @typescript-eslint/no-misused-promises async (payload: MessagePayload) => { try { - const data: Types.UnprocessedOnChainRawNotification | undefined = - payload?.data?.data ? JSON.parse(payload?.data?.data) : undefined; + const data: Types.UnprocessedRawNotification | undefined = payload?.data + ?.data + ? JSON.parse(payload?.data?.data) + : undefined; if (!data) { return; } - const notificationData = toRawOnChainNotification(data); + const notificationData = toRawAPINotification(data); const notification = Processors.processNotification(notificationData); await handler(notification); } catch (error) { diff --git a/packages/notification-services-controller/src/shared/is-onchain-notification.test.ts b/packages/notification-services-controller/src/shared/is-onchain-notification.test.ts index 4d3be69df68..c44e15a6b13 100644 --- a/packages/notification-services-controller/src/shared/is-onchain-notification.test.ts +++ b/packages/notification-services-controller/src/shared/is-onchain-notification.test.ts @@ -1,6 +1,7 @@ import { isOnChainRawNotification } from '.'; import { createMockFeatureAnnouncementRaw, + createMockPlatformNotification, createMockNotificationEthSent, } from '../NotificationServicesController/mocks'; @@ -11,8 +12,13 @@ describe('is-onchain-notification - isOnChainRawNotification()', () => { expect(result).toBe(true); }); it('returns false if not OnChainRawNotification', () => { - const notification = createMockFeatureAnnouncementRaw(); - const result = isOnChainRawNotification(notification); - expect(result).toBe(false); + const testNotifications = [ + createMockFeatureAnnouncementRaw(), + createMockPlatformNotification(), + ]; + testNotifications.forEach((notification) => { + const result = isOnChainRawNotification(notification); + expect(result).toBe(false); + }); }); }); diff --git a/packages/notification-services-controller/src/shared/is-onchain-notification.ts b/packages/notification-services-controller/src/shared/is-onchain-notification.ts index b84587e8b61..252310bc83b 100644 --- a/packages/notification-services-controller/src/shared/is-onchain-notification.ts +++ b/packages/notification-services-controller/src/shared/is-onchain-notification.ts @@ -15,8 +15,7 @@ export function isOnChainRawNotification( // It is safe enough just to check "some" fields, and catch any errors down the line if the shape is bad. const isValidEnoughToBeOnChainNotification = [ assumed?.id, - assumed?.data, - assumed?.trigger_id, + assumed?.payload?.data, ].every((field) => field !== undefined); return isValidEnoughToBeOnChainNotification; } 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 8f180d1bfbc..acd8f0a04a5 100644 --- a/packages/notification-services-controller/src/shared/to-raw-notification.ts +++ b/packages/notification-services-controller/src/shared/to-raw-notification.ts @@ -1,7 +1,9 @@ import type { + UnprocessedRawNotification, + NormalisedAPINotification, OnChainRawNotification, - UnprocessedOnChainRawNotification, -} from 'src/NotificationServicesController/types'; + PlatformRawNotification, +} from 'src/NotificationServicesController/types/notification-api'; /** * A true "raw notification" does not have some fields that exist on this type. E.g. the `type` field. @@ -11,11 +13,34 @@ import type { * @param data - raw onchain notification * @returns a complete raw onchain notification */ -export function toRawOnChainNotification( - data: UnprocessedOnChainRawNotification, -): OnChainRawNotification { - return { - ...data, - type: data?.data?.kind, - } as OnChainRawNotification; +export function toRawAPINotification( + data: UnprocessedRawNotification, +): NormalisedAPINotification { + const exhaustedAllCases = (_: 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) { + throw new Error( + 'toRawAPINotification - No kind found for on-chain notification', + ); + } + return { + ...data, + type: data.payload.data.kind, + } as OnChainRawNotification; + } + + if (data.notification_type === 'platform') { + return { + ...data, + type: data.notification_type, + } as PlatformRawNotification; + } + + return exhaustedAllCases(data); }