mango-alerts/src/index.ts

150 lines
5.2 KiB
TypeScript

import Koa from "koa";
import Router from "koa-router";
import mongo from "koa-mongo";
import bodyParser from "koa-bodyparser";
import cors from "@koa/cors";
import * as cron from "node-cron";
import {MongoClient, ObjectId} from "mongodb";
import { MangoClient, MarginAccount, IDS } from '@blockworks-foundation/mango-client';
import { Connection, PublicKey } from '@solana/web3.js';
import { UserError } from './errors';
import { sendLogsToDiscord } from './logger';
import { initiateTelegramBot, generateTelegramCode, validateMarginAccount, validatePhoneNumber, validateEmail, reduceMangoGroups, sendAlert } from './utils';
import config from './environment';
const MESSAGE = 'Your collateral ratio is at or below @ratio@% \n';
const app = new Koa();
const router = new Router;
const cluster = 'mainnet-beta';
const client = new MangoClient();
const clusterIds = IDS[cluster];
const connection = new Connection(config.rpcEndpoint || IDS.cluster_urls[cluster], 'singleGossip');
const dexProgramId = new PublicKey(clusterIds.dex_program_id);
const mangoProgramId = new PublicKey(clusterIds.mango_program_id);
let db: any;
app.use(cors());
app.use(bodyParser());
app.use(mongo({ uri: config.dbConnectionString }, { useUnifiedTopology: true }));
initiateTelegramBot();
router.post('/alerts', async(ctx, next) => {
try {
const alert: any = ctx.request.body;
await validateMarginAccount(client, connection, dexProgramId, alert);
if (alert.alertProvider == 'sms') {
const phoneNumber = `+${alert.phoneNumber.code}${alert.phoneNumber.phone}`;
await validatePhoneNumber(phoneNumber);
ctx.body = { status: 'success' };
} else if (alert.alertProvider == 'mail') {
validateEmail(alert.email);
ctx.body = { status: 'success' };
} else if (alert.alertProvider == 'tg') {
const code = generateTelegramCode();
alert.tgCode = code;
ctx.body = { code };
} else {
throw new UserError('Invalid alert provider');
}
alert.open = true;
alert.timestamp = Date.now();
ctx.db.collection('alerts').insertOne(alert);
} catch (e) {
let errorMessage = 'Something went wrong';
if (e.name == 'UserError') {
errorMessage = e.message;
} else {
sendLogsToDiscord(null, e);
}
ctx.throw(400, errorMessage);
}
await next();
});
router.get('/alerts/:marginAccountPk', async(ctx, next) => {
try {
const { marginAccountPk } = ctx.params;
if (!marginAccountPk) {
throw new UserError('Missing margin account');
}
const alerts = await ctx.db.collection('alerts').find(
{ marginAccountPk },
{ projection: {
'_id': 0,
'collateralRatioThresh': 1,
'alertProvider': 1,
'open': 1,
'timestamp': 1,
'triggeredTimestamp': 1
}}).toArray();
ctx.body = { alerts };
} catch (e) {
let errorMessage = 'Something went wrong';
if (e.name == 'UserError') {
errorMessage = e.message;
} else {
sendLogsToDiscord(null, e);
}
ctx.throw(400, errorMessage);
}
})
app.use(router.allowedMethods());
app.use(router.routes());
app.listen(config.port, () => {
const readyMessage = `> Server ready on http://localhost:${config.port}`;
console.log(readyMessage)
sendLogsToDiscord(readyMessage, null);
});
const handleAlert = async (alert: any, mangoGroups: any[], db: any) => {
try {
const mangoGroupMapping = mangoGroups[alert.mangoGroupPk];
const marginAccountPk = new PublicKey(alert.marginAccountPk);
const marginAccount = mangoGroupMapping.marginAccounts.find((ma: MarginAccount) => ma.publicKey.equals(marginAccountPk));
const collateralRatio = marginAccount.getCollateralRatio(mangoGroupMapping['mangoGroup'], mangoGroupMapping['prices']);
if ((100 * collateralRatio) <= alert.collateralRatioThresh) {
let message = MESSAGE.replace('@ratio@', alert.collateralRatioThresh);
message += marginAccount.toPrettyString(
mangoGroupMapping['mangoGroup'],
mangoGroupMapping['prices']
);
message += '\nVisit https://trade.mango.markets/'
const alertSent = await sendAlert(alert, message);
if (alertSent) {
db.collection('alerts').updateOne({ _id: new ObjectId(alert._id) }, { '$set': { open: false, triggeredTimestamp: Date.now() } });
}
}
} catch (e) {
sendLogsToDiscord(null, e);
}
}
const runCron = async () => {
const mongoConnection = await MongoClient.connect(config.dbConnectionString, { useUnifiedTopology: true });
if (!db) db = mongoConnection.db(config.db);
cron.schedule("* * * * *", async () => {
try {
const alerts: any[] = await db.collection('alerts').find({open: true}).toArray();
const uniqueMangoGroupPks: string[] = [...new Set(alerts.map(alert => alert.mangoGroupPk))];
const mangoGroups:any = await reduceMangoGroups(client, connection, mangoProgramId, uniqueMangoGroupPks);
alerts.forEach(async (alert) => {
handleAlert(alert, mangoGroups, db);
});
const expiryTime = Date.now() - (1000 * 60 * 15); // 15 Minutes
db.collection('alerts').deleteMany({ alertProvider: 'tg', tgChatId: { '$exists': false }, timestamp: { '$lt': expiryTime } });
} catch (e) {
sendLogsToDiscord(null, e);
}
});
}
runCron();