Skip to content

Commit

Permalink
Add initial tests for ender notification integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
adamfraser committed Aug 19, 2024
1 parent 1cd65b6 commit 0afa13f
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 28 deletions.
7 changes: 4 additions & 3 deletions indexer/packages/notifications/__tests__/localization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('deriveLocalizedNotificationMessage', () => {
});

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

Expand All @@ -30,11 +30,12 @@ describe('deriveLocalizedNotificationMessage', () => {
const notification = createNotification(NotificationType.ORDER_FILLED, {
[NotificationDynamicFieldKey.MARKET]: 'BTC/USD',
[NotificationDynamicFieldKey.AVERAGE_PRICE]: '45000',
[NotificationDynamicFieldKey.AMOUNT]: '1000',
});

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

const result = deriveLocalizedNotificationMessage(notification);
Expand Down
2 changes: 1 addition & 1 deletion indexer/packages/notifications/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const notificationsConfigSchema = {
...baseConfigSchema,

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

// APP ID for the Google Firebase Messaging project
FIREBASE_PROJECT_ID: parseString({ default: 'dydx-v4' }),
Expand Down
44 changes: 39 additions & 5 deletions indexer/packages/notifications/src/lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { logger } from '@dydxprotocol-indexer/base';
import {
App,
cert,
initializeApp,
ServiceAccount,
Expand All @@ -22,9 +23,19 @@ const initializeFirebaseApp = () => {

const serviceAccount: ServiceAccount = defaultGoogleApplicationCredentials;

const firebaseApp = initializeApp({
credential: cert(serviceAccount),
});
let firebaseApp: App;
try {
firebaseApp = initializeApp({
credential: cert(serviceAccount),
});
} catch (error) {
logger.error({
at: 'notifications#firebase',
message: 'Failed to initialize Firebase App',
error,
});
return undefined;
}

logger.info({
at: 'notifications#firebase',
Expand All @@ -35,7 +46,30 @@ const initializeFirebaseApp = () => {
};

const firebaseApp = initializeFirebaseApp();
const firebaseMessaging = getMessaging(firebaseApp);
// Initialize Firebase Messaging if the app was initialized successfully
let firebaseMessaging = null;
if (firebaseApp) {
try {
firebaseMessaging = getMessaging(firebaseApp);
logger.info({
at: 'notifications#firebase',
message: 'Firebase Messaging initialized successfully',
});
} catch (error) {
logger.error({
at: 'notifications#firebase',
message: 'Firebase Messaging failed to initialize',
});
}
}

export const sendMulticast = firebaseMessaging.sendMulticast.bind(firebaseMessaging);
export const sendMulticast = firebaseMessaging
? firebaseMessaging.sendMulticast.bind(firebaseMessaging)
: () => {
logger.error({
at: 'notifications#firebase',
message: 'Firebase Messaging is not initialized, sendMulticast is a no-op',
});
return Promise.resolve(null);
};
export { BatchResponse, getMessaging, MulticastMessage } from 'firebase-admin/messaging';
7 changes: 4 additions & 3 deletions indexer/packages/notifications/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ function replacePlaceholders(template: string, variables: Record<string, string>

export function deriveLocalizedNotificationMessage(notification: Notification): NotificationMesage {
const tempLocalizationFields = {
[LocalizationKey.DEPOSIT_SUCCESS_TITLE]: 'Deposit successful',
[LocalizationKey.DEPOSIT_SUCCESS_TITLE]: 'Deposit Successful',
[LocalizationKey.DEPOSIT_SUCCESS_BODY]: 'You have successfully deposited {AMOUNT} {MARKET} to your dYdX account.',
[LocalizationKey.ORDER_FILLED_BODY]: 'Order Filled successful',
[LocalizationKey.ORDER_FILLED_TITLE]: 'Filled {MARKET} order at {AVERAGE_PRICE}.',
[LocalizationKey.ORDER_FILLED_BODY]: 'Order Filled',
// eslint-disable-next-line no-template-curly-in-string
[LocalizationKey.ORDER_FILLED_TITLE]: 'Your order for {AMOUNT} {MARKET} was filled at ${AVERAGE_PRICE}',
};

switch (notification.type) {
Expand Down
5 changes: 2 additions & 3 deletions indexer/packages/notifications/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { logger } from '@dydxprotocol-indexer/base';
import { TokenTable } from '@dydxprotocol-indexer/postgres';

import {
BatchResponse,
MulticastMessage,
sendMulticast,
} from './lib/firebase';
Expand Down Expand Up @@ -48,8 +47,8 @@ export async function sendFirebaseMessage(
};

try {
const result: BatchResponse = await sendMulticast(message);
if (result.failureCount && result.failureCount > 0) {
const result = await sendMulticast(message);
if (result?.failureCount && result?.failureCount > 0) {
logger.info({
at: 'notifications#firebase',
message: `Failed to send Firebase message: ${JSON.stringify(message)}`,
Expand Down
17 changes: 11 additions & 6 deletions indexer/packages/notifications/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,45 @@ export enum Deeplink {
ORDER_FILLED = '/profile',
}

export interface UserNotificationFields {
email?: string;
isEmailVerified: boolean;
notifications: {};
export enum Topic {
TRADING = 'trading',
PRICE_ALERTS = 'price_alerts',
}

interface BaseNotification <T extends Record<string, string>> {
type: NotificationType,
titleKey: LocalizationKey;
bodyKey: LocalizationKey;
topic: Topic;
deeplink: Deeplink;
dynamicValues: T,
}

export interface DepositSuccessNotification extends BaseNotification<{
interface DepositSuccessNotification extends BaseNotification<{
[NotificationDynamicFieldKey.AMOUNT]: string;
[NotificationDynamicFieldKey.MARKET]: string;
}> {
type: NotificationType.DEPOSIT_SUCCESS;
titleKey: LocalizationKey.DEPOSIT_SUCCESS_TITLE;
bodyKey: LocalizationKey.DEPOSIT_SUCCESS_BODY;
topic: Topic.TRADING;
dynamicValues: {
[NotificationDynamicFieldKey.AMOUNT]: string;
[NotificationDynamicFieldKey.MARKET]: string;
}
}

export interface OrderFilledNotification extends BaseNotification <{
interface OrderFilledNotification extends BaseNotification <{
[NotificationDynamicFieldKey.MARKET]: string;
[NotificationDynamicFieldKey.AVERAGE_PRICE]: string;
}>{
type: NotificationType.ORDER_FILLED;
titleKey: LocalizationKey.ORDER_FILLED_TITLE;
bodyKey: LocalizationKey.ORDER_FILLED_BODY;
topic: Topic.TRADING;
dynamicValues: {
[NotificationDynamicFieldKey.MARKET]: string;
[NotificationDynamicFieldKey.AMOUNT]: string;
[NotificationDynamicFieldKey.AVERAGE_PRICE]: string;
};
}
Expand All @@ -97,6 +100,7 @@ export function createNotification<T extends NotificationType>(
type: NotificationType.DEPOSIT_SUCCESS,
titleKey: LocalizationKey.DEPOSIT_SUCCESS_TITLE,
bodyKey: LocalizationKey.DEPOSIT_SUCCESS_BODY,
topic: Topic.TRADING,
deeplink: Deeplink.DEPOSIT,
dynamicValues: dynamicValues as DepositSuccessNotification['dynamicValues'],
};
Expand All @@ -106,6 +110,7 @@ export function createNotification<T extends NotificationType>(
type: NotificationType.ORDER_FILLED,
titleKey: LocalizationKey.ORDER_FILLED_TITLE,
bodyKey: LocalizationKey.ORDER_FILLED_BODY,
topic: Topic.TRADING,
deeplink: Deeplink.ORDER_FILLED,
dynamicValues: dynamicValues as OrderFilledNotification['dynamicValues'],
};
Expand Down
8 changes: 6 additions & 2 deletions indexer/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -393,14 +393,10 @@ class AddressesController extends Controller {
throw new NotFoundError(`No wallet found for address: ${address}`);
}

const token = await TokenTable.findAll({ address }, []);
if (!token) {
throw new NotFoundError(`No token found for address: ${address}`);
}

try {
const notification = createNotification(NotificationType.ORDER_FILLED, {
[NotificationDynamicFieldKey.MARKET]: 'BTC/USD',
[NotificationDynamicFieldKey.AMOUNT]: '100',
[NotificationDynamicFieldKey.AVERAGE_PRICE]: '1000',
});
await sendFirebaseMessage(wallet.address, notification);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { sendOrderFilledNotification } from '../../src/helpers/notifications/notifications-functions';
import { OrderFromDatabase } from '@dydxprotocol-indexer/postgres';

import { createNotification, sendFirebaseMessage, NotificationType } from '@dydxprotocol-indexer/notifications';
import { defaultSubaccountId, defaultMarket } from '@dydxprotocol-indexer/postgres/build/__tests__/helpers/constants';

// Mock only the sendFirebaseMessage function
jest.mock('@dydxprotocol-indexer/notifications', () => {
const actualModule = jest.requireActual('@dydxprotocol-indexer/notifications');
return {
...actualModule, // keep all other exports intact
sendFirebaseMessage: jest.fn(),
createNotification: jest.fn(),
};
});

describe('sendOrderFilledNotification', () => {
it('should create and send a notification', async () => {
const mockOrder: OrderFromDatabase = {
id: '1',
subaccountId: defaultSubaccountId,
clientId: '1',
clobPairId: String(defaultMarket.id),
side: 'BUY',
size: '10',
totalFilled: '0',
price: '100.50',
type: 'LIMIT',
status: 'OPEN',
timeInForce: 'GTT',
reduceOnly: false,
orderFlags: '0',
goodTilBlock: '1000000',
createdAtHeight: '900000',
clientMetadata: '0',
triggerPrice: undefined,
updatedAt: new Date().toISOString(),
updatedAtHeight: '900001',
} as OrderFromDatabase;

await sendOrderFilledNotification(mockOrder);

// Assert that createNotification was called with correct arguments
expect(createNotification).toHaveBeenCalledWith(
NotificationType.ORDER_FILLED,
{
AMOUNT: '10',
MARKET: 'BTC-USD',
AVERAGE_PRICE: '100.50',
},
);

// Assert that sendFirebaseMessage was called with correct arguments
expect(sendFirebaseMessage).toHaveBeenCalledWith(defaultSubaccountId, undefined);
});

it('should throw an error if market is not found', async () => {
const mockOrder: OrderFromDatabase = {
id: '1',
subaccountId: 'subaccount123',
clientId: '1',
clobPairId: '1',
side: 'BUY',
size: '10',
totalFilled: '0',
price: '100.50',
type: 'LIMIT',
status: 'OPEN',
timeInForce: 'GTT',
reduceOnly: false,
orderFlags: '0',
goodTilBlock: '1000000',
createdAtHeight: '900000',
clientMetadata: '0',
triggerPrice: undefined,
updatedAt: new Date().toISOString(),
updatedAtHeight: '900001',
} as OrderFromDatabase;

await expect(sendOrderFilledNotification(mockOrder)).rejects.toThrow('sendOrderFilledNotification # Market not found');
});
});
1 change: 1 addition & 0 deletions indexer/services/ender/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@dydxprotocol-indexer/redis": "workspace:^0.0.1",
"@dydxprotocol-indexer/v4-proto-parser": "workspace:^0.0.1",
"@dydxprotocol-indexer/v4-protos": "workspace:^0.0.1",
"@dydxprotocol-indexer/notifications": "workspace:^0.0.1",
"big.js": "^6.0.2",
"dd-trace": "^3.32.1",
"dotenv-flow": "^3.2.0",
Expand Down
10 changes: 10 additions & 0 deletions indexer/services/ender/src/handlers/order-fills/order-handler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
createNotification, NotificationDynamicFieldKey, NotificationType, sendFirebaseMessage,
} from '@dydxprotocol-indexer/notifications';
import {
FillFromDatabase,
FillModel,
Liquidity,
MarketFromDatabase,
MarketModel,
MarketTable,
OrderFromDatabase,
OrderModel,
OrderStatus,
Expand Down Expand Up @@ -31,6 +35,7 @@ import { orderFillWithLiquidityToOrderFillEventWithOrder } from '../../helpers/t
import { OrderFillWithLiquidity } from '../../lib/translated-types';
import { ConsolidatedKafkaEvent, OrderFillEventWithOrder } from '../../lib/types';
import { AbstractOrderFillHandler } from './abstract-order-fill-handler';
import { sendOrderFilledNotification } from 'src/helpers/notifications/notifications-functions';

export class OrderHandler extends AbstractOrderFillHandler<OrderFillWithLiquidity> {
eventType: string = 'OrderFillEvent';
Expand Down Expand Up @@ -115,6 +120,11 @@ export class OrderHandler extends AbstractOrderFillHandler<OrderFillWithLiquidit
redisClient,
);

// If order is filled, send a notification to firebase
if (order.status === OrderStatus.FILLED) {
await sendOrderFilledNotification(order);
}

// If the order is stateful and fully-filled, send an order removal to vulcan. We only do this
// for stateful orders as we are guaranteed a stateful order cannot be replaced until the next
// block.
Expand Down
Loading

0 comments on commit 0afa13f

Please sign in to comment.