From dcab188cedcc0ee8c2f184c713b1ebf6fa390764 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 1 Feb 2024 16:56:58 +0530 Subject: [PATCH 1/3] Implemented range bound health factor ,borrow and supply apy notifications --- src/sample_showrunners/aave/aaveChannel.ts | 308 +++++++++++++----- src/sample_showrunners/aave/aaveJobs.ts | 15 +- .../aave/aavePoolDataProvidrAbi.json | 1 + src/sample_showrunners/aave/aaveRoutes.ts | 29 +- src/sample_showrunners/aave/aaveSettings.json | 14 +- src/sample_showrunners/aave/mockCache.ts | 88 +++++ src/sample_showrunners/aave/readme.md | 281 ++++++++++++++++ src/sample_showrunners/aave/sample.test.ts | 34 ++ 8 files changed, 683 insertions(+), 87 deletions(-) create mode 100644 src/sample_showrunners/aave/aavePoolDataProvidrAbi.json create mode 100644 src/sample_showrunners/aave/mockCache.ts create mode 100644 src/sample_showrunners/aave/readme.md create mode 100644 src/sample_showrunners/aave/sample.test.ts diff --git a/src/sample_showrunners/aave/aaveChannel.ts b/src/sample_showrunners/aave/aaveChannel.ts index 3f48676..07f1000 100644 --- a/src/sample_showrunners/aave/aaveChannel.ts +++ b/src/sample_showrunners/aave/aaveChannel.ts @@ -1,43 +1,42 @@ // @name: Aave Channel -// @version: 1.0 +// @version: 1.3.0 +// @changes : Implemented Slider type notifications for Supply and borrow APY in Aave(v3) import { Service, Inject } from 'typedi'; -import config, { defaultSdkSettings, settings } from '../../config'; +import config from '../../config'; import { ethers } from 'ethers'; +import keys from "./aaveKeys.json"; import aaveSettings from './aaveSettings.json'; +import { PushAPI, CONSTANTS } from '@pushprotocol/restapi'; import aaveLendingPoolDeployedContractABI from './aave_LendingPool.json'; import { EPNSChannel } from '../../helpers/epnschannel'; import { Logger } from 'winston'; +import aavePoolDataProviderAbi from "./aavePoolDataProvidrAbi.json"; -const NETWORK_TO_MONITOR = config.web3MainnetNetwork; -const HEALTH_FACTOR_THRESHOLD = 1.6; +const NETWORK_TO_MONITOR = aaveSettings.MainnetProvider; +const provider = new ethers.providers.JsonRpcProvider(config.web3MainnetProvider || aaveSettings.MainnetProvider); +const signer = new ethers.Wallet(keys.PRIVATE_KEY_NEW_STANDARD.PK, provider); const CUSTOMIZABLE_SETTINGS = { precision: 3, }; @Service() export default class AaveChannel extends EPNSChannel { - constructor(@Inject('logger') public logger: Logger) { + constructor(@Inject('logger') public logger: Logger, @Inject("cached") public cached) { super(logger, { - sdkSettings: { - epnsCoreSettings: defaultSdkSettings.epnsCoreSettings, - epnsCommunicatorSettings: defaultSdkSettings.epnsCommunicatorSettings, - networkSettings: defaultSdkSettings.networkSettings, - }, networkToMonitor: NETWORK_TO_MONITOR, dirname: __dirname, name: 'Aave', url: 'https://aave.com/', useOffChain: true, - address:'0xAA940b3501176af328423d975C350d0d1BaAae50' }); } - + tokens: string[] = []; + supplyApy: string[] = []; + borrowApy: string[] = []; // To form and write to smart contract - public async sendMessageToContract(simulate) { - const sdk = await this.getSdk(); - this.logInfo('sendMessageToContract'); - + public async getUserSettings(simulate) { + const userAlice = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.STAGING }); //simulate object settings START const logicOverride = typeof simulate == 'object' @@ -45,41 +44,126 @@ export default class AaveChannel extends EPNSChannel { ? simulate.logicOverride.mode : false : false; - const simulateAaveNetwork = - logicOverride && simulate.logicOverride.hasOwnProperty('aaveNetwork') - ? simulate.logicOverride.aaveNetwork - : false; - let aave: any; - if (simulateAaveNetwork) { - this.logInfo('Using Simulated Aave Network'); - aave = sdk.advanced.getInteractableContracts( - simulateAaveNetwork, - settings, - this.walletKey, - aaveSettings.aaveLendingPoolDeployedContractMainnet, - aaveLendingPoolDeployedContractABI, - ); - } else { - this.logInfo('Getting Aave Contract'); - aave = await sdk.getContract( - aaveSettings.aaveLendingPoolDeployedContractMainnet, - JSON.stringify(aaveLendingPoolDeployedContractABI), - ); - this.log(`Got Contract`); - } - this.logInfo(`Getting subscribed users`); - const users = await sdk.getSubscribedUsers(); - for (const user of users) { - let res = await this.checkHealthFactor(aave, user, sdk, simulate); - } + let status: boolean = false; + status = await this.getData(); + if (status) { + let i = 1; - return true; - } + while (true) { + const userData: any = await userAlice.channel.subscribers({ + page: i, + limit: 30, + setting: true, + }); + if (userData.itemcount != 0) { + i++; + userData.subscribers.map((subscriberObj) => { + const userSettings = JSON.parse(subscriberObj.settings); + if (userSettings !== null) { - public async checkHealthFactor(aave, userAddress, sdk: any, simulate) { - this.logInfo(`Checking Health Factor`); + // this.logInfo("User Info" + JSON.stringify(userSettings[0])); + userSettings.map(async (settings) => { + if (settings.index == 1 && settings.enabled == true) { + // Aave user settings Enabled. + let temp = userSettings[0]; + let lowerLimit = JSON.stringify(temp.user.lower); + let upperLimit = JSON.stringify(temp.user.upper); + this.checkHealthFactor(subscriberObj.subscriber, Number(lowerLimit), Number(upperLimit), simulate); + } + else if (settings.index == 1 && settings.enabled == false) { + //If User settings Exist but is disabled by the user => send normal notification. + this.checkHealthFactor(subscriberObj.subscriber, 0, 3, simulate) + } + // Supply APY code goes here --> + if (settings.index == 2 && settings.enabled == true) { + let k = 0; + let loopCounter = 0; + let title = 'Aave v3 supply APYs are here!'; + let message = 'Here is a List of Assets that you can supply to on Aave v3'; + let payloadTitle = 'Aave V3 Supply APY Alert!'; + let payloadMsg = ``; + let notificationType = 3; + this.supplyApy.map(async (apy) => { + // console.log(apy); + if (Number(apy) >= Number(JSON.stringify(settings.user))) { + if (loopCounter % 2 == 0 && loopCounter != 1) { + let sentence = `${this.tokens[k]}'s APY :[d:${apy}]%\t\t`; + payloadMsg += sentence; + + } else { + let sentence = `${this.tokens[k]}'s APY :[d:${apy}]%\n`; + payloadMsg += sentence; + } + loopCounter++; + } + k++; + }) + // console.log("Payload " + payloadMsg) + const tx = await this.sendNotification({ + recipient: subscriberObj.subscriber, + title: title, + message: message, + payloadTitle: payloadTitle, + payloadMsg: payloadMsg, + notificationType: notificationType, + cta: 'https://app.aave.com/#/dashboard', + image: null, + simulate: simulate, + }); + } + // Borrow APY code goes here --> + if (settings.index == 3 && settings.enabled == true) { + let k = 0; + let loopCounter = 0; + let title = `Aave v3's Borrow APYs are here!`; + let message = 'Here is a List of Assets that you can Borrow on Aave v3'; + let payloadTitle = 'Aave V3 Borrow APY Alert!'; + let payloadMsg = ``; + let notificationType = 3; + this.borrowApy.map(async (apy) => { + if (Number(apy) >= Number(JSON.stringify(settings.user))) { + if (loopCounter % 2 == 0 && loopCounter != 1) { + let sentence = `${this.tokens[k]}'s APY :[d:${apy}]%\t\t`; + payloadMsg += sentence; + } else { + let sentence = `${this.tokens[k]}'s APY :[d:${apy}]%\n`; + payloadMsg += sentence; + } + loopCounter++; + } + + k++; + }) + const tx = await this.sendNotification({ + recipient: subscriberObj.subscriber, + title: title, + message: message, + payloadTitle: payloadTitle, + payloadMsg: payloadMsg, + notificationType: notificationType, + cta: 'https://app.aave.com/#/dashboard', + image: null, + simulate: simulate, + }); + } + }) + } + else { + // For Users who have not opted into notification setting + // this.checkHealthFactor(subscriberObj.subscriber, 0, 3, simulate) + } + }) + } else { + break; + } + } + this.logInfo("-------------[ JOB Finished ]------------------"); + return true; + } + } + public async checkHealthFactor(userAddress, lowerLimit, upperLimit, simulate) { try { const logicOverride = typeof simulate == 'object' @@ -96,41 +180,71 @@ export default class AaveChannel extends EPNSChannel { ? simulate.logicOverride.aaveNetwork : false; - if (!aave) { - aave = await sdk.getContract( - aaveSettings.aaveLendingPoolDeployedContractMainnet, - JSON.stringify(aaveLendingPoolDeployedContractABI), - ); - } if (!userAddress) { if (simulateApplyToAddr) { userAddress = simulateApplyToAddr; } else { - this.logDebug('userAddress is not defined'); + // this.logDebug('userAddress is not defined'); } } } catch (err) { this.logError('An error occured while checking health factor'); this.logError(err); } + try{ + let aaveV2 = await this.getContract( + aaveSettings.aaveLendingPoolDeployedContractMainnet, + JSON.stringify(aaveLendingPoolDeployedContractABI), + ); + let aaveV3 = await this.getContract( + aaveSettings.aaveV3PoolContractMainnet, + JSON.stringify(aaveLendingPoolDeployedContractABI), + ); + // console.log("User Address"+userAddress); //simulate object settings END - - const userData = await aave.contract.getUserAccountData(userAddress); - let healthFactor = ethers.utils.formatEther(userData.healthFactor); - this.logInfo('For wallet: %s, Health Factor: %o', userAddress, healthFactor); - if (Number(healthFactor) <= HEALTH_FACTOR_THRESHOLD) { + const aaveV2UserData = await aaveV2?.contract.getUserAccountData(userAddress); + const aaveV3UserData = await aaveV3?.contract.getUserAccountData(userAddress); + let healthFactorV2 = ethers.utils.formatEther(aaveV2UserData.healthFactor); + let healthFactorV3 = ethers.utils.formatEther(aaveV3UserData.healthFactor); + // console.log(`HF of ${userAddress} is ${healthFactorV3}`) + // this.logInfo('For wallet: %s, Health Factor: %o', userAddress, healthFactor); + if (Number(healthFactorV2).toFixed(2) >= lowerLimit && Number(healthFactorV2).toFixed(2) <= upperLimit) { + // this.logInfo("Aave v2 Notification sending to " + userAddress); const precision = CUSTOMIZABLE_SETTINGS.precision; - const newHealthFactor = parseFloat(healthFactor).toFixed(precision); - const title = 'Aave Liquidity Alert!'; + const newHealthFactor = parseFloat(healthFactorV2).toFixed(precision); + const title = 'Aave V2 Liquidation Alert!'; + const message = + userAddress + + ' your account has healthFactor ' + + newHealthFactor + + '. Maintain it above 1 to avoid liquidation.'; + const payloadTitle = 'Aave V2 Liquidity Alert!'; + const payloadMsg = `Your account on Aave V2 has healthFactor [d:${newHealthFactor}] . Maintain it above 1 to avoid liquidation.`; + const notificationType = 3; + const tx = await this.sendNotification({ + recipient: userAddress, + title: title, + message: message, + payloadTitle: payloadTitle, + payloadMsg: payloadMsg, + notificationType: notificationType, + cta: 'https://app.aave.com/#/dashboard', + image: null, + simulate: simulate, + }); + } + if (Number(healthFactorV3).toFixed(2) >= lowerLimit && Number(healthFactorV3).toFixed(2) <= upperLimit) { + this.logInfo("Aave v3 Notification sending to " + userAddress); + const precision = CUSTOMIZABLE_SETTINGS.precision; + const newHealthFactor = parseFloat(healthFactorV3).toFixed(precision); + const title = 'Aave V3 Liquidation Alert!'; const message = userAddress + ' your account has healthFactor ' + newHealthFactor + '. Maintain it above 1 to avoid liquidation.'; - const payloadTitle = 'Aave Liquidity Alert!'; - const payloadMsg = `Your account has healthFactor [b:${newHealthFactor}] . Maintain it above 1 to avoid liquidation.[timestamp: ${Math.floor( - Date.now() / 1000, - )}]`; + const payloadTitle = 'Aave V3 Liquidity Alert!'; + const payloadMsg = `Your account on Aave V3 has healthFactor [d:${newHealthFactor}] . Maintain it above 1 to avoid liquidation.`; const notificationType = 3; const tx = await this.sendNotification({ recipient: userAddress, @@ -143,17 +257,63 @@ export default class AaveChannel extends EPNSChannel { image: null, simulate: simulate, }); + } else { + // this.logInfo(`[Wallet: ${userAddress} is SAFE with Health Factor:: ${healthFactor}`); + } + return true; + }catch(e){ + this.logInfo("Error occured in Aave Liquidity Alert") + } + } + + + public async getData():Promise { + try{ + let aaveV3 = await this.getContract( + aaveSettings.aaveV3PoolDataProvider, + JSON.stringify(aavePoolDataProviderAbi), + ); - return { - success: true, - data: tx, - }; + //Re-settings Arrays + this.tokens.length = 0; + this.supplyApy.length = 0; + this.borrowApy.length = 0; + + let aaveV3Tokens = await aaveV3?.contract.getAllReservesTokens(); + let RAY = 10 ** 27 // 10 to the power 27 + let SECONDS_PER_YEAR = 31536000 + // console.log("Tokens"+aaveV3Tokens[1]); + for (let i = 0; i < aaveV3Tokens.length; i++) { + + let aaveV2APR = await aaveV3?.contract.getReserveData(aaveV3Tokens[i][1]); + let depositAPR = (aaveV2APR[5] / RAY) + let variableBorrowAPR = (aaveV2APR[6] / RAY) + + let depositAPY = (((1 + (depositAPR / SECONDS_PER_YEAR)) ** SECONDS_PER_YEAR) - 1) * 100 + let variableBorrowAPY = (((1 + (variableBorrowAPR / SECONDS_PER_YEAR)) ** SECONDS_PER_YEAR) - 1) * 100 + this.tokens.push(aaveV3Tokens[i][0]); + this.supplyApy.push((depositAPY).toFixed(2)); + this.borrowApy.push(variableBorrowAPY.toFixed(2)); + + // console.log(aaveV3Tokens[i][0] + "[" + depositAPY.toFixed(2) + "," + variableBorrowAPY.toFixed(2) + "]"); + + } + + }catch(e){ + this.logInfo("Error occured in Supply Borrow APY in aave") + } + return true; + } + + public async testLogic(healthFactor) { + + if (Number(healthFactor) >= 0 && Number(healthFactor) <= 3) { + const precision = CUSTOMIZABLE_SETTINGS.precision; + const newHealthFactor = parseFloat(healthFactor).toFixed(precision); + return true; } else { - this.logInfo(`[Wallet: ${userAddress} is SAFE with Health Factor:: ${healthFactor}`); - return { - success: false, - data: userAddress + ' is not about to get liquidated', - }; + return false; } } + } diff --git a/src/sample_showrunners/aave/aaveJobs.ts b/src/sample_showrunners/aave/aaveJobs.ts index fbacdbf..4e9a6aa 100644 --- a/src/sample_showrunners/aave/aaveJobs.ts +++ b/src/sample_showrunners/aave/aaveJobs.ts @@ -23,20 +23,19 @@ import AaveChannel from './aaveChannel'; export default () => { const startTime = new Date(new Date().setHours(0, 0, 0, 0)); - const dailyRule = new schedule.RecurrenceRule(); - dailyRule.hour = 0; - dailyRule.minute = 0; - dailyRule.second = 0; - dailyRule.dayOfWeek = new schedule.Range(0, 6); + const threeHourRule = new schedule.RecurrenceRule(); + threeHourRule.hour = new schedule.Range(0, 23, 3); + threeHourRule.minute = 0; + threeHourRule.second = 0; // AAVE CHANNEL RUNS EVERY 24 Hours - logger.info(` 🛵 Scheduling Showrunner - Aave Channel [on 6 Hours] [${new Date(Date.now())}]`); - schedule.scheduleJob({ start: startTime, rule: dailyRule }, async function () { + logger.info(` 🛵 Scheduling Showrunner - Aave Channel [on 3 Hours] [${new Date(Date.now())}]`); + schedule.scheduleJob({ start: startTime, rule: threeHourRule }, async function () { const aaveChannel = Container.get(AaveChannel); const taskName = 'Aave users address checks and sendMessageToContract()'; try { - await aaveChannel.sendMessageToContract(false); + await aaveChannel.getUserSettings(false); logger.info(`[${new Date(Date.now())}] 🐣 Cron Task Completed -- ${taskName}`); } catch (err) { diff --git a/src/sample_showrunners/aave/aavePoolDataProvidrAbi.json b/src/sample_showrunners/aave/aavePoolDataProvidrAbi.json new file mode 100644 index 0000000..65c906b --- /dev/null +++ b/src/sample_showrunners/aave/aavePoolDataProvidrAbi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"contract IPoolAddressesProvider","name":"addressesProvider","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ADDRESSES_PROVIDER","outputs":[{"internalType":"contract IPoolAddressesProvider","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getATokenTotalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAllATokens","outputs":[{"components":[{"internalType":"string","name":"symbol","type":"string"},{"internalType":"address","name":"tokenAddress","type":"address"}],"internalType":"struct IPoolDataProvider.TokenData[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAllReservesTokens","outputs":[{"components":[{"internalType":"string","name":"symbol","type":"string"},{"internalType":"address","name":"tokenAddress","type":"address"}],"internalType":"struct IPoolDataProvider.TokenData[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getDebtCeiling","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getDebtCeilingDecimals","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getFlashLoanEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getInterestRateStrategyAddress","outputs":[{"internalType":"address","name":"irStrategyAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getLiquidationProtocolFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getPaused","outputs":[{"internalType":"bool","name":"isPaused","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getReserveCaps","outputs":[{"internalType":"uint256","name":"borrowCap","type":"uint256"},{"internalType":"uint256","name":"supplyCap","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getReserveConfigurationData","outputs":[{"internalType":"uint256","name":"decimals","type":"uint256"},{"internalType":"uint256","name":"ltv","type":"uint256"},{"internalType":"uint256","name":"liquidationThreshold","type":"uint256"},{"internalType":"uint256","name":"liquidationBonus","type":"uint256"},{"internalType":"uint256","name":"reserveFactor","type":"uint256"},{"internalType":"bool","name":"usageAsCollateralEnabled","type":"bool"},{"internalType":"bool","name":"borrowingEnabled","type":"bool"},{"internalType":"bool","name":"stableBorrowRateEnabled","type":"bool"},{"internalType":"bool","name":"isActive","type":"bool"},{"internalType":"bool","name":"isFrozen","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getReserveData","outputs":[{"internalType":"uint256","name":"unbacked","type":"uint256"},{"internalType":"uint256","name":"accruedToTreasuryScaled","type":"uint256"},{"internalType":"uint256","name":"totalAToken","type":"uint256"},{"internalType":"uint256","name":"totalStableDebt","type":"uint256"},{"internalType":"uint256","name":"totalVariableDebt","type":"uint256"},{"internalType":"uint256","name":"liquidityRate","type":"uint256"},{"internalType":"uint256","name":"variableBorrowRate","type":"uint256"},{"internalType":"uint256","name":"stableBorrowRate","type":"uint256"},{"internalType":"uint256","name":"averageStableBorrowRate","type":"uint256"},{"internalType":"uint256","name":"liquidityIndex","type":"uint256"},{"internalType":"uint256","name":"variableBorrowIndex","type":"uint256"},{"internalType":"uint40","name":"lastUpdateTimestamp","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getReserveEModeCategory","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getReserveTokensAddresses","outputs":[{"internalType":"address","name":"aTokenAddress","type":"address"},{"internalType":"address","name":"stableDebtTokenAddress","type":"address"},{"internalType":"address","name":"variableDebtTokenAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getSiloedBorrowing","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getTotalDebt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"}],"name":"getUnbackedMintCap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"asset","type":"address"},{"internalType":"address","name":"user","type":"address"}],"name":"getUserReserveData","outputs":[{"internalType":"uint256","name":"currentATokenBalance","type":"uint256"},{"internalType":"uint256","name":"currentStableDebt","type":"uint256"},{"internalType":"uint256","name":"currentVariableDebt","type":"uint256"},{"internalType":"uint256","name":"principalStableDebt","type":"uint256"},{"internalType":"uint256","name":"scaledVariableDebt","type":"uint256"},{"internalType":"uint256","name":"stableBorrowRate","type":"uint256"},{"internalType":"uint256","name":"liquidityRate","type":"uint256"},{"internalType":"uint40","name":"stableRateLastUpdated","type":"uint40"},{"internalType":"bool","name":"usageAsCollateralEnabled","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/src/sample_showrunners/aave/aaveRoutes.ts b/src/sample_showrunners/aave/aaveRoutes.ts index f1bc173..f08b967 100644 --- a/src/sample_showrunners/aave/aaveRoutes.ts +++ b/src/sample_showrunners/aave/aaveRoutes.ts @@ -29,7 +29,7 @@ export default (app: Router) => { logger.debug('Calling /showrunners/aave/send_message endpoint with body: %o', req.body); try { const aave = Container.get(aaveChannel); - const data = await aave.sendMessageToContract(req.body.simulate); + const data = await aave.getUserSettings(req.body.simulate); return res.status(200).json({ success: true, data: data }); } catch (e) { logger.error('🔥 error: %o', e); @@ -39,7 +39,7 @@ export default (app: Router) => { ); route.post( - '/checkHealthFactor', + '/getData', celebrate({ body: Joi.object({ simulate: [Joi.bool(), Joi.object()], @@ -51,7 +51,30 @@ export default (app: Router) => { logger.debug('Calling /showrunners/aave/send_message endpoint with body: %o', req.body); try { const aave = Container.get(aaveChannel); - const data = await aave.checkHealthFactor(null, null, null, req.body.simulate); + const data = await aave.getData(); + return res.status(200).json({ success: true, data: data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return handleResponse(res, 500, false, 'error', JSON.stringify(e)); + } + }, + ); + + route.post( + '/checkHealthFactor', + 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/aave/send_message endpoint with body: %o',req.body); + try { + // console.log("Body"+req.body); + const aave = Container.get(aaveChannel); + const data = await aave.checkHealthFactor(JSON.stringify(req.body.simulate.logicOverride.applyToAddr),0,3, req.body.simulate); return res.status(200).json({ success: true, data: data }); } catch (e) { diff --git a/src/sample_showrunners/aave/aaveSettings.json b/src/sample_showrunners/aave/aaveSettings.json index 69e7f11..50d0553 100644 --- a/src/sample_showrunners/aave/aaveSettings.json +++ b/src/sample_showrunners/aave/aaveSettings.json @@ -1,6 +1,16 @@ { - "aaveLendingPoolDeployedContractPolygonMainnet": "0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf", "aaveLendingPoolDeployedContractPolygonMumbai": "0x9198F13B08E299d85E096929fA9781A1E3d5d827", "aaveLendingPoolDeployedContractMainnet": "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", - "aaveLendingPoolDeployedContractKovan": "0xE0fBa4Fc209b4948668006B2bE61711b7f465bAe" + "aaveV3PoolContractMainnet":"0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", + "aaveV3PoolContractMumbai":"0xcC6114B983E4Ed2737E9BD3961c9924e6216c704", + "aaveLendingPoolDeployedContractGoerli": "0x4bd5643ac6f66a5237E18bfA7d47cF22f1c9F210", + "goerliProvider":"https://eth-goerli.api.onfinality.io/public", + "MainnetProvider":"https://rpc.ankr.com/eth", + "MumbaiProvider":"https://rpc.ankr.com/polygon_mumbai", + "polygonProvider":"https://rpc.ankr.com/polygon", + "aaveV3PoolDataProvider":"0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", + "aaveV2PoolDataProvider":"0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d", + "aaveV3PolygonPoolDataProvider":"0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654", + "aaveV3PolygonPoolLending":"0x794a61358D6845594F94dc1DB02A252b5b4814aD", + "aaveV2PoolLendingMumbai":"0x8dff5e27ea6b7ac08ebfdf9eb090f32ee9a30fcf" } \ No newline at end of file diff --git a/src/sample_showrunners/aave/mockCache.ts b/src/sample_showrunners/aave/mockCache.ts new file mode 100644 index 0000000..20827ef --- /dev/null +++ b/src/sample_showrunners/aave/mockCache.ts @@ -0,0 +1,88 @@ +const redis = require('async-redis'); + +class CacheInstance { + private ReddisInstance; + constructor() {} + /** + * Set cache + * @description adds a part + * @param {String} key Cache Key + * @param {String} value Cache Value + * @return {Promise<{ null }>} + */ + public async setCache(key: String, value: Number) { + return null; + } + + /** + * push lcache + * @description adds to a cache like an array + * @param {String} key Cache Key + * @param {String} value Cache Value + * @return {Promise<{ null }>} + */ + public async pushLCache(key: String, value: Number) { + return null; + } + + /** + * get lcache + * @description get all items in a list + * @param {String} key Cache Key + * @return {Promise<{ null }>} + */ + public async getLCache(key: String) { + return null; + } + + /** + * Add caches + * @description adds to already existing value in cache + * @param {String} key Cache Key + * @param {Number} value Value to be added + * @return {Promise<{ null }>} + */ + public async addCache(key: String, value: Number) { + const prev: Number = Number(await this.getCache(key)); + if (prev != 0) { + value = Number(prev) + Number(value); + value = Number(value) / 2; + } + return null; + } + + /** + * Remove cache + * @description deletes a cache key and its associated values + * @param {String} key Cache Key + * @return {Promise<{ null }>} + */ + public async removeCache(key: String) { + return null; + } + + /** + * Get cache + * @description retrieves the value of a cache key + * @param {String} key Cache Key + * @return {Promise<{ String }>} + */ + public async getCache(key: String) { + return null; + } + + /** + * Set set Icache + * @description add cache for certain time period + * @param {String} key Cache Key + * @param {Number} expiresIn Time in seconds for expiry + * @param {String} value Cache value + * @return {Promise<{ String }>} + */ + + public async addIcache(key: String, value: String, expiresIn: Number) { + return null; + } +} + +export default new CacheInstance(); \ No newline at end of file diff --git a/src/sample_showrunners/aave/readme.md b/src/sample_showrunners/aave/readme.md new file mode 100644 index 0000000..8c0ec2d --- /dev/null +++ b/src/sample_showrunners/aave/readme.md @@ -0,0 +1,281 @@ + +# Notification settings using Showrunners + +## Inside Aave's Notification Settings. + +- The Aave Channel supports both versions of Aave i.e Aave v2 and v3. +- Need of the channel : To alert the user of his health factor before Liquidation of his assets starts. +- You can select a range of Health Factor and if your health factor falls in that range then a notification about your health factor will be sent. + - Eg : Only send me notification if my health factor is in range of 1.2 to 1.5. +-Every 3 hours , Aave channel will check for your health factor on both versions and then send/not send notification. + +### Pre-requisites + +- In this tutorial, we will only understand the logic part of Aave i.e how Notifications are being triggered in Aave. +- For step-by-step tutorial of Notification settings,please refer "Eth Tracker Tutorial(Link here)". +- Subscribe to this channel Address [Aave Channel]("app.push.org/channels?channel=0xAA940b3501176af328423d975C350d0d1BaAae50","Aave channel on Push") + +### Perform following steps to get started with Notification settings. + +Step 1 : Create a Position on [Aave V3/V2[Mainnet]]("https://app.aave.com/","Aave V3 channel on Mainnet") by Supplying and borrowing against some asset. +Step 2: You should be able to see some health Factor like 1.5 or 1.8. +Step 3: +- Create a function called ``getUserSetings()`` which will have main logic part. +- This function will do following things : + - Fetch Notification setting set by the user. + - If Notification settings exist and is enabled by the user then get the "lower" and "upper" range values set by the user and pass the values to the "checkHealthFactor" function with susbcriber's address. + - If Notification settings exists but are turned off i.e disabled by the user then pass dummy values of lower and upper values to the "checkHealthFactor" function. + (Fun Fact : If you have just supplied assets on Aave and have not borrowed against it then you would have a crazy Health Factor > 3 due to which no notifications will be sent.). + +```javascript + while (true) { + const userData: any = await userAlice.channel.subscribers({ + page: i, + limit: 30, + setting: true, + }); + if (userData.itemcount != 0) { + i++; + userData.subscribers.map((subscriberObj) => { + const userSettings = JSON.parse(subscriberObj.settings); + if (userSettings !== null) { + status = false; + + // this.logInfo("User Info" + JSON.stringify(userSettings[0])); + userSettings.map(async (settings) => { + if (settings.index == 1 && settings.enabled == true) { + // Aave user settings Enabled. + let temp = userSettings[0]; + let lowerLimit = JSON.stringify(temp.user.lower); + let upperLimit = JSON.stringify(temp.user.upper); + this.checkHealthFactor(subscriberObj.subscriber, Number(lowerLimit), Number(upperLimit), simulate); + } + else if (settings.index == 1 && settings.enabled == false) { + //If User settings Exist but is disabled by the user => send normal notification. + this.checkHealthFactor(subscriberObj.subscriber, 0, 3, simulate) + } + // Supply APY code goes here --> + + //Borrow APY code goes here --> + }); + } + })}else{ + i=1; + break; + } + } +``` + - If Notification settings dosen't exist then we would just send a normal notification if there Health Factor is in range of 0 to 3. + - Here if you are wondering why i have set the range as 0 to 3 then the reason is that if you don't borrow against your asset then your asset is completely safe and you need not to worry about anything but if you have borrowed against youe assets then you need to worry about liquidation og your assets. + - Generally if you borrow then your health decreases and comes in range of 1 to 3 and if your health factor reaches 1 then your assets can get liquidated. +- In this step we will fetch the Notification settings of our channel's subscribers. + +Step 4: +- In this step we check the health factor of Channel subscribers and then trigger notification on the basis of their upper and lower values set by the user. + +```javascript + public async checkHealthFactor(userAddress, lowerLimit, upperLimit, simulate) { + try { + const logicOverride = + typeof simulate == 'object' + ? simulate.hasOwnProperty('logicOverride') && simulate.logicOverride.mode + ? simulate.logicOverride.mode + : false + : false; + const simulateApplyToAddr = + logicOverride && simulate.logicOverride.hasOwnProperty('applyToAddr') + ? simulate.logicOverride.applyToAddr + : false; + const simulateAaveNetwork = + logicOverride && simulate.logicOverride.hasOwnProperty('aaveNetwork') + ? simulate.logicOverride.aaveNetwork + : false; + + if (!userAddress) { + if (simulateApplyToAddr) { + userAddress = simulateApplyToAddr; + } else { + // this.logDebug('userAddress is not defined'); + } + } + } catch (err) { + this.logError('An error occured while checking health factor'); + this.logError(err); + } + try{ + let aaveV2 = await this.getContract( + aaveSettings.aaveLendingPoolDeployedContractMainnet, + JSON.stringify(aaveLendingPoolDeployedContractABI), + ); + let aaveV3 = await this.getContract( + aaveSettings.aaveV3PoolContractMainnet, + JSON.stringify(aaveLendingPoolDeployedContractABI), + ); + // console.log("User Address"+userAddress); + //simulate object settings END + const aaveV2UserData = await aaveV2?.contract.getUserAccountData(userAddress); + const aaveV3UserData = await aaveV3?.contract.getUserAccountData(userAddress); + let healthFactorV2 = ethers.utils.formatEther(aaveV2UserData.healthFactor); + let healthFactorV3 = ethers.utils.formatEther(aaveV3UserData.healthFactor); + // console.log(`HF of ${userAddress} is ${healthFactorV3}`) + // this.logInfo('For wallet: %s, Health Factor: %o', userAddress, healthFactor); + if (Number(healthFactorV2).toFixed(2) >= lowerLimit && Number(healthFactorV2).toFixed(2) <= upperLimit) { + // this.logInfo("Aave v2 Notification sending to " + userAddress); + const precision = CUSTOMIZABLE_SETTINGS.precision; + const newHealthFactor = parseFloat(healthFactorV2).toFixed(precision); + const title = 'Aave V2 Liquidation Alert!'; + const message = + userAddress + + ' your account has healthFactor ' + + newHealthFactor + + '. Maintain it above 1 to avoid liquidation.'; + const payloadTitle = 'Aave V2 Liquidity Alert!'; + const payloadMsg = `Your account on Aave V2 has healthFactor [d:${newHealthFactor}] . Maintain it above 1 to avoid liquidation.`; + const notificationType = 3; + const tx = await this.sendNotification({ + recipient: userAddress, + title: title, + message: message, + payloadTitle: payloadTitle, + payloadMsg: payloadMsg, + notificationType: notificationType, + cta: 'https://app.aave.com/#/dashboard', + image: null, + simulate: simulate, + }); + } + if (Number(healthFactorV3).toFixed(2) >= lowerLimit && Number(healthFactorV3).toFixed(2) <= upperLimit) { + this.logInfo("Aave v3 Notification sending to " + userAddress); + const precision = CUSTOMIZABLE_SETTINGS.precision; + const newHealthFactor = parseFloat(healthFactorV3).toFixed(precision); + const title = 'Aave V3 Liquidation Alert!'; + const message = + userAddress + + ' your account has healthFactor ' + + newHealthFactor + + '. Maintain it above 1 to avoid liquidation.'; + const payloadTitle = 'Aave V3 Liquidity Alert!'; + const payloadMsg = `Your account on Aave V3 has healthFactor [d:${newHealthFactor}] . Maintain it above 1 to avoid liquidation.`; + const notificationType = 3; + const tx = await this.sendNotification({ + recipient: userAddress, + title: title, + message: message, + payloadTitle: payloadTitle, + payloadMsg: payloadMsg, + notificationType: notificationType, + cta: 'https://app.aave.com/#/dashboard', + image: null, + simulate: simulate, + }); + } else { + // this.logInfo(`[Wallet: ${userAddress} is SAFE with Health Factor:: ${healthFactor}`); + } + return true; + }catch(e){ + this.logInfo("Error occured in Aave Liquidity Alert") + } + } +``` + + +Step 5 : Our second goal is to give notification about APYs for assets that can be borrowed or supplied to on aave. + +- So first we will start by getting all the addresses of assets available on Aave. + +- In the below code,we are quering the addresses of the assets using "getAllReservesTokens" method, it returns a array in which each element is a array itself with two values i.e [assetName,assetAdderss]. + +- Up next,we have a for loop in which we are quering the data of APR about each asset using "getReserveData" method. + (APR : It is the rate of interest you get on your asset on yearly basis. + APY : It is the compounded rate of interest you get on your assets on yearly basis. ) + +```javascript + let status:boolean = false; + status = await this.getData(); + + public async getData(){ + let aaveV3 = await this.getContract( + aaveSettings.aaveV3PoolDataProvider, + JSON.stringify(aavePoolDataProviderAbi), + ); + + //Re-settings Arrays + this.tokens.length = 0; + this.supplyApy.length = 0; + this.borrowApy.length = 0; + + let aaveV3Tokens = await aaveV3?.contract.getAllReservesTokens(); + let RAY = 10 ** 27 // 10 to the power 27 + let SECONDS_PER_YEAR = 31536000 + +for(let i=0;i { + // console.log(apy); + if (Number(apy) >=Number(JSON.stringify(settings.user))) { + if(loopCounter%2 == 0 && loopCounter !=1){ + let sentence = `${this.tokens[k]}'s APY :[d:${apy}]%\t\t`; + payloadMsg += sentence; + + }else{ + let sentence = `${this.tokens[k]}'s APY :[d:${apy}]%\n`; + payloadMsg += sentence; + } + loopCounter++; + } + k++; + }) + // console.log("Payload " + payloadMsg) + const tx = await this.sendNotification({ + recipient: subscriberObj.subscriber, + title: title, + message: message, + payloadTitle: payloadTitle, + payloadMsg: payloadMsg, + notificationType: notificationType, + cta: 'https://app.aave.com/#/dashboard', + image: null, + simulate: simulate, + }); + } +``` +- We are using a similar logic for Borrow APY too which can be decoded easily. + +- I hope you enjoyed this tutotial,reach out to [Push Discord]("https://discord.gg/pushprotocol","Push Discord Link") if you stuck somehwere or have any doubts. + diff --git a/src/sample_showrunners/aave/sample.test.ts b/src/sample_showrunners/aave/sample.test.ts new file mode 100644 index 0000000..5814d56 --- /dev/null +++ b/src/sample_showrunners/aave/sample.test.ts @@ -0,0 +1,34 @@ +import AaveChannel from "./aaveChannel"; +import fakeCache from './mockCache'; +import Container from 'typedi'; + +// mock logger object +const mockLogger: any = { + info: (...___: any[]) => true, + debug: (...___: any[]) => true, + error: (...value: any[]) => console.log('test', ...value), +}; + +const aave = new AaveChannel(mockLogger as any,fakeCache); +describe('Liquidation Factors', () => { + + beforeAll(() => { + Container.set('cached', fakeCache); + }); + + it('Can check Health factor between 0 to 3', async() => { + let i=0; + while(i<=3){ + expect(await aave.testLogic(i)).toBe(true); + i = i+0.1; + } + }); + + it('It checks Health factor above 3', async() => { + let i=3.1; + while(i<=4){ + expect(await aave.testLogic(i)).toBe(false); + i = i+0.1; + } + }); +}); From 4fd3661e28be13e209d016afe5a5b213d173db52 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 1 Feb 2024 16:57:29 +0530 Subject: [PATCH 2/3] Implemented range bound health factor ,borrow and supply apy notifications --- src/sample_showrunners/aave/mockCache.ts | 88 ---------------------- src/sample_showrunners/aave/sample.test.ts | 34 --------- 2 files changed, 122 deletions(-) delete mode 100644 src/sample_showrunners/aave/mockCache.ts delete mode 100644 src/sample_showrunners/aave/sample.test.ts diff --git a/src/sample_showrunners/aave/mockCache.ts b/src/sample_showrunners/aave/mockCache.ts deleted file mode 100644 index 20827ef..0000000 --- a/src/sample_showrunners/aave/mockCache.ts +++ /dev/null @@ -1,88 +0,0 @@ -const redis = require('async-redis'); - -class CacheInstance { - private ReddisInstance; - constructor() {} - /** - * Set cache - * @description adds a part - * @param {String} key Cache Key - * @param {String} value Cache Value - * @return {Promise<{ null }>} - */ - public async setCache(key: String, value: Number) { - return null; - } - - /** - * push lcache - * @description adds to a cache like an array - * @param {String} key Cache Key - * @param {String} value Cache Value - * @return {Promise<{ null }>} - */ - public async pushLCache(key: String, value: Number) { - return null; - } - - /** - * get lcache - * @description get all items in a list - * @param {String} key Cache Key - * @return {Promise<{ null }>} - */ - public async getLCache(key: String) { - return null; - } - - /** - * Add caches - * @description adds to already existing value in cache - * @param {String} key Cache Key - * @param {Number} value Value to be added - * @return {Promise<{ null }>} - */ - public async addCache(key: String, value: Number) { - const prev: Number = Number(await this.getCache(key)); - if (prev != 0) { - value = Number(prev) + Number(value); - value = Number(value) / 2; - } - return null; - } - - /** - * Remove cache - * @description deletes a cache key and its associated values - * @param {String} key Cache Key - * @return {Promise<{ null }>} - */ - public async removeCache(key: String) { - return null; - } - - /** - * Get cache - * @description retrieves the value of a cache key - * @param {String} key Cache Key - * @return {Promise<{ String }>} - */ - public async getCache(key: String) { - return null; - } - - /** - * Set set Icache - * @description add cache for certain time period - * @param {String} key Cache Key - * @param {Number} expiresIn Time in seconds for expiry - * @param {String} value Cache value - * @return {Promise<{ String }>} - */ - - public async addIcache(key: String, value: String, expiresIn: Number) { - return null; - } -} - -export default new CacheInstance(); \ No newline at end of file diff --git a/src/sample_showrunners/aave/sample.test.ts b/src/sample_showrunners/aave/sample.test.ts deleted file mode 100644 index 5814d56..0000000 --- a/src/sample_showrunners/aave/sample.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import AaveChannel from "./aaveChannel"; -import fakeCache from './mockCache'; -import Container from 'typedi'; - -// mock logger object -const mockLogger: any = { - info: (...___: any[]) => true, - debug: (...___: any[]) => true, - error: (...value: any[]) => console.log('test', ...value), -}; - -const aave = new AaveChannel(mockLogger as any,fakeCache); -describe('Liquidation Factors', () => { - - beforeAll(() => { - Container.set('cached', fakeCache); - }); - - it('Can check Health factor between 0 to 3', async() => { - let i=0; - while(i<=3){ - expect(await aave.testLogic(i)).toBe(true); - i = i+0.1; - } - }); - - it('It checks Health factor above 3', async() => { - let i=3.1; - while(i<=4){ - expect(await aave.testLogic(i)).toBe(false); - i = i+0.1; - } - }); -}); From 3d4a68443cc38c27b7a3265c85f57caedcf1ff06 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 1 Feb 2024 17:08:37 +0530 Subject: [PATCH 3/3] added minor fixes --- src/sample_showrunners/aave/aaveChannel.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/sample_showrunners/aave/aaveChannel.ts b/src/sample_showrunners/aave/aaveChannel.ts index 07f1000..960aa8a 100644 --- a/src/sample_showrunners/aave/aaveChannel.ts +++ b/src/sample_showrunners/aave/aaveChannel.ts @@ -74,7 +74,7 @@ export default class AaveChannel extends EPNSChannel { } else if (settings.index == 1 && settings.enabled == false) { //If User settings Exist but is disabled by the user => send normal notification. - this.checkHealthFactor(subscriberObj.subscriber, 0, 3, simulate) + // Don't send any notifications. } // Supply APY code goes here --> if (settings.index == 2 && settings.enabled == true) { @@ -133,7 +133,6 @@ export default class AaveChannel extends EPNSChannel { } loopCounter++; } - k++; }) const tx = await this.sendNotification({ @@ -152,7 +151,7 @@ export default class AaveChannel extends EPNSChannel { } else { // For Users who have not opted into notification setting - // this.checkHealthFactor(subscriberObj.subscriber, 0, 3, simulate) + this.checkHealthFactor(subscriberObj.subscriber, 0, 3, simulate) } }) } else { @@ -305,15 +304,4 @@ export default class AaveChannel extends EPNSChannel { return true; } - public async testLogic(healthFactor) { - - if (Number(healthFactor) >= 0 && Number(healthFactor) <= 3) { - const precision = CUSTOMIZABLE_SETTINGS.precision; - const newHealthFactor = parseFloat(healthFactor).toFixed(precision); - return true; - } else { - return false; - } - } - }