diff --git a/src/adaptors/lucky-peach/index.js b/src/adaptors/lucky-peach/index.js new file mode 100755 index 0000000000..0eac01fdf7 --- /dev/null +++ b/src/adaptors/lucky-peach/index.js @@ -0,0 +1,238 @@ +const superagent = require('superagent'); +const { request, gql } = require('graphql-request'); +const sdk = require('@defillama/sdk'); + +const utils = require('../utils'); +const { aTokenAbi } = require('../aave-v3/abi'); +const poolAbi = require('../aave-v3/poolAbi'); + +const SECONDS_PER_YEAR = 31536000; + +const chainUrlParam = { + hemi: ['proto_hemi_v3'], +}; + +const getPrices = async (addresses) => { + const prices = ( + await superagent.get( + `https://coins.llama.fi/prices/current/${addresses + .join(',') + .toLowerCase()}` + ) + ).body.coins; + + const pricesBySymbol = Object.entries(prices).reduce( + (acc, [name, price]) => ({ + ...acc, + [price.symbol.toLowerCase()]: price.price, + }), + {} + ); + + const pricesByAddress = Object.entries(prices).reduce( + (acc, [name, price]) => ({ + ...acc, + [name.split(':')[1]]: price.price, + }), + {} + ); + + return { pricesByAddress, pricesBySymbol }; +}; + +const API_URLS = { + hemi: ['https://app.sentio.xyz//api/v1/graphql/luckypeaches/lending?api-key=9J0nGOsoMO9In1dfdW88rN6wbNZYCV9qU'], +}; + +const query = gql` + query ReservesQuery { + reserves(where: { name_not: "" }) { + name + borrowingEnabled + pool { + pool + } + aToken { + id + rewards { + id + emissionsPerSecond + rewardToken + rewardTokenDecimals + rewardTokenSymbol + distributionEnd + } + underlyingAssetAddress + underlyingAssetDecimals + } + vToken { + rewards { + emissionsPerSecond + rewardToken + rewardTokenDecimals + rewardTokenSymbol + distributionEnd + } + } + symbol + liquidityRate + variableBorrowRate + baseLTVasCollateral + isFrozen + } + } +`; + +const apy = async () => { + let data = await Promise.all( + Object.entries(API_URLS).flatMap(([chain, urls]) => { + return urls.map(async (url) => [ + chain, + (await request(url, query)).reserves, + ]); + }) + ); + + data = data.map(([chain, reserves]) => [ + chain, + reserves.filter((p) => !p.isFrozen), + ]); + + const totalSupply = await Promise.all( + data.map(async ([chain, reserves]) => + ( + await sdk.api.abi.multiCall({ + chain: chain, + abi: aTokenAbi.find(({ name }) => name === 'totalSupply'), + calls: reserves.map((reserve) => ({ + target: reserve.aToken.id, + })), + }) + ).output.map(({ output }) => output) + ) + ); + + const underlyingBalances = await Promise.all( + data.map(async ([chain, reserves]) => + ( + await sdk.api.abi.multiCall({ + chain: chain, + abi: aTokenAbi.find(({ name }) => name === 'balanceOf'), + calls: reserves.map((reserve, i) => ({ + target: reserve.aToken.underlyingAssetAddress, + params: [reserve.aToken.id], + })), + }) + ).output.map(({ output }) => output) + ) + ); + + const underlyingTokens = data.map(([chain, reserves]) => + reserves.map((pool) => `${chain}:${pool.aToken.underlyingAssetAddress}`) + ); + + const rewardTokens = data.map(([chain, reserves]) => + reserves.map((pool) => + pool.aToken.rewards.map((rew) => `${chain}:${rew.rewardToken}`) + ) + ); + + const allTokens = underlyingTokens.flat().concat(rewardTokens.flat(Infinity)); + const pricesByAddress = {}; + const pricesBySymbol = {}; + + for (let i = 0; i < allTokens.length; i += 50) { + const chunk = allTokens.slice(i, i + 50); + const { + pricesByAddress: chunkPricesByAddress, + pricesBySymbol: chunkPricesBySymbol, + } = await getPrices(chunk); + Object.assign(pricesByAddress, chunkPricesByAddress); + Object.assign(pricesBySymbol, chunkPricesBySymbol); + } + + const pools = data.map(([chain, markets], i) => { + const chainPools = markets.map((pool, idx) => { + const supply = totalSupply[i][idx]; + const currentSupply = underlyingBalances[i][idx]; + const totalSupplyUsd = + (supply / 10 ** pool.aToken.underlyingAssetDecimals) * + (pricesByAddress[pool.aToken.underlyingAssetAddress] || + pricesBySymbol[pool.symbol]); + const tvlUsd = + (currentSupply / 10 ** pool.aToken.underlyingAssetDecimals) * + (pricesByAddress[pool.aToken.underlyingAssetAddress] || + pricesBySymbol[pool.symbol]); + const { rewards } = pool.aToken; + + const rewardPerYear = rewards.reduce( + (acc, rew) => + acc + + (rew.emissionsPerSecond / 10 ** rew.rewardTokenDecimals) * + SECONDS_PER_YEAR * + (pricesByAddress[rew.rewardToken] || + pricesBySymbol[rew.rewardTokenSymbol] || + 0), + 0 + ); + + const { rewards: rewardsBorrow } = pool.vToken; + const rewardPerYearBorrow = rewardsBorrow.reduce( + (acc, rew) => + acc + + (rew.emissionsPerSecond / 10 ** rew.rewardTokenDecimals) * + SECONDS_PER_YEAR * + (pricesByAddress[rew.rewardToken] || + pricesBySymbol[rew.rewardTokenSymbol] || + 0), + 0 + ); + + let totalBorrowUsd = totalSupplyUsd - tvlUsd; + totalBorrowUsd = totalBorrowUsd < 0 ? 0 : totalBorrowUsd; + + const supplyRewardEnd = pool.aToken.rewards[0]?.distributionEnd; + const borrowRewardEnd = pool.vToken.rewards[0]?.distributionEnd; + + return { + pool: `${pool.aToken.id}-${chain}`.toLowerCase(), + chain: utils.formatChain(chain), + project: 'lucky-peach', + symbol: pool.symbol, + tvlUsd, + apyBase: (pool.liquidityRate / 10 ** 27) * 100, + apyReward: + supplyRewardEnd * 1000 > new Date() + ? (rewardPerYear / totalSupplyUsd) * 100 + : null, + rewardTokens: + supplyRewardEnd * 1000 > new Date() + ? rewards.map((rew) => rew.rewardToken) + : null, + underlyingTokens: [pool.aToken.underlyingAssetAddress], + totalSupplyUsd, + totalBorrowUsd, + apyBaseBorrow: Number(pool.variableBorrowRate) / 1e25, + apyRewardBorrow: + borrowRewardEnd * 1000 > new Date() + ? (rewardPerYearBorrow / totalBorrowUsd) * 100 + : null, + ltv: Number(pool.baseLTVasCollateral) / 10000, + url: `https://luckypeach.xyz/reserve-overview/?underlyingAsset=${ + pool.aToken.underlyingAssetAddress + }&marketName=${ + chain === 'ethereum' + ? mainnnet_pools[pool.pool.pool] + : chainUrlParam[chain][0] + }&utm_source=defillama&utm_medium=listing&utm_campaign=external`, + borrowable: pool.borrowingEnabled, + }; + }); + + return chainPools; + }); + + return pools.flat().filter((p) => !!p.tvlUsd); +}; + +module.exports = { timetravel: false, apy };