From 563a09d683cf1d540e733e2a3a010c0dfdf126f8 Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 6 Mar 2024 16:48:46 +0530 Subject: [PATCH] transfered price tracker --- .../priceTracker/priceTrackerChannel.ts | 324 ++++++++++++++++++ .../priceTracker/priceTrackerJobs.ts | 43 +++ .../priceTracker/priceTrackerKeys.json | 7 + .../priceTracker/priceTrackerModel.ts | 47 +++ .../priceTracker/priceTrackerRoutes.ts | 37 ++ .../priceTracker/priceTrackerSettings.json | 10 + src/sample_showrunners/priceTracker/test.json | 2 + 7 files changed, 470 insertions(+) create mode 100644 src/sample_showrunners/priceTracker/priceTrackerChannel.ts create mode 100644 src/sample_showrunners/priceTracker/priceTrackerJobs.ts create mode 100644 src/sample_showrunners/priceTracker/priceTrackerKeys.json create mode 100644 src/sample_showrunners/priceTracker/priceTrackerModel.ts create mode 100644 src/sample_showrunners/priceTracker/priceTrackerRoutes.ts create mode 100644 src/sample_showrunners/priceTracker/priceTrackerSettings.json create mode 100644 src/sample_showrunners/priceTracker/test.json diff --git a/src/sample_showrunners/priceTracker/priceTrackerChannel.ts b/src/sample_showrunners/priceTracker/priceTrackerChannel.ts new file mode 100644 index 0000000..b523c13 --- /dev/null +++ b/src/sample_showrunners/priceTracker/priceTrackerChannel.ts @@ -0,0 +1,324 @@ +import { Inject, Service } from 'typedi'; +import { Logger } from 'winston'; +import config from '../../config'; +import settings from './priceTrackerSettings.json'; +import { EPNSChannel } from '../../helpers/epnschannel'; +import keys from './priceTrackerKeys.json'; +import { PushAPI, CONSTANTS } from '@pushprotocol/restapi'; +import { ethers } from 'ethers'; +import axios from 'axios'; + +import { priceTrackerModel, priceTrackerGlobalModel, priceTrackerTokenModel } from './priceTrackerModel'; + +const bent = require('bent'); // Download library + +const NETWORK_TO_MONITOR = config.web3TestnetSepoliaNetwork; + +@Service() +export default class PricetrackerChannel extends EPNSChannel { + model: any; + constructor(@Inject('logger') public logger: Logger, @Inject('cached') public cached) { + super(logger, { + networkToMonitor: NETWORK_TO_MONITOR, + dirname: __dirname, + name: 'Price Tracker', + url: 'https://push.org/', + useOffChain: true, + }); + } + + public async triggerUserNotification(simulate) { + const logger = this.logger; + + try { + this.logInfo(`πŸ””πŸ””Sending notifications`); + + // Get New price function call + await this.getNewPrice(simulate); + } catch (error) { + logger.error(`[${new Date(Date.now())}]-[Price Tracker]- Errored on CMC API... skipped with error: %o`, err); + } + } + + public async getNewPrice(simulate) { + try { + const logger = this.logger; + logger.debug(`[${new Date(Date.now())}]-[Pricetracker]-Getting price of tokens... `); + + // API URL components and settings + const cmcroute = settings.route; + const cmcEndpoint = settings.cmcEndpoint; + const pollURL = `${cmcEndpoint}${cmcroute}?id=${settings.id}&aux=cmc_rank&CMC_PRO_API_KEY=${ + settings.cmcKey || config.cmcAPIKey + }`; + + // Fetching data from the CMC API + let { data } = await axios.get(pollURL); + data = data.data; + + // Initalize provider, signer and userAlice for Channel interaction + const provider = new ethers.providers.JsonRpcProvider(config.web3TestnetSepoliaProvider || settings.providerUrl); + const signer = new ethers.Wallet(keys.PRIVATE_KEY_NEW_STANDARD.PK, provider); + const userAlice = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.STAGING }); + + // Global variables + let i = 1; + let tokenInfo = []; + + // Structuring token data info + for (let id in data) { + let tokenPrice = data[id].quote.USD?.price; + let tokenSymbol = data[id].symbol; + let formattedPrice = Number(Number(tokenPrice).toFixed(2)); + tokenInfo.push({ symbol: tokenSymbol, price: formattedPrice }); + } + + // Global variables from DB + const priceTrackerGlobalData = + (await priceTrackerGlobalModel.findOne({ _id: 'global' })) || + (await priceTrackerGlobalModel.create({ + _id: 'global', + cycles: 0, + })); + + // Set CYCLES variable in DB + const CYCLES = priceTrackerGlobalData.cycles; + + // Looping for subscribers' data in the channel + while (true) { + const userData: any = await userAlice.channel.subscribers({ + page: i, + limit: 10, + setting: true, + }); + + if (userData.itemcount != 0) { + i++; + } else { + i = 1; + + // UPDATE CYCLES VALUE + // HERE + await priceTrackerGlobalModel.findOneAndUpdate({ _id: 'global' }, { $inc: { cycles: 3 } }, { upsert: true }); + const priceTickerGlobalData = await priceTrackerGlobalModel.findOne({ _id: 'global' }); + + // this.logInfo(`Cycles value after all computation: ${priceTickerGlobalData?.cycles}`); + + break; + } + + // Looping through all subscribers here for userSettings + try { + await Promise.all( + userData?.subscribers?.map(async (subscriberObj: { settings: string; subscriber: any }) => { + // Converting String to JS object + let userSettings = JSON.parse(subscriberObj?.settings); + + // For merging different token detals in payload + const notifData2 = []; + + // Only perform computation if user settings exist + try { + if (userSettings !== null) { + + this.logInfo(`Subs ${subscriberObj.subscriber}`); + // Looping through userSettings to handle each userSetting + + await Promise.all( + userSettings?.map(async (mapObj, index) => { + // If subscriber is subscribed to the setting + if (mapObj.user == true) { + // Get current price of the token + const currentToken = tokenInfo.find((obj) => obj.symbol === mapObj.description); + const currentPrice = currentToken?.price; + + // Get previous token price + const previousPriceData = (await priceTrackerTokenModel.findOne({ _id: mapObj.description })) + ? await priceTrackerTokenModel.findOne({ _id: mapObj.description }) + : 0; + + // Update the new price + await priceTrackerTokenModel.findOneAndUpdate( + { _id: mapObj.description }, + { tokenPrevPrice: currentPrice }, + { upsert: true }, + ); + + // Calculate Change + // const changePercentage = ((Math.abs(Number(currentPrice) - previousPriceData.tokenPrevPrice) / previousPriceData.tokenPrevPrice) * 100).toFixed(2); + const changePercentage = ( + ((Number(currentPrice) - previousPriceData.tokenPrevPrice) / + previousPriceData.tokenPrevPrice) * + 100 + ).toFixed(2); + + // The 4 conditions here + // index - 9 ---> Time Interval + // index - 10 ---> Price Change + if (userSettings[9]?.enabled == true && userSettings[10]?.enabled == true) { + this.logInfo(`Price Alert & Time Interval Slider case: ${subscriberObj.subscriber}`); + + // Fetch user values for settings + let userValueTime = userSettings[9].user ==0 ?3:userSettings[9].user; + let userValuePrice = userSettings[10].user; + + // Fetch user last cycle values + const userDBValue = + (await priceTrackerModel.findOne({ _id: subscriberObj.subscriber })) || + (await priceTrackerModel.create({ + _id: subscriberObj.subscriber, + lastCycle: priceTrackerGlobalData.cycles, + })); + + this.logInfo( + `Mapped value of ${userDBValue._id} is ${userDBValue.lastCycle} from both price and time`, + ); + this.logInfo(`User value of ${userDBValue._id} is ${userValueTime} from both price and time`); + + // Condition to trigger notification + if ( + Math.abs(Number(changePercentage)) >= userValuePrice && + userDBValue.lastCycle + userValueTime == CYCLES + ) { + // UPDATE the users mapped value in DB + await priceTrackerModel.findOneAndUpdate( + { _id: subscriberObj.subscriber }, + { lastCycle: CYCLES }, + { upsert: true }, + ); + // Build the payload of the notification + const payloadMsg = + Number(changePercentage) > 0 + ? `Percentage Change (${mapObj.description}): [s:+${Math.abs( + Number(changePercentage), + )}% ($ ${currentPrice})]\n ` + : `Percentage Change (${mapObj.description}): [d:-${Math.abs( + Number(changePercentage), + )}% ($ ${currentPrice})]\n `; + this.logInfo(`Address: ${subscriberObj.subscriber} Data : ${payloadMsg}`); + + notifData2.push({ key: `${Math.abs(Number(changePercentage))}`, notif: `${payloadMsg}` }); + } + } else if (userSettings[10]?.enabled == true) { + this.logInfo(`Price Alert Slider only case: ${subscriberObj.subscriber}`); + + // Fetch user values for settings + let userValue = userSettings[10].user; + + // Condition to trigger notification + if (Math.abs(Number(changePercentage)) >= userValue) { + // Math.abs(Number(changePercentage)) >= userValue + // this.logInfo(`Sending notif to ${userValue}`) + + // Build the payload of the notification + const payloadMsg = + Number(changePercentage) > 0 + ? `Percentage Change (${mapObj.description}): [s:+${Math.abs( + Number(changePercentage), + )}% ($ ${currentPrice})]\n ` + : `Percentage Change (${mapObj.description}): [d:-${Math.abs( + Number(changePercentage), + )}% ($ ${currentPrice})]\n `; + + notifData2.push({ key: `${Math.abs(Number(changePercentage))}`, notif: `${payloadMsg}` }); + } + } else if (userSettings[9]?.enabled == true) { + this.logInfo(`Time Interval Slider only case: ${subscriberObj.subscriber}`); + + // Fetch user values for settings + let userValue = userSettings[9].user ==0 ?3:userSettings[9].user; + + const userDBValue = + (await priceTrackerModel.findOne({ _id: subscriberObj.subscriber })) || + (await priceTrackerModel.create({ + _id: subscriberObj.subscriber, + lastCycle: priceTrackerGlobalData.cycles, + })); + + if (userDBValue.lastCycle + userValue == CYCLES) { + // userDBValue.lastCycle + userValue == CYCLES + // userDBValue.lastCycle + 6 == CYCLES + // userValue = 210, CYCLES + // this.logInfo(`This address will receive the notif: ${subscriberObj.subscriber}`); + + // UPDATE the users mapped value in DB + await priceTrackerModel.findOneAndUpdate( + { _id: subscriberObj.subscriber }, + { lastCycle: CYCLES }, + { upsert: true }, + ); + + // Build the payload of the notification + const payloadMsg = `${mapObj.description} at [d:$${currentPrice}]\n `; + + notifData2.push({ key: `${currentPrice}`, notif: `${payloadMsg}` }); + } + } else { + + // Build the payload of the notification + const payloadMsg = `${mapObj.description} at [d:$${currentPrice}]\n `; + + notifData2.push({ key: `${currentPrice}`, notif: `${payloadMsg}` }); + } + } + }), + ); + + try { + // Build a payload using the array + const title = 'Token Price Movements'; + const message = 'HeyπŸ‘‹! Here is your token movements. Check it out!!'; + const payloadTitle = 'Token Price Movement'; + + let payloadMsg = ''; + + // Sort array in descending order + const sortedPayload = notifData2.sort((a, b) => b.key - a.key); + + for (let i = 0; i < sortedPayload.length; i++) { + payloadMsg += sortedPayload[i].notif; + } + + const payload = { + type: 3, // Type of Notification + notifTitle: title, // Title of Notification + notifMsg: message, // Message of Notification + title: payloadTitle, // Internal Title + msg: payloadMsg, // Internal Message + recipient: subscriberObj.subscriber, // Recipient + }; + + // Send a notification only is body exists + if (payload.msg !== '') { + this.sendNotification({ + recipient: payload.recipient, // new + title: payload.notifTitle, + message: payload.notifMsg, + payloadTitle: payload.title, + payloadMsg: payloadMsg, + notificationType: 3, + simulate: simulate, + image: null, + }); + } + } catch (error) { + throw { + error: error, + message: `Error Sending Notification: ${error.message}`, + }; + } + } + } catch (error) { + this.logError(`Error Parsing user-settings: ${error.message}`); + } + }), + ); + } catch (error) { + this.logError(`Error Parsing user-settings: ${error.message}`); + } + } + } catch (error) { + this.logError(`πŸ’€πŸ’€βš‘βš‘ERROR OCCURED getNewPrice(), ${error.message}`); + } + } +} diff --git a/src/sample_showrunners/priceTracker/priceTrackerJobs.ts b/src/sample_showrunners/priceTracker/priceTrackerJobs.ts new file mode 100644 index 0000000..43eb56c --- /dev/null +++ b/src/sample_showrunners/priceTracker/priceTrackerJobs.ts @@ -0,0 +1,43 @@ +// Do Scheduling +// https://github.com/node-schedule/node-schedule +// * * * * * * +// ┬ ┬ ┬ ┬ ┬ ┬ +// β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +// β”‚ β”‚ β”‚ β”‚ β”‚ β”” day of week (0 - 7) (0 or 7 is Sun) +// β”‚ β”‚ β”‚ β”‚ └───── month (1 - 12) +// β”‚ β”‚ β”‚ └────────── day of month (1 - 31) +// β”‚ β”‚ └─────────────── hour (0 - 23) +// β”‚ └──────────────────── minute (0 - 59) +// └───────────────────────── second (0 - 59, OPTIONAL) +// Execute a cron job every 5 Minutes = */5 * * * * +// Starts from seconds = * * * * * * + +import logger from '../../loaders/logger'; + +import { Container } from 'typedi'; +import schedule from 'node-schedule'; +import PriceTrackerChannel from './priceTrackerChannel'; + +export default () => { + // wallet tracker jobs + const startTime = new Date(new Date().setHours(0, 0, 0, 0)); + + const threeHourRule = new schedule.RecurrenceRule(); + threeHourRule.hour = new schedule.Range(0, 23, 3); + threeHourRule.minute = 0; + threeHourRule.second = 0; + + const channel = Container.get(PriceTrackerChannel); + channel.logInfo(`-- πŸ›΅ Scheduling Showrunner ${channel.cSettings.name} - Channel [on 3hr ]`); + + schedule.scheduleJob({ start: startTime, rule: threeHourRule }, async function () { + const taskName = `${channel.cSettings.name} priceTracker.loadEvents(null) and triggerUserNotification()`; + try { + await channel.triggerUserNotification(true); + logger.info(`${new Date(Date.now())}] 🐣 Cron Task Completed -- ${taskName}`); + } catch (err) { + logger.error(`${new Date(Date.now())}] ❌ Cron Task Failed -- ${taskName}`); + logger.error(`${new Date(Date.now())}] Error Object: %o`, err); + } + }); +}; diff --git a/src/sample_showrunners/priceTracker/priceTrackerKeys.json b/src/sample_showrunners/priceTracker/priceTrackerKeys.json new file mode 100644 index 0000000..d728716 --- /dev/null +++ b/src/sample_showrunners/priceTracker/priceTrackerKeys.json @@ -0,0 +1,7 @@ +{ + "PRIVATE_KEY_NEW_STANDARD": { + "PK": "0x{Privatekey}", + "CHAIN_ID": "eip115:11155111 (for sepolia)" + }, + "PRIVATE_KEY_OLD_STANDARD": "0x{Privatekey}" + } \ No newline at end of file diff --git a/src/sample_showrunners/priceTracker/priceTrackerModel.ts b/src/sample_showrunners/priceTracker/priceTrackerModel.ts new file mode 100644 index 0000000..e9abeca --- /dev/null +++ b/src/sample_showrunners/priceTracker/priceTrackerModel.ts @@ -0,0 +1,47 @@ +import { model, Schema } from 'mongoose'; + +export interface PriceTrackerData { + _id?: string; + lastCycle?: number; +} + +const priceTrackerSchema = new Schema({ + _id: { + type: String, + }, + lastCycle: { + type: Number, + }, +}); + +export const priceTrackerModel = model('priceTrackerUserDB', priceTrackerSchema); + +export interface PriceTrackerGlobal { + _id?: string; + cycles?: number; +} + +const priceTrackerGlobalSchema = new Schema({ + _id: { + type: String, + }, + cycles: { + type: Number, + }, +}); + +export const priceTrackerGlobalModel = model('priceTrackerGlobalDB', priceTrackerGlobalSchema); + +export interface PriceTrackerToken { + _id?: String; + symbol?: String; + tokenPrevPrice?: Number; +} + +const PriceTrackerTokenSchema = new Schema({ + _id: String, + symbol: String, + tokenPrevPrice: Number, +}); + +export const priceTrackerTokenModel = model('priceTokenTracker', PriceTrackerTokenSchema); diff --git a/src/sample_showrunners/priceTracker/priceTrackerRoutes.ts b/src/sample_showrunners/priceTracker/priceTrackerRoutes.ts new file mode 100644 index 0000000..4c16aef --- /dev/null +++ b/src/sample_showrunners/priceTracker/priceTrackerRoutes.ts @@ -0,0 +1,37 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { Container } from 'typedi'; +import middlewares from '../../api/middlewares'; +import { celebrate, Joi } from 'celebrate'; +import { Logger } from 'winston'; +import priceTrackerChannel from './priceTrackerChannel'; + +const route = Router(); + +export default (app: Router) => { + app.use('/showrunners/priceTracker', route); + + route.post( + '/booleanNotification', + celebrate({ + body: Joi.object({ + simulate: [Joi.bool(), Joi.object()], + }), + }), + middlewares.onlyLocalhost, + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + logger.debug('Calling /showrunners/wt ticker endpoint with body: %o', req.body); + try { + const priceTracker = Container.get(priceTrackerChannel); + const response = await priceTracker.triggerUserNotification(req.body.simulate); + + return res.status(201).json({ success: true, data: response }); + } catch (e) { + logger.error('πŸ”₯ error: %o', e); + return next(e); + } + }, + ); + + +}; diff --git a/src/sample_showrunners/priceTracker/priceTrackerSettings.json b/src/sample_showrunners/priceTracker/priceTrackerSettings.json new file mode 100644 index 0000000..76b8819 --- /dev/null +++ b/src/sample_showrunners/priceTracker/priceTrackerSettings.json @@ -0,0 +1,10 @@ +{ + "cmcEndpoint": "https://pro-api.coinmarketcap.com/", + "providerUrl":"https://ethereum-sepolia.publicnode.com", + "route":"v2/cryptocurrency/quotes/latest", + "cmcKey":"4fd478f1-5d64-4666-bd4d-c9d1489a6c5e", + "id":"1,1027,1839,5426,2010,5805,1975,6636,3890,9111", + "tokenNames":["BTC","ETH","BNB","SOL","ADA","AVAX","LINK","DOT","MATIC","PUSH"] +} + + diff --git a/src/sample_showrunners/priceTracker/test.json b/src/sample_showrunners/priceTracker/test.json new file mode 100644 index 0000000..6562d4e --- /dev/null +++ b/src/sample_showrunners/priceTracker/test.json @@ -0,0 +1,2 @@ +12:03:56 info [Tue Mar 05 2024 12:25:56 GMT+0530 (India Standard Time)]-[Price Tracker Channel]- Processing [{"type":1,"user":true,"index":1,"default":true,"description":"BTC"},{"type":1,"user":true,"index":2,"default":true,"description":"ETH"},{"type":1,"user":false,"index":3,"default":false,"description":"BNB"},{"type":1,"user":false,"index":4,"default":false,"description":"SOL"},{"type":1,"user":false,"index":5,"default":false,"description":"AVAX"},{"type":1,"user":true,"index":6,"default":true,"description":"MATIC"},{"type":1,"user":false,"index":7,"default":false,"description":"LINK"},{"type":1,"user":false,"index":8,"default":false,"description":"ADA"},{"type":1,"user":true,"index":9,"default":true,"description":"PUSH"},{"type":2,"user":3,"index":10,"ticker":3,"default":3,"enabled":true,"lowerLimit":1,"upperLimit":72,"description":"Time Interval +(In Hrs)"},{"type":2,"user":0,"index":11,"ticker":1,"default":2,"enabled":true,"lowerLimit":0,"upperLimit":100,"description":"Price Change (In %)"}] for 0xef7cc8c01a2aef51cccf4e25c0262664369f5567 \ No newline at end of file