Defi based notification via Showrunner example
This is a step-by-step introductory tutorial that will teach you how to build a channel based on DEFI protocols and their respective events in a contract.
HELLO!
If you are new to Push protocol and don't have a proper idea of how to create a DEFI-based channel for notifications on top of showrunners. This guide is for you ;)
We will walk through coding and interacting with the aave smart contract for monitoring the events.
And don’t worry if you don’t understand what any these words mean yet, I'll explain everything!
- Contract Address for AAVE Lending Pool on the ethereum main-network :
0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9
- ABI for AAVE Lending Pool smart contract:
[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "onBehalfOf",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "borrowRateMode",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "borrowRate",
"type": "uint256"
},
{
"indexed": true,
"internalType": "uint16",
"name": "referral",
"type": "uint16"
}
],
"name": "Borrow",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "onBehalfOf",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": true,
"internalType": "uint16",
"name": "referral",
"type": "uint16"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "target",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "initiator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "premium",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint16",
"name": "referralCode",
"type": "uint16"
}
],
"name": "FlashLoan",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "collateralAsset",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "debtAsset",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "debtToCover",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "liquidatedCollateralAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "liquidator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "receiveAToken",
"type": "bool"
}
],
"name": "LiquidationCall",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "RebalanceStableBorrowRate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "repayer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Repay",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "liquidityRate",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "stableBorrowRate",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "variableBorrowRate",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "liquidityIndex",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "variableBorrowIndex",
"type": "uint256"
}
],
"name": "ReserveDataUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "ReserveUsedAsCollateralDisabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "ReserveUsedAsCollateralEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rateMode",
"type": "uint256"
}
],
"name": "Swap",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "Unpaused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "reserve",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Withdraw",
"type": "event"
},
{
"inputs": [],
"name": "FLASHLOAN_PREMIUM_TOTAL",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "LENDINGPOOL_REVISION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_NUMBER_RESERVES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_STABLE_RATE_BORROW_SIZE_PERCENT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "interestRateMode",
"type": "uint256"
},
{
"internalType": "uint16",
"name": "referralCode",
"type": "uint16"
},
{
"internalType": "address",
"name": "onBehalfOf",
"type": "address"
}
],
"name": "borrow",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "onBehalfOf",
"type": "address"
},
{
"internalType": "uint16",
"name": "referralCode",
"type": "uint16"
}
],
"name": "deposit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "balanceFromBefore",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "balanceToBefore",
"type": "uint256"
}
],
"name": "finalizeTransfer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiverAddress",
"type": "address"
},
{
"internalType": "address[]",
"name": "assets",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "modes",
"type": "uint256[]"
},
{
"internalType": "address",
"name": "onBehalfOf",
"type": "address"
},
{
"internalType": "bytes",
"name": "params",
"type": "bytes"
},
{
"internalType": "uint16",
"name": "referralCode",
"type": "uint16"
}
],
"name": "flashLoan",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getAddressesProvider",
"outputs": [
{
"internalType": "contract ILendingPoolAddressesProvider",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "getConfiguration",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "data",
"type": "uint256"
}
],
"internalType": "struct DataTypes.ReserveConfigurationMap",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "getReserveData",
"outputs": [
{
"components": [
{
"components": [
{
"internalType": "uint256",
"name": "data",
"type": "uint256"
}
],
"internalType": "struct DataTypes.ReserveConfigurationMap",
"name": "configuration",
"type": "tuple"
},
{
"internalType": "uint128",
"name": "liquidityIndex",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "variableBorrowIndex",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "currentLiquidityRate",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "currentVariableBorrowRate",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "currentStableBorrowRate",
"type": "uint128"
},
{
"internalType": "uint40",
"name": "lastUpdateTimestamp",
"type": "uint40"
},
{
"internalType": "address",
"name": "aTokenAddress",
"type": "address"
},
{
"internalType": "address",
"name": "stableDebtTokenAddress",
"type": "address"
},
{
"internalType": "address",
"name": "variableDebtTokenAddress",
"type": "address"
},
{
"internalType": "address",
"name": "interestRateStrategyAddress",
"type": "address"
},
{
"internalType": "uint8",
"name": "id",
"type": "uint8"
}
],
"internalType": "struct DataTypes.ReserveData",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "getReserveNormalizedIncome",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "getReserveNormalizedVariableDebt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getReservesList",
"outputs": [
{
"internalType": "address[]",
"name": "",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "getUserAccountData",
"outputs": [
{
"internalType": "uint256",
"name": "totalCollateralETH",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalDebtETH",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "availableBorrowsETH",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "currentLiquidationThreshold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ltv",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "healthFactor",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "getUserConfiguration",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "data",
"type": "uint256"
}
],
"internalType": "struct DataTypes.UserConfigurationMap",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "address",
"name": "aTokenAddress",
"type": "address"
},
{
"internalType": "address",
"name": "stableDebtAddress",
"type": "address"
},
{
"internalType": "address",
"name": "variableDebtAddress",
"type": "address"
},
{
"internalType": "address",
"name": "interestRateStrategyAddress",
"type": "address"
}
],
"name": "initReserve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ILendingPoolAddressesProvider",
"name": "provider",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "collateralAsset",
"type": "address"
},
{
"internalType": "address",
"name": "debtAsset",
"type": "address"
},
{
"internalType": "address",
"name": "user",
"type": "address"
},
{
"internalType": "uint256",
"name": "debtToCover",
"type": "uint256"
},
{
"internalType": "bool",
"name": "receiveAToken",
"type": "bool"
}
],
"name": "liquidationCall",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "rebalanceStableBorrowRate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "rateMode",
"type": "uint256"
},
{
"internalType": "address",
"name": "onBehalfOf",
"type": "address"
}
],
"name": "repay",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256",
"name": "configuration",
"type": "uint256"
}
],
"name": "setConfiguration",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "val",
"type": "bool"
}
],
"name": "setPause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "address",
"name": "rateStrategyAddress",
"type": "address"
}
],
"name": "setReserveInterestRateStrategyAddress",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "bool",
"name": "useAsCollateral",
"type": "bool"
}
],
"name": "setUserUseReserveAsCollateral",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256",
"name": "rateMode",
"type": "uint256"
}
],
"name": "swapBorrowRateMode",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdraw",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
So before implementing the notifications we should know what exactly our use case is for the notifications.
Here we are working on notifying users whenever their loans are approaching liquidity. If you take a look into the contract here, There is a
getUserAccountData
method which returns data following the schema: { totalCollateralETH
uint256
, totalDebtETH
uint256
, availableBorrowsETH
uint256
, currentLiquidationThreshold
uint256
, ltv
uint256
, healthFactor
uint256 }
. it contains a parameter
healthFactor
parameter, which we monitor until it goes below a defined threshold, and then we send a notification to the subscriber/user in question.Don't worry if that's a bit overwhelming at this point, you'll see how these things work in action!
Now buckle up, and let's get started.
For starting with showrunners and setting it up follow this guide here. // need to add link
First we need to create a folder in
src/showrunners/<
your_channel_name>
In our case the name of the folder we are going to create is
AAVE
as that is the name of the channel we plan to create.
Now that we have created the folder, it's time for creating files in the folder. Start with creating
aaveSettings.json
and put the following contents in it, which are the addresses of the lending pool contract on different networks.{
"aaveLendingPoolDeployedContractPolygonMainnet": "0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf",
"aaveLendingPoolDeployedContractPolygonMumbai": "0x9198F13B08E299d85E096929fA9781A1E3d5d827",
"aaveLendingPoolDeployedContractMainnet": "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9",
"aaveLendingPoolDeployedContractKovan": "0xE0fBa4Fc209b4948668006B2bE61711b7f465bAe"
}
If you need to monitor more EVM networks, we can add more contract address here.
We will create our main file for monitoring our events, Name the files as
<channel_name>Channel.ts,
in our case it will be aaveChannel.ts
.In
aaveChannel.ts
, we can see our example code.Let's review it to understand a bit about what's going on in code!
Here's the entire code:
// @name: Aave Channel
// @version: 1.0
import { Service, Inject } from 'typedi';
import config, { defaultSdkSettings, settings } from '../../config';
import { ethers } from 'ethers';
import epnsHelper from '@epnsproject/backend-sdk-staging';
import aaveSettings from './aaveSettings.json';
import aaveLendingPoolDeployedContractABI from './aave_LendingPool.json';
import { EPNSChannel } from '../../helpers/epnschannel';
import { Logger } from 'winston';
const NETWORK_TO_MONITOR = config.web3MainnetNetwork;
const HEALTH_FACTOR_THRESHOLD = 1.6;
const CUSTOMIZABLE_SETTINGS = {
precision: 3,
};
@Service()
export default class AaveChannel extends EPNSChannel {
constructor(@Inject('logger') public logger: Logger) {
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,
});
}
// To form and write to smart contract
public async sendMessageToContract(simulate) {
const sdk = await this.getSdk();
this.logInfo('sendMessageToContract');
//simulate object settings START
const logicOverride =
typeof simulate == 'object'
? simulate.hasOwnProperty('logicOverride') && simulate.logicOverride.mode
? 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);
}
return true;
}
public async checkHealthFactor(aave, userAddress, sdk: epnsHelper, simulate) {
this.logInfo(`Checking Health Factor`);
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 (!aave) {
aave = await sdk.getContract(
aaveSettings.aaveLendingPoolDeployedContractMainnet,
JSON.stringify(aaveLendingPoolDeployedContractABI),
);
}
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);
}
//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 precision = CUSTOMIZABLE_SETTINGS.precision;
const newHealthFactor = parseFloat(healthFactor).toFixed(precision);
const title = 'Aave Liquidity 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 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,
});
return {
success: true,
data: tx,
};
} else {
this.logInfo(`[Wallet: ${userAddress} is SAFE with Health Factor:: ${healthFactor}`);
return {
success: false,
data: userAddress + ' is not about to get liquidated',
};
}
}
}
Let's break it down into sections so we can have better understanding of code.
👍
👍
The constructor of the class is used to initialise a lot of key variables which are specific to this channel, the key things to note are
name
which represents the name of the channel, url
which is essentially the home page(if any) of the channel, this would the where the users would be redirected to by default, if no CTA is provided for a notification.// The constructor of the class
constructor(@Inject('logger') public logger: Logger) {
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,
});
}
This is a utility function responsible for checking, it essentially takes in a user address, and then fetches details about the position associated with the address.
Among the details fetched for the user is the variable
healthFactor
which tells us how close this user is to liquidation, where a value of 1
means the user is not close to liquidation and a value of 0
means the user is liquidated. We then compare the value gotten back to a cut-off value, and if it falls below the cut-off value, we trigger a notification to the user in question. public async checkHealthFactor(aave, userAddress, sdk: epnsHelper, simulate) {
this.logInfo(`Checking Health Factor`);
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 (!aave) {
aave = await sdk.getContract(
aaveSettings.aaveLendingPoolDeployedContractMainnet,
JSON.stringify(aaveLendingPoolDeployedContractABI),
);
}
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);
}
//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 precision = CUSTOMIZABLE_SETTINGS.precision;
const newHealthFactor = parseFloat(healthFactor).toFixed(precision);
const title = 'Aave Liquidity 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 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,
});
return {
success: true,
data: tx,
};
} else {
this.logInfo(`[Wallet: ${userAddress} is SAFE with Health Factor:: ${healthFactor}`);
return {
success: false,
data: userAddress + ' is not about to get liquidated',
};
}
}
This is the
main
function which is ties everything together and is called by an external party in order to perform the duties of the channel, this function loops over the subscribers of the channel, and calls the checkHealthFactor
method on them, which in turn then fetches public async sendMessageToContract(simulate) {
const sdk = await this.getSdk();
this.logInfo('sendMessageToContract');
//simulate object settings START
const logicOverride =
typeof simulate == 'object'
? simulate.hasOwnProperty('logicOverride') && simulate.logicOverride.mode
? 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);
}
return true;
}
Now to trigger notification manually, we will be needing a
channelRoute.ts
file.Here is the code for our
aaveRoute.ts
file -import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import middlewares from '../../api/middlewares';
import { celebrate, Joi } from 'celebrate';
import aaveChannel from './aaveChannel';
import { Logger } from 'winston';
import { handleResponse } from '../../helpers/utilsHelper';
const route = Router();
export default (app: Router) => {
app.use('/showrunners/aave', route);
/**
* Send Message
* @description Send a notification via the aave showrunner
* @param {boolean} simulate whether to send the actual message or simulate message sending
*/
route.post(
'/send_message',
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 {
const aave = Container.get(aaveChannel);
const data = await aave.sendMessageToContract(req.body.simulate);
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));
}
},
);
};
Now that we have done with routes file, lets have a demo jobs file which will hit our function every 20 minutes.
Setting up a jobs file for a channel is not necessary, it can be customized according to the use case
Code for the aave
Jobs.ts
-