From 81b833f3ccb324aa95a7bb8a4e4f0e59fbdd8be2 Mon Sep 17 00:00:00 2001 From: Amilcar Rey Date: Wed, 20 Dec 2023 04:13:03 -0300 Subject: [PATCH] Adding hatom lending (#1117) * Base structura passing test * Adding querys and some calculations * Calculate base and rewards apy * Get total boosted colateral to calculate the boostedAPY * refactor: :heavy_minus_sign: Delete boosted rewards * refactor: :wastebasket: Clean unnecesary code and packages --- src/adaptors/hatom-lending/index.js | 57 +++++++ src/adaptors/hatom-lending/utils/data.js | 171 ++++++++++++++++++++ src/adaptors/hatom-lending/utils/math.js | 92 +++++++++++ src/adaptors/hatom-lending/utils/queries.js | 78 +++++++++ 4 files changed, 398 insertions(+) create mode 100644 src/adaptors/hatom-lending/index.js create mode 100644 src/adaptors/hatom-lending/utils/data.js create mode 100644 src/adaptors/hatom-lending/utils/math.js create mode 100644 src/adaptors/hatom-lending/utils/queries.js diff --git a/src/adaptors/hatom-lending/index.js b/src/adaptors/hatom-lending/index.js new file mode 100644 index 0000000000..a9b2fdf066 --- /dev/null +++ b/src/adaptors/hatom-lending/index.js @@ -0,0 +1,57 @@ +const BigNumber = require('bignumber.js'); +const utils = require('../utils'); +const { calcRewardsAPY } = require('./utils/math.js'); +const { getMoneyMarkets, getTokenPrices, getExchangeRates, getRewardsBatches, getBoostedRewards, getBoostedColateralMap } = require('./utils/data.js'); + +const MARKETS = [ + { symbol: "USDC", address: "erd1qqqqqqqqqqqqqpgqkrgsvct7hfx7ru30mfzk3uy6pxzxn6jj78ss84aldu" }, + { symbol: 'WBTC', address: "erd1qqqqqqqqqqqqqpgqg47t8v5nwzvdxgf6g5jkxleuplu8y4f678ssfcg5gy" }, + { symbol: "WETH", address: "erd1qqqqqqqqqqqqqpgq8h8upp38fe9p4ny9ecvsett0usu2ep7978ssypgmrs" }, + { symbol: "USDT", address: "erd1qqqqqqqqqqqqqpgqvxn0cl35r74tlw2a8d794v795jrzfxyf78sstg8pjr" } +] + +const apy = async () => { + const [mm, prices, rewards] = await Promise.all([ + getMoneyMarkets(), + getTokenPrices(), + getRewardsBatches(), + ]); + const exchangeRates = getExchangeRates(mm) + + return MARKETS.map(({ symbol }) => { + const currentMM = mm[symbol] + const currentPrice = prices[symbol] + const currentExchangeRate = exchangeRates[symbol] + const currentRewards = rewards[symbol] + + const rewardsAPY = calcRewardsAPY({ + speed: currentRewards.speed, + hTokenExchangeRate: currentExchangeRate, + totalCollateral: currentMM.totalColateral.toString(), + marketPrice: currentPrice, + rewardsToken: currentRewards.rewardsToken, + rewardsTokenPrice: prices[currentRewards.rewardsToken.symbol], + marketDecimals: currentMM.decimals + }) + + const tvlUsd = new BigNumber(currentMM.cash).multipliedBy(currentPrice).dividedBy(`1e${currentMM.decimals}`).toNumber() + const apyBase = mm[symbol].supplyAPY + const apyReward = new BigNumber(rewardsAPY).toNumber() + return { + pool: symbol, + chain: 'MultiversX', + project: 'hatom-lending', + symbol: symbol, + tvlUsd: tvlUsd, + apyBase: apyBase, + apyReward: apyReward, + rewardTokens: [currentRewards.rewardsToken.symbol], + } + }) +} + +module.exports = { + timetravel: false, + apy: apy, + url: 'https://app.hatom.com/lend', +}; \ No newline at end of file diff --git a/src/adaptors/hatom-lending/utils/data.js b/src/adaptors/hatom-lending/utils/data.js new file mode 100644 index 0000000000..8ca79ca16e --- /dev/null +++ b/src/adaptors/hatom-lending/utils/data.js @@ -0,0 +1,171 @@ +const BigNumber = require('bignumber.js'); +const { default: axios } = require('axios'); +const { request } = require('graphql-request'); + +const { queryPrices, queryMoneyMarkets, queryRewards } = require('./queries') +const { calcLiquidStakingExchangeRate, calcSimulateExchangeRate } = require('./math'); + +const API_URL = 'https://mainnet-api.hatom.com/graphql'; + +async function getMoneyMarkets() { + const response = await request(API_URL, queryMoneyMarkets, {}); + return response.queryMoneyMarket.reduce((prev, market) => { + const symbol = market.underlying.symbol; + const value = { + address: market.address, + decimals: market.underlying.decimals, + cash: market.stateHistory[0].cash, + borrows: market.stateHistory[0].borrows, + reserves: market.stateHistory[0].reserves, + rate: market.stateHistory[0].supplyRatePerSecond, + timestamp: market.stateHistory[0].timestamp, + totalSupply: market.stateHistory[0].totalSupply, + borrowRatePerSecond: market.stateHistory[0].borrowRatePerSecond, + supplyAPY: market.stateHistory[0].supplyAPY, + supplyRatePerSecond: market.stateHistory[0].supplyRatePerSecond, + totalColateral: market.totalCollateral, + } + return { + ...prev, + [symbol]: value, + }; + }, {}) +} + +async function getTokenPrices() { + const { queryToken, queryLiquidStaking } = + await request(API_URL, queryPrices, {}); + + const liquidStakingExchangeRate = calcLiquidStakingExchangeRate( + queryLiquidStaking?.[0]?.state?.cashReserve, + queryLiquidStaking?.[0]?.state?.totalShares, + ); + + const queryTokenPopulated = queryToken + .filter( + ({ dailyPriceHistory, symbol }) => + dailyPriceHistory.length > 0 || symbol === 'EGLD', + ) + .map((tokenItem) => { + const filteredToken = tokenItem.dailyPriceHistory; + + const priceEgld = filteredToken?.[0]?.quote?.priceInEgld || '0'; + + let dailyPriceInEgld = '0'; + + if (tokenItem.symbol == 'EGLD') { + dailyPriceInEgld = '1'; + } else if (tokenItem.symbol == 'SEGLD') { + dailyPriceInEgld = new BigNumber(1).multipliedBy(liquidStakingExchangeRate).dividedBy(BigNumber.WAD).toString(); + } else { + dailyPriceInEgld = priceEgld; + } + + const dailyPriceInUSD = filteredToken?.[0]?.price?.price || '0'; + + return { + ...tokenItem, + dailyPriceInEgld, + dailyPriceInUSD, + }; + }); + + const itemEgldInUSD = queryTokenPopulated.find( + ({ symbol }) => symbol === 'EGLD', + ); + const itemEgldInUSDC = queryTokenPopulated.find( + ({ symbol }) => symbol === 'USDC', + ); + + const agregatorEGLDInUSD = new BigNumber( + itemEgldInUSD?.dailyPriceInUSD || '0', + ) + .dividedBy(`1e${18}`) + .toString(); + + const priceHistoryEGLDInUSDC = + new BigNumber(1) + .dividedBy(itemEgldInUSDC?.dailyPriceInEgld || 0) + .toString() || '0'; + + const usdcPriceInEgld = new BigNumber(agregatorEGLDInUSD).isZero() + ? priceHistoryEGLDInUSDC + : agregatorEGLDInUSD; + + const egldInUsdc = usdcPriceInEgld !== '0' ? usdcPriceInEgld : '0'; + + return queryTokenPopulated.reduce( + (prev, { dailyPriceInEgld, dailyPriceInUSD, symbol }) => { + const priceUSD = + !new BigNumber(egldInUsdc).isEqualTo('0') || + !new BigNumber(dailyPriceInEgld).isEqualTo('0') + ? new BigNumber(egldInUsdc) + .multipliedBy(dailyPriceInEgld) + .toString() + : '0'; + + const value = !new BigNumber(dailyPriceInUSD).isZero() + ? new BigNumber(dailyPriceInUSD).dividedBy(`1e${18}`).toString() + : priceUSD; + + return { + ...prev, + [symbol]: value, + }; + }, + {}, + ) +} + +function getExchangeRates(moneyMarkets) { + const symbols = Object.keys(moneyMarkets); + return symbols.reduce( + (prev, symbol) => { + const value = calcSimulateExchangeRate({ + cash: moneyMarkets[symbol].cash || '0', + borrows: + moneyMarkets[symbol].borrows || '0', + reserves: + moneyMarkets[symbol].reserves || '0', + totalSupply: + moneyMarkets[symbol].totalSupply || '0', + rate: + moneyMarkets[symbol].supplyRatePerSecond || '0', + timestamp: + moneyMarkets[symbol].timestamp || new Date().toISOString(), + }) + return { + ...prev, + [symbol]: value + } + }, + {}, + ); +} + +async function getRewardsBatches() { + const response = await request(API_URL, queryRewards, {}); + return response.queryRewardsBatchState.reduce((prev, batch) => { + const symbol = batch.moneyMarket.underlying.symbol; + const value = { + id: batch.id, + speed: batch.speed, + type: batch.type, + endTime: batch.endTime, + fullyDistributed: batch.fullyDistributed, + totalAmount: batch.totalAmount, + rewardsToken: batch.rewardsToken, + } + return { + ...prev, + [symbol]: value, + }; + }, {}) +} + +module.exports = { + getMoneyMarkets, + getTokenPrices, + getExchangeRates, + getRewardsBatches, +} diff --git a/src/adaptors/hatom-lending/utils/math.js b/src/adaptors/hatom-lending/utils/math.js new file mode 100644 index 0000000000..654585ec60 --- /dev/null +++ b/src/adaptors/hatom-lending/utils/math.js @@ -0,0 +1,92 @@ +const BigNumber = require('bignumber.js'); + +const calcRewardsAPY = ({ + speed, + hTokenExchangeRate, + totalCollateral, + marketPrice, + rewardsToken, + rewardsTokenPrice, + marketDecimals, +}) => { + const SECONDS_PER_DAY = new BigNumber(86400); + const DAYS_PER_YEAR = new BigNumber(365); + const secondsInAYear = new BigNumber(SECONDS_PER_DAY).multipliedBy( + DAYS_PER_YEAR, + ); + + const sp = new BigNumber(speed).dividedBy(`1e${18 + rewardsToken?.decimals}`); + + const calc1 = sp + .multipliedBy(rewardsTokenPrice) + .multipliedBy(secondsInAYear); + + const calc2 = new BigNumber(totalCollateral) + .multipliedBy(hTokenExchangeRate) + .dividedBy(`1e18`) + .dividedBy(`1e${marketDecimals}`) + .multipliedBy(marketPrice); + + if (calc2.isEqualTo(0)) { + return '0'; + } + + const result = calc1.dividedBy(calc2).multipliedBy(100); + + return result.isNaN() ? '0' : result.toString(); +}; + +const calcSimulateExchangeRate = ({ + cash, + borrows, + reserves, + totalSupply, + rate, + timestamp, +}) => { + return new BigNumber( + calcExchangeRate({ cash, borrows, reserves, totalSupply }), + ) + .multipliedBy(calcRateSimulate(rate, timestamp)) + .toString(); +}; + +const calcRateSimulate = (rate, timestamp) => { + const currentDate = new Date(); + const currentDateInSeconds = currentDate.getTime() / 1000; + const timestampInSeconds = new Date(timestamp).getTime() / 1000; + + return new BigNumber(rate) + .multipliedBy(currentDateInSeconds - timestampInSeconds) + .dividedBy(`1e18`) + .plus(1) + .toString(); +}; + +const calcExchangeRate = ({ cash, borrows, reserves, totalSupply }) => { + const value = new BigNumber(cash) + .plus(borrows) + .minus(reserves) + .times(1e18) + .div(totalSupply) + .toFixed(0); + + return new BigNumber(value).isNaN() ? '0' : value; +}; + +const calcLiquidStakingExchangeRate = (cashReserve, totalShares) => { + if (totalShares === '0') { + return INITIAL_EXCHANGE_RATE; + } + + return new BigNumber(cashReserve) + .multipliedBy(`1e18`) + .dividedBy(totalShares) + .toFixed(0, BigNumber.ROUND_DOWN); +}; + +module.exports = { + calcRewardsAPY, + calcLiquidStakingExchangeRate, + calcSimulateExchangeRate, +}; \ No newline at end of file diff --git a/src/adaptors/hatom-lending/utils/queries.js b/src/adaptors/hatom-lending/utils/queries.js new file mode 100644 index 0000000000..f3fa2a6db1 --- /dev/null +++ b/src/adaptors/hatom-lending/utils/queries.js @@ -0,0 +1,78 @@ +const queryPrices = ` + query QueryTokenPrices { + queryLiquidStaking { + state { + cashReserve + totalShares + } + } + queryToken { + id + symbol + dailyPriceHistory(first: 1, order: { desc: day }) { + quote { + priceInEgld + timestamp + } + price { + price + timestamp + } + } + } + } +`; + +const queryMoneyMarkets = ` +query QueryMoneyMarket { + queryMoneyMarket { + address + totalCollateral + underlying { + symbol + name + decimals + id + } + stateHistory(first:1, order:{ + desc: timestamp + }) { + cash + borrows + reserves + timestamp + supplyAPY + supplyRatePerSecond + borrowAPY + borrowRatePerSecond + totalSupply + } + } + }` + +const queryRewards = ` +query QueryRewards { + queryRewardsBatchState { + id + speed + type + endTime + fullyDistributed + totalAmount + moneyMarket{ + address + underlying{ + symbol + } + } + rewardsToken{ + symbol + decimals + } + } +}` +module.exports = { + queryPrices, + queryMoneyMarkets, + queryRewards +}; \ No newline at end of file