Skip to content

Commit

Permalink
[CT-1040] Push Notifications (1 of 4) - Add Notification Package (#2185)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamfraser authored Sep 11, 2024
1 parent 7bca22d commit 2ba777e
Show file tree
Hide file tree
Showing 25 changed files with 1,562 additions and 10 deletions.
1 change: 1 addition & 0 deletions indexer/Dockerfile.bazooka.remote
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ COPY ./packages/kafka/ ./packages/kafka/
COPY ./packages/redis/ ./packages/redis/
COPY ./services/bazooka/ ./services/bazooka/
COPY ./packages/v4-proto-parser/ ./packages/v4-proto-parser/
COPY ./packages/notifications/ ./packages/notifications/

# Copy tsconfig in order to build typescript into javascript
COPY tsconfig.json ./
Expand Down
2 changes: 2 additions & 0 deletions indexer/Dockerfile.service.local
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ COPY ./packages/redis/package.json ./packages/redis/
COPY ./packages/v4-protos/package.json ./packages/v4-protos/
COPY ./packages/v4-proto-parser/package.json ./packages/v4-proto-parser/package.json
COPY ./packages/compliance/package.json ./packages/compliance/
COPY ./packages/notifications/package.json ./packages/notifications/

# Copy build files from all packages being run
COPY ./packages/base/build ./packages/base/build/
Expand All @@ -33,6 +34,7 @@ COPY ./packages/redis/build ./packages/redis/build/
COPY ./packages/v4-protos/build ./packages/v4-protos/build/
COPY ./packages/v4-proto-parser/build ./packages/v4-proto-parser/build/
COPY ./packages/compliance/build ./packages/compliance/build/
COPY ./packages/notifications/build ./packages/notifications/build/

# Copy package.json, build files, and environment files from service being run
COPY ./services/${service}/package.json ./services/${service}/
Expand Down
2 changes: 2 additions & 0 deletions indexer/Dockerfile.service.remote
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ COPY ./packages/redis/package.json ./packages/redis/
COPY ./packages/v4-protos/package.json ./packages/v4-protos/
COPY ./packages/v4-proto-parser/package.json ./packages/v4-proto-parser/package.json
COPY ./packages/compliance/package.json ./packages/compliance/
COPY ./packages/notifications/package.json ./packages/notifications/

# Copy build files from all packages being run
COPY ./packages/base/build ./packages/base/build/
Expand All @@ -33,6 +34,7 @@ COPY ./packages/redis/build ./packages/redis/build/
COPY ./packages/v4-protos/build ./packages/v4-protos/build/
COPY ./packages/v4-proto-parser/build ./packages/v4-proto-parser/build/
COPY ./packages/compliance/build ./packages/compliance/build/
COPY ./packages/notifications/build ./packages/notifications/build/

# Copy package.json, build files, and environment files from service being run
COPY ./services/${service}/package.json ./services/${service}/
Expand Down
3 changes: 3 additions & 0 deletions indexer/packages/notifications/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Service Level Variables

SERVICE_NAME=notifications
8 changes: 8 additions & 0 deletions indexer/packages/notifications/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
DB_NAME=dydx_dev
DB_USERNAME=dydx_dev
DB_PASSWORD=dydxserver123
PG_POOL_MAX=2
PG_POOL_MIN=1
DB_HOSTNAME=postgres
DB_READONLY_HOSTNAME=postgres
DB_PORT=5432
Empty file.
10 changes: 10 additions & 0 deletions indexer/packages/notifications/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SERVICE_NAME=notifications

FIREBASE_PROJECT_ID=projectID
FIREBASE_PRIVATE_KEY='-----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY-----'
FIREBASE_CLIENT_EMAIL=[email protected]

DB_NAME=dydx_test
DB_USERNAME=dydx_test
DB_PASSWORD=dydxserver123
DB_PORT=5436
11 changes: 11 additions & 0 deletions indexer/packages/notifications/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const baseConfig = require('./node_modules/@dydxprotocol-indexer/dev/.eslintrc');

module.exports = {
...baseConfig,

// Override the base configuraiton to set the correct tsconfigRootDir.
parserOptions: {
...baseConfig.parserOptions,
tsconfigRootDir: __dirname,
},
};
38 changes: 38 additions & 0 deletions indexer/packages/notifications/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Notifications

Notification package to create and send push notifications, using [Google Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging)

## Package Configuration

### Firebase Setup Requirements

To initialize Firebase correctly, this package requires the following three environment variables to be configured. These values can be obtained by downloading the service account file from the Firebase Admin Console:

- FIREBASE_PRIVATE_KEY_BASE64: A Base64-encoded private key string.
- FIREBASE_PROJECT_ID: The Firebase project ID as a string.
- FIREBASE_CLIENT_EMAIL: The client email associated with the Firebase project.

Each of these environment variables must be securely stored in AWS Secrets Manager for the service that uses the notifications package.

For V1 of this project, which triggers notifications via Ender, these values must specifically be added to the Ender secrets vault in AWS.

## Mobile App Token Registration

To enable push notifications, users must register their devices by using the following endpoint:

**POST** ```v4/:address/registerToken```

**Request Payload**: ```{ token: "<TOKEN_HASH>", language: 'en' }```

`token`: A valid push notification token generated by the Google Firebase SDK.

`language`: A string representing the user's preferred language, following the ISO 639-1 standard. This must be one of the supported languages listed below:

- 'en' (English)
- 'es' (Spanish)
- 'fr' (French)
- 'de' (German)
- 'it' (Italian)
- 'ja' (Japanese)
- 'ko' (Korean)
- 'zh' (Chinese)
65 changes: 65 additions & 0 deletions indexer/packages/notifications/__tests__/localization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
deriveLocalizedNotificationMessage,
} from '../src/localization';
import {
NotificationType,
NotificationDynamicFieldKey,
createNotification,
isValidLanguageCode,
} from '../src/types';

describe('deriveLocalizedNotificationMessage', () => {
test('should generate a correct message for DepositSuccessNotification', () => {
const notification = createNotification(NotificationType.DEPOSIT_SUCCESS, {
[NotificationDynamicFieldKey.AMOUNT]: '1000',
[NotificationDynamicFieldKey.MARKET]: 'USDT',
});

const expected = {
title: 'Deposit Successful',
body: 'You have successfully deposited 1000 USDT to your dYdX account.',
};

const result = deriveLocalizedNotificationMessage(notification);
expect(result).toEqual(expected);
});

test('should generate a correct message for OrderFilledNotification', () => {
const notification = createNotification(NotificationType.ORDER_FILLED, {
[NotificationDynamicFieldKey.MARKET]: 'BTC/USD',
[NotificationDynamicFieldKey.AVERAGE_PRICE]: '45000',
[NotificationDynamicFieldKey.AMOUNT]: '1000',
});

const expected = {
title: 'Order Filled',
body: 'Your order for 1000 BTC/USD was filled at $45000',
};

const result = deriveLocalizedNotificationMessage(notification);
expect(result).toEqual(expected);
});

describe('isValidLanguageCode', () => {
test('should return true for valid language codes', () => {
const validCodes = ['en', 'es', 'fr', 'de', 'it', 'ja', 'ko', 'zh'];
validCodes.forEach((code) => {
expect(isValidLanguageCode(code)).toBe(true);
});
});

test('should return false for invalid language codes', () => {
const invalidCodes = ['', 'EN', 'eng', 'esp', 'fra', 'deu', 'ita', 'jpn', 'kor', 'zho', 'xx'];
invalidCodes.forEach((code) => {
expect(isValidLanguageCode(code)).toBe(false);
});
});

test('should return false for non-string inputs', () => {
const nonStringInputs = [null, undefined, 123, {}, []];
nonStringInputs.forEach((input) => {
expect(isValidLanguageCode(input as any)).toBe(false);
});
});
});
});
65 changes: 65 additions & 0 deletions indexer/packages/notifications/__tests__/message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { logger } from '@dydxprotocol-indexer/base';
import { sendFirebaseMessage } from '../src/message';
import { sendMulticast } from '../src/lib/firebase';
import { createNotification, NotificationType } from '../src/types';

jest.mock('../src/lib/firebase', () => ({
sendMulticast: jest.fn(),
}));

describe('sendFirebaseMessage', () => {
let loggerInfoSpy: jest.SpyInstance;
let loggerWarnSpy: jest.SpyInstance;
let loggerErrorSpy: jest.SpyInstance;

beforeAll(() => {
loggerInfoSpy = jest.spyOn(logger, 'info').mockImplementation();
loggerWarnSpy = jest.spyOn(logger, 'warning').mockImplementation();
loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation();
});

afterAll(() => {
loggerInfoSpy.mockRestore();
loggerWarnSpy.mockRestore();
loggerErrorSpy.mockRestore();
});

const defaultToken = {
token: 'faketoken',
language: 'en',
};

const mockNotification = createNotification(NotificationType.ORDER_FILLED, {
AMOUNT: '10',
MARKET: 'BTC-USD',
AVERAGE_PRICE: '100.50',
});

it('should send a Firebase message successfully', async () => {
await sendFirebaseMessage(
[{ token: defaultToken.token, language: defaultToken.language }],
mockNotification,
);

expect(sendMulticast).toHaveBeenCalledWith(expect.objectContaining(
{
tokens: [defaultToken.token],
notification: { body: 'Your order for 10 BTC-USD was filled at $100.50', title: 'Order Filled' },
}));
});

it('should log an error if sending the message fails', async () => {
const mockedSendMulticast = sendMulticast as jest.MockedFunction<typeof sendMulticast>;
mockedSendMulticast.mockRejectedValueOnce(new Error('Send failed'));

await sendFirebaseMessage(
[{ token: defaultToken.token, language: defaultToken.language }],
mockNotification,
);

expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({
message: 'Send failed',
notificationType: mockNotification.type,
}));
});
});
2 changes: 2 additions & 0 deletions indexer/packages/notifications/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Use the base configuration as-is.
module.exports = require('./node_modules/@dydxprotocol-indexer/dev/jest.config');
6 changes: 6 additions & 0 deletions indexer/packages/notifications/jest.globalSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This function runs once before all tests.
module.exports = () => {
// This loads the environment variables for tests.
// eslint-disable-next-line global-require
require('dotenv-flow/config');
};
1 change: 1 addition & 0 deletions indexer/packages/notifications/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This file runs before each test file.
37 changes: 37 additions & 0 deletions indexer/packages/notifications/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@dydxprotocol-indexer/notifications",
"version": "0.0.1",
"description": "",
"main": "build/src/index.js",
"devDependencies": {
"@dydxprotocol-indexer/dev": "workspace:^0.0.1",
"@types/jest": "^28.1.4",
"jest": "^28.1.2",
"typescript": "^4.7.4",
"ts-node": "^10.8.2"
},
"scripts": {
"lint": "eslint --ext .ts,.js .",
"lint:fix": "eslint --ext .ts,.js . --fix",
"build": "rm -rf build/ && tsc",
"build:prod": "pnpm run build",
"build:watch": "pnpm run build -- --watch",
"test": "NODE_ENV=test jest --runInBand --forceExit"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dydxprotocol/indexer.git"
},
"author": "",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/dydxprotocol/indexer/issues"
},
"homepage": "https://github.com/dydxprotocol/indexer#readme",
"dependencies": {
"firebase-admin": "^12.4.0",
"@dydxprotocol-indexer/base": "workspace:^0.0.1",
"@dydxprotocol-indexer/postgres": "workspace:^0.0.1",
"dotenv-flow": "^3.2.0"
}
}
27 changes: 27 additions & 0 deletions indexer/packages/notifications/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Environment variables required for Notifications module.
*/

import {
parseString,
parseSchema,
baseConfigSchema,
} from '@dydxprotocol-indexer/base';

export const notificationsConfigSchema = {
...baseConfigSchema,

// Private Key for the Google Firebase Messaging project
// default is a dummy value
FIREBASE_PRIVATE_KEY_BASE64: parseString({ default: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tClBMQUNFSE9MREVSX0tFWV9GT1JfREVWRUxPUE1FTlQKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=' }),

// APP ID for the Google Firebase Messaging project
// default is a dummy value
FIREBASE_PROJECT_ID: parseString({ default: 'dydx-v4' }),

// Client email for the Google Firebase Messaging project
// default is a dummy value
FIREBASE_CLIENT_EMAIL: parseString({ default: '[email protected]' }),
};

export default parseSchema(notificationsConfigSchema);
4 changes: 4 additions & 0 deletions indexer/packages/notifications/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './lib/firebase';
export * from './localization';
export * from './types';
export * from './message';
Loading

0 comments on commit 2ba777e

Please sign in to comment.