Skip to main content

DeFi based trigger via showrunner scaffold

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.

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!

You can access the code for the channel and get some vibe for how the code is looking !!

Pre-requisites

Here are the list of things you will require to make the channel up into the showrunners:

  • 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',
},
];

Use case for Notification:

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.

Step 1. Setup channel folder

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.

Step 2 . Adding necessary files into the folder

Adding aaveSettings.json

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.

Adding aaveChannel.ts file

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 sendMessageToNode(simulate) {
const sdk = await this.getSdk();
this.logInfo('sendMessageToNode');

//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.:thumbsup::thumbsup:

Constructor Section

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,
});
}

Check HealthFactor section

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',
};
}
}

Send Message To Contract section

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 sendMessageToNode(simulate) {
const sdk = await this.getSdk();
this.logInfo('sendMessageToNode');

//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;
}

Setting up routes for our channel

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.sendMessageToNode(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));
}
},
);
};

Setting up jobs file for channel

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 aaveJobs.ts -

// 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 config from '../../config';
import logger from '../../loaders/logger';

import { Container } from 'typedi';
import schedule from 'node-schedule';

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);

// 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 () {
const aaveChannel = Container.get(AaveChannel);
const taskName = 'Aave users address checks and sendMessageToNode()';

try {
await aaveChannel.sendMessageToNode(false);
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);
}
}
);
};

Setting up keys file for channel

This file would be named aaveKeys.json, and it would contain the private keys of the channel

{
"PRIVATE_KEY": "0x_PRIVATE_KEY"
}

Putting it all together

At the end of the day, you should have a file structure which looks this way:

You can now heat up the server by running docker-compose up and npm run dev and start sending notification.

In the channel file you can also track the block numbers for which last notification by using database , so that next time the jobs hit the engine, it won't repeat sending notification from same block again i.e. repeated notifications

That's all for now :)

If you enjoyed this tutorial, Do join our discord server to meet other dev and builders.