alerts v3 wip

This commit is contained in:
saml33 2021-12-15 12:20:28 +11:00
parent 9a31a83af3
commit 6b31263632
6 changed files with 4109 additions and 251 deletions

View File

@ -3,9 +3,6 @@
"version": "1.0.0", "version": "1.0.0",
"main": "build/index", "main": "build/index",
"license": "MIT", "license": "MIT",
"engines": {
"node": "12.14.0"
},
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"postinstall": "tsc", "postinstall": "tsc",
@ -33,7 +30,7 @@
"typescript": "^4.2.4" "typescript": "^4.2.4"
}, },
"dependencies": { "dependencies": {
"@blockworks-foundation/mango-client": "https://github.com/blockworks-foundation/mango-client-ts.git#5_tokens", "@blockworks-foundation/mango-client": "^3.2.9",
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@solana/web3.js": "^1.2.6", "@solana/web3.js": "^1.2.6",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
@ -47,6 +44,7 @@
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-telegram-bot-api": "^0.52.0", "node-telegram-bot-api": "^0.52.0",
"nodemailer": "^6.5.0", "nodemailer": "^6.5.0",
"nodemailer-mailjet-transport": "^1.0.4",
"twilio": "^3.60.0", "twilio": "^3.60.0",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-discord-transport": "^1.3.0" "winston-discord-transport": "^1.3.0"

View File

@ -1,23 +1,22 @@
import * as dotenv from 'dotenv'; import * as dotenv from "dotenv"
dotenv.config(); dotenv.config()
export default { export default {
rpcEndpoint: process.env.RPC_ENDPOINT || '', rpcEndpoint: process.env.RPC_ENDPOINT || "",
dbConnectionString: (process.env.NODE_ENV == 'production') ? dbConnectionString:
`mongodb://${process.env.DB_USER}:${encodeURIComponent(process.env.DB_PASS || '')}@${process.env.DB_HOSTS}/${process.env.DB}${process.env.DB_OPTIONS}`: `mongodb+srv://${process.env.DB_USER}:${process.env.DB_PASS}@${process.env.DB_CLUSTER}.fqb1s.mongodb.net/${process.env.DB}?retryWrites=true&w=majority` ||
'mongodb://localhost:27017/mango_alerts', "",
db: process.env.DB || '', db: process.env.DB || "",
port: process.env.PORT || 3000, port: process.env.PORT || 3000,
twilioSid: process.env.TWILIO_ACCOUNT_SID || '', twilioSid: process.env.TWILIO_ACCOUNT_SID || "",
twilioToken: process.env.TWILIO_AUTH_TOKEN || '', twilioToken: process.env.TWILIO_AUTH_TOKEN || "",
twilioNumber: process.env.TWILIO_PHONE_NUMBER || '', twilioNumber: process.env.TWILIO_PHONE_NUMBER || "",
mailUser: process.env.MAIL_USER || '', mailUser: process.env.MAIL_USER || "",
mailPass: process.env.MAIL_PASS || '', mailJetKey: process.env.MAILJET_KEY || "",
mailJetSecret: process.env.MAILJET_SECRET || "",
tgToken: process.env.TG_TOKEN || '', // tgToken: process.env.TG_TOKEN || '',
// discordWebhook: process.env.DISCORD_WEBHOOK || ''
discordWebhook: process.env.DISCORD_WEBHOOK || ''
} }

View File

@ -1,149 +1,222 @@
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 Koa from "koa"; import {
import Router from "koa-router"; Cluster,
import mongo from "koa-mongo"; Config,
import bodyParser from "koa-bodyparser"; GroupConfig,
import cors from "@koa/cors"; MangoClient,
import * as cron from "node-cron"; MangoAccount,
import {MongoClient, ObjectId} from "mongodb"; IDS,
} from "@blockworks-foundation/mango-client"
import { Commitment, Connection, PublicKey } from "@solana/web3.js"
import { MangoClient, MarginAccount, IDS } from '@blockworks-foundation/mango-client'; import { UserError } from "./errors"
import { Connection, PublicKey } from '@solana/web3.js'; // import { sendLogsToDiscord } from "./logger"
import {
// initiateTelegramBot,
// generateTelegramCode,
validateMangoAccount,
// validatePhoneNumber,
validateEmail,
reduceMangoGroups,
sendAlert,
} from "./utils"
import config from "./environment"
import { UserError } from './errors'; const MESSAGE = "Your health ratio is at or below @ratio@% \n"
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 app = new Koa(); // const rpcUrl = "https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88"
const router = new Router;
const cluster = 'mainnet-beta'; const clientConfig = new Config(IDS)
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()); const cluster = (process.env.CLUSTER || "mainnet") as Cluster
app.use(bodyParser()); const groupName = process.env.GROUP || "mainnet.1"
app.use(mongo({ uri: config.dbConnectionString }, { useUnifiedTopology: true })); const groupIds = clientConfig.getGroup(cluster, groupName)
if (!groupIds) {
throw new Error(`Group ${groupName} not found`)
}
initiateTelegramBot(); const mangoProgramId = groupIds.mangoProgramId
// const mangoGroupKey = groupIds.publicKey
router.post('/alerts', async(ctx, next) => { const connection = new Connection(
process.env.ENDPOINT_URL || clientConfig.cluster_urls[cluster],
"processed" as Commitment
)
const client = new MangoClient(connection, mangoProgramId)
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 { try {
const alert: any = ctx.request.body; const alert: any = ctx.request.body
await validateMarginAccount(client, connection, dexProgramId, alert); // await validateMangoAccount(client, alert)
if (alert.alertProvider == 'sms') { if (alert.alertProvider == "mail") {
const phoneNumber = `+${alert.phoneNumber.code}${alert.phoneNumber.phone}`; validateEmail(alert.email)
await validatePhoneNumber(phoneNumber); ctx.body = { status: "success" }
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 { } else {
throw new UserError('Invalid alert provider'); throw new UserError("Invalid alert provider")
} }
alert.open = true; alert.open = true
alert.timestamp = Date.now(); alert.timestamp = Date.now()
ctx.db.collection('alerts').insertOne(alert); ctx.db.collection("alerts").insertOne(alert)
} catch (e) { } catch (e: any) {
let errorMessage = 'Something went wrong'; let errorMessage = "Something went wrong"
if (e.name == 'UserError') { if (e.name == "UserError") {
errorMessage = e.message; errorMessage = e.message
} else { } else {
sendLogsToDiscord(null, e); // sendLogsToDiscord(null, e)
} }
ctx.throw(400, errorMessage); ctx.throw(400, errorMessage)
} }
await next(); await next()
}); })
router.get('/alerts/:marginAccountPk', async(ctx, next) => { router.post("/delete-alert", async (ctx, next) => {
try { try {
const { marginAccountPk } = ctx.params; const id: any = new ObjectId(ctx.request.body.id)
if (!marginAccountPk) { if (id) {
throw new UserError('Missing margin account'); ctx.body = { status: "success" }
} }
const alerts = await ctx.db.collection('alerts').find( ctx.db.collection("alerts").deleteOne({ _id: id })
{ marginAccountPk }, } catch (e: any) {
{ projection: { let errorMessage = "Something went wrong"
'_id': 0, if (e.name == "UserError") {
'collateralRatioThresh': 1, errorMessage = e.message
'alertProvider': 1, }
'open': 1, ctx.throw(400, errorMessage)
'timestamp': 1, }
'triggeredTimestamp': 1 await next()
}}).toArray(); })
ctx.body = { alerts };
} catch (e) { router.get("/alerts/:mangoAccountPk", async (ctx, next) => {
let errorMessage = 'Something went wrong'; try {
if (e.name == 'UserError') { const { mangoAccountPk } = ctx.params
errorMessage = e.message; if (!mangoAccountPk) {
throw new UserError("Missing margin account")
}
const alerts = await ctx.db
.collection("alerts")
.find(
{ mangoAccountPk },
{
projection: {
_id: 1,
health: 1,
alertProvider: 1,
open: 1,
timestamp: 1,
triggeredTimestamp: 1,
},
}
)
.toArray()
ctx.body = { alerts }
} catch (e: any) {
let errorMessage = "Something went wrong"
if (e.name == "UserError") {
errorMessage = e.message
} else { } else {
sendLogsToDiscord(null, e); // sendLogsToDiscord(null, e)
} }
ctx.throw(400, errorMessage); ctx.throw(400, errorMessage)
} }
}) })
app.use(router.allowedMethods()); app.use(router.allowedMethods())
app.use(router.routes()); app.use(router.routes())
app.listen(config.port, () => { app.listen(config.port, () => {
const readyMessage = `> Server ready on http://localhost:${config.port}`; const readyMessage = `> Server ready on http://localhost:${config.port}`
console.log(readyMessage) console.log(readyMessage)
sendLogsToDiscord(readyMessage, null); // sendLogsToDiscord(readyMessage, null)
}); })
const handleAlert = async (alert: any, mangoGroups: any[], db: any) => { const handleAlert = async (alert: any, db: any) => {
try { try {
const mangoGroupMapping = mangoGroups[alert.mangoGroupPk]; const mangoAccountPk = new PublicKey(alert.mangoAccountPk)
const marginAccountPk = new PublicKey(alert.marginAccountPk); const mangoGroupPk = new PublicKey(alert.mangoGroupPk)
const marginAccount = mangoGroupMapping.marginAccounts.find((ma: MarginAccount) => ma.publicKey.equals(marginAccountPk)); const mangoGroup = await client.getMangoGroup(mangoGroupPk)
const collateralRatio = marginAccount.getCollateralRatio(mangoGroupMapping['mangoGroup'], mangoGroupMapping['prices']); const mangoCache = await mangoGroup.loadCache(connection)
if ((100 * collateralRatio) <= alert.collateralRatioThresh) { const mangoAccount = await client.getMangoAccount(
let message = MESSAGE.replace('@ratio@', alert.collateralRatioThresh); mangoAccountPk,
message += marginAccount.toPrettyString( mangoGroup.dexProgramId
mangoGroupMapping['mangoGroup'], )
mangoGroupMapping['prices'] const health = await mangoAccount.getHealthRatio(
); mangoGroup,
message += '\nVisit https://trade.mango.markets/' mangoCache,
const alertSent = await sendAlert(alert, message); "Maint"
)
if (health.toNumber() <= parseFloat(alert.health)) {
let message = MESSAGE.replace("@ratio@", alert.health)
message += mangoAccount.name || alert.mangoAccountPk
message += "\nVisit https://trade.mango.markets/"
const alertSent = await sendAlert(alert, message)
if (alertSent) { if (alertSent) {
db.collection('alerts').updateOne({ _id: new ObjectId(alert._id) }, { '$set': { open: false, triggeredTimestamp: Date.now() } }); db.collection("alerts").updateOne(
{ _id: new ObjectId(alert._id) },
{ $set: { open: false, triggeredTimestamp: Date.now() } }
)
} }
} }
} catch (e) { } catch (e) {
sendLogsToDiscord(null, e); console.log(e)
// sendLogsToDiscord(null, e)
} }
} }
const runCron = async () => { const runCron = async () => {
const mongoConnection = await MongoClient.connect(config.dbConnectionString, { useUnifiedTopology: true }); const uri = config.dbConnectionString
if (!db) db = mongoConnection.db(config.db); const mongoClient = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
const mongoConnection = await mongoClient.connect()
if (!db) db = mongoConnection.db("mango")
cron.schedule("* * * * *", async () => { cron.schedule("* * * * *", async () => {
try { try {
const alerts: any[] = await db.collection('alerts').find({open: true}).toArray(); const alerts: any[] = await db
const uniqueMangoGroupPks: string[] = [...new Set(alerts.map(alert => alert.mangoGroupPk))]; .collection("alerts")
const mangoGroups:any = await reduceMangoGroups(client, connection, mangoProgramId, uniqueMangoGroupPks); .find({ open: true })
.toArray()
console.log(alerts)
// const uniqueMangoGroupPks: string[] = [
// ...new Set(alerts.map((alert) => alert.mangoGroupPk)),
// ]
// const mangoGroups: any = await reduceMangoGroups(
// client,
// uniqueMangoGroupPks
// )
alerts.forEach(async (alert) => { alerts.forEach(async (alert) => {
handleAlert(alert, mangoGroups, db); handleAlert(alert, db)
}); })
const expiryTime = Date.now() - (1000 * 60 * 15); // 15 Minutes
db.collection('alerts').deleteMany({ alertProvider: 'tg', tgChatId: { '$exists': false }, timestamp: { '$lt': expiryTime } });
} catch (e) { } catch (e) {
sendLogsToDiscord(null, e); // sendLogsToDiscord(null, e)
} }
}); })
} }
runCron(); runCron()

View File

@ -1,28 +1,28 @@
import winston from "winston"; import winston from "winston"
import DiscordTransport from "winston-discord-transport"; import DiscordTransport from "winston-discord-transport"
import config from './environment'; import config from "./environment"
const logger = winston.createLogger({ // const logger = winston.createLogger({
transports: [ // transports: [
new DiscordTransport({ // new DiscordTransport({
webhook: config.discordWebhook, // webhook: config.discordWebhook,
defaultMeta: { service: "mango_alerts_server" }, // defaultMeta: { service: "mango_alerts_server" },
level: "info" // level: "info"
}) // })
] // ]
}); // });
export const sendLogsToDiscord = async (message: string | null, error: Error | null) => { // export const sendLogsToDiscord = async (message: string | null, error: Error | null) => {
if (message) { // if (message) {
logger.log({ // logger.log({
level: "info", // level: "info",
message: message, // message: message,
}); // });
} else if (error) { // } else if (error) {
logger.log({ // logger.log({
level: "error", // level: "error",
message: error.message, // message: error.message,
error: error // error: error
}); // });
} // }
} // }

View File

@ -1,132 +1,158 @@
import { Twilio } from "twilio"; // import { Twilio } from "twilio"
import * as nodemailer from 'nodemailer'; // import * as mailjetTransport from 'nodemailer-mailjet-transport'
import * as TelegramBot from 'node-telegram-bot-api'; // import * as TelegramBot from "node-telegram-bot-api"
import * as EmailValidator from 'email-validator'; // import { MongoClient } from "mongodb"
import { MongoClient } from "mongodb"; import * as nodemailer from "nodemailer"
import * as EmailValidator from "email-validator"
import { MangoClient } from '@blockworks-foundation/mango-client'; import { MangoClient } from "@blockworks-foundation/mango-client"
import { Connection, PublicKey } from '@solana/web3.js'; import { PublicKey } from "@solana/web3.js"
import { UserError } from './errors'; import { UserError } from "./errors"
import config from './environment'; import config from "./environment"
// This needs to be global because it uses event listeners const mailjetTransport = require("nodemailer-mailjet-transport")
const bot = new TelegramBot.default(config.tgToken, {polling: true});
const twilioClient = new Twilio(config.twilioSid, config.twilioToken);
export const validateMarginAccount = (client: MangoClient, connection: Connection, dexProgramId: PublicKey, alert: any) => { // // This needs to be global because it uses event listeners
// // const bot = new TelegramBot.default(config.tgToken, {polling: true});
// const twilioClient = new Twilio(config.twilioSid, config.twilioToken)
export const validateMangoAccount = (client: MangoClient, alert: any) => {
return new Promise<void>(async (resolve, reject) => { return new Promise<void>(async (resolve, reject) => {
try { try {
const mangoGroupPk = new PublicKey(alert.mangoGroupPk); const mangoGroupPk = new PublicKey(alert.mangoGroupPk)
const marginAccountPk = new PublicKey(alert.marginAccountPk); const mangoAccountPk = new PublicKey(alert.marginAccountPk)
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const mangoGroup = await client.getMangoGroup(mangoGroupPk)
const marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId); const mangoAccount = await client.getMangoAccount(
if (!mangoGroup || !marginAccount) { mangoAccountPk,
reject(new UserError('Invalid margin account or mango group')); mangoGroup.dexProgramId
)
if (!mangoGroup || !mangoAccount) {
reject(new UserError("Invalid margin account or mango group"))
} else { } else {
resolve(); resolve()
} }
} catch (e) { } catch (e) {
reject(new UserError('Invalid margin account or mango group')); reject(new UserError("Invalid margin account or mango group"))
} }
});
}
export const validatePhoneNumber = (phoneNumber: string) => {
return new Promise<void>((resolve, reject) => {
twilioClient.lookups.phoneNumbers(phoneNumber).fetch((e, _) => {
if (e) {
reject(new UserError('The entered phone number is incorrect'));
} else {
resolve();
}
})
}) })
} }
// export const validatePhoneNumber = (phoneNumber: string) => {
// return new Promise<void>((resolve, reject) => {
// twilioClient.lookups.phoneNumbers(phoneNumber).fetch((e, _) => {
// if (e) {
// reject(new UserError("The entered phone number is incorrect"))
// } else {
// resolve()
// }
// })
// })
// }
export const validateEmail = (email: string) => { export const validateEmail = (email: string) => {
if (!EmailValidator.validate(email)) { if (!EmailValidator.validate(email)) {
throw new UserError('The entered email is incorrect'); throw new UserError("The entered email is incorrect")
} }
return; return
} }
const sendSms = (phoneNumber: string, message: string) => { // const sendSms = (phoneNumber: string, message: string) => {
return new Promise<void>((resolve, reject) => { // return new Promise<void>((resolve, reject) => {
twilioClient.messages // twilioClient.messages
.create({ // .create({
from: config.twilioNumber, // from: config.twilioNumber,
to: phoneNumber, // to: phoneNumber,
body: message, // body: message,
// })
// .then((_) => resolve())
// .catch((e) => reject(e))
// })
// }
const sendEmail = async (email: string, message: string) => {
const transport = nodemailer.createTransport(
mailjetTransport({
auth: {
apiKey: config.mailJetKey,
apiSecret: config.mailJetSecret,
},
}) })
.then(_ => resolve()) )
.catch(e => reject(e))
});
}
const sendEmail = (email: string, message: string) => {
const transporter = nodemailer.createTransport(
`smtps://${config.mailUser}%40gmail.com:${config.mailPass}@smtp.gmail.com`
);
const mailOptions = { const mailOptions = {
from : `${config.mailUser}@gmail.com`, from: `${config.mailUser}@mango.markets`,
to : email, to: email,
subject : 'Mango Markets Alerts', subject: "Mango Alerts",
text: message text: message,
}; }
transporter.sendMail( mailOptions );
try {
const info = await transport.sendMail(mailOptions)
console.log(info)
} catch (err) {
console.error(err)
}
transport.sendMail(mailOptions)
} }
export const sendAlert = async (alert: any, message: string) => { export const sendAlert = async (alert: any, message: string) => {
if (alert.alertProvider == 'sms') { if (alert.alertProvider == "mail") {
const phoneNumber = `+${alert.phoneNumber.code}${alert.phoneNumber.phone}`; const email = alert.email
await sendSms(phoneNumber, message); sendEmail(email, message)
} else if (alert.alertProvider == 'mail') {
const email = alert.email;
sendEmail(email, message);
} else if (alert.alertProvider == 'tg') {
if (!alert.tgChatId) return false;
bot.sendMessage(alert.tgChatId, message);
} }
return true; // else if (alert.alertProvider == "sms") {
// const phoneNumber = `+${alert.phoneNumber.code}${alert.phoneNumber.phone}
// await sendSms(phoneNumber, message)
// }
// else if (alert.alertProvider == 'tg') {
// if (!alert.tgChatId) return false;
// bot.sendMessage(alert.tgChatId, message);
// }
return true
} }
export const reduceMangoGroups = async (client: MangoClient, connection: Connection, mangoProgramId: PublicKey, mangoGroupPks: string[]) => { export const reduceMangoGroups = async (
const mangoGroups:any = {}; client: MangoClient,
// connection: Connection,
// mangoProgramId: PublicKey,
mangoGroupPks: string[]
) => {
const mangoGroups: any = {}
for (let mangoGroupPk of mangoGroupPks) { for (let mangoGroupPk of mangoGroupPks) {
const mangoGroup = await client.getMangoGroup(connection, new PublicKey(mangoGroupPk)); const mangoGroup = await client.getMangoGroup(new PublicKey(mangoGroupPk))
const marginAccounts = await client.getAllMarginAccounts(connection, mangoProgramId, mangoGroup); const mangoAccounts = await client.getAllMangoAccounts(mangoGroup)
mangoGroups[mangoGroupPk] = { mangoGroups[mangoGroupPk] = {
mangoGroup, mangoGroup,
marginAccounts, mangoAccounts,
prices: await mangoGroup.getPrices(connection), // prices: await mangoGroup.getPrices(connection),
};
}
return mangoGroups;
}
export const initiateTelegramBot = () => {
bot.on('message', async (message: any) => {
const mongoConnection = await MongoClient.connect(config.dbConnectionString, { useUnifiedTopology: true });
const db = mongoConnection.db(config.db);
const tgCode = message.text;
const alert = await db.collection('alerts').findOne({tgCode});
if (alert) {
await db.collection('alerts').updateOne({ tgCode }, {'$set': { tgChatId: message.chat.id } } );
bot.sendMessage(message.chat.id, 'Thanks, You have successfully claimed your alert\nYou can now close the dialogue on website');
} else {
bot.sendMessage(message.chat.id, 'Sorry, this code is either invalid or expired');
} }
mongoConnection.close(); }
}); return mangoGroups
} }
export const generateTelegramCode = () => { // export const initiateTelegramBot = () => {
var text = ""; // bot.on('message', async (message: any) => {
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // const mongoConnection = await MongoClient.connect(config.dbConnectionString, { useUnifiedTopology: true });
for (let i = 0; i < 5; i++) { // const db = mongoConnection.db(config.db);
text += possible.charAt(Math.floor(Math.random() * possible.length)); // const tgCode = message.text;
} // const alert = await db.collection('alerts').findOne({tgCode});
return text; // if (alert) {
} // await db.collection('alerts').updateOne({ tgCode }, {'$set': { tgChatId: message.chat.id } } );
// bot.sendMessage(message.chat.id, 'Thanks, You have successfully claimed your alert\nYou can now close the dialogue on website');
// } else {
// bot.sendMessage(message.chat.id, 'Sorry, this code is either invalid or expired');
// }
// mongoConnection.close();
// });
// }
// export const generateTelegramCode = () => {
// var text = ""
// var possible =
// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
// for (let i = 0; i < 5; i++) {
// text += possible.charAt(Math.floor(Math.random() * possible.length))
// }
// return text
// }

3762
yarn.lock Normal file

File diff suppressed because it is too large Load Diff