From a6050bec0d2c6c524d2a1d09398f39449bc2e070 Mon Sep 17 00:00:00 2001 From: Danil Shaymurzin Date: Thu, 26 Dec 2024 05:00:45 +0500 Subject: [PATCH 1/6] feat: add rewardApy for EVAA Protocol --- src/adaptors/evaa-protocol/index.js | 129 ++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/adaptors/evaa-protocol/index.js b/src/adaptors/evaa-protocol/index.js index 376234e24f..cc20b0463a 100644 --- a/src/adaptors/evaa-protocol/index.js +++ b/src/adaptors/evaa-protocol/index.js @@ -336,6 +336,111 @@ async function getPrices(endpoint = "api.stardust-mainnet.iotaledger.net") { } } +function isLeapYear(year) { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} + +async function getDistributions(endpoint = 'evaa.space') { + try { + let result = await fetch(`https://${endpoint}/query/distributions/list`, { + headers: { accept: 'application/json' }, + }); + let resData = await result.json(); + return resData; + } catch (error) { + console.error(error); + return undefined; + } +} + +function calculateRewardApy(distributionsResp, pool, data, prices) { + try { + if ( + !distributionsResp?.distributions || + distributionsResp.distributions.length === 0 + ) { + console.log('Invalid distributions data:', distributionsResp); + return undefined; + } + + const currentCampaign = distributionsResp?.distributions.find( + (campaign) => campaign.started && !campaign.expired + ); + + if (!currentCampaign) return 0; + + const currentSeasons = currentCampaign?.seasons + ?.map((season) => { + if (!season.started || season.expired) { + return; + } + + const rewardingAssetId = BigInt(season?.rewarding_asset_id ?? 0); + const rewardsAssetId = BigInt(season?.rewards_asset_id ?? 0); + + const rewardingAssetData = data.assetsData.get(rewardingAssetId); + const rewardsAssetData = data.assetsData.get(rewardsAssetId); + const rewardPool = season?.pool; + if (!rewardingAssetData || !rewardsAssetData || rewardPool !== pool) { + return; + } + + const rewardType = season?.reward_type; + + const rewardingAssetConfig = data.assetsConfig.get(rewardingAssetId); + const rewardsAssetConfig = data.assetsConfig.get(rewardsAssetId); + + const rewardingScaleFactor = + 10 ** Number(rewardingAssetConfig.decimals); + const rewardsScaleFactor = 10 ** Number(rewardsAssetConfig.decimals); + + const rewardingPriceData = prices.dict.get(rewardingAssetId); + const rewardsPriceData = prices.dict.get(rewardsAssetId); + + const rewardingPrice = + Number(rewardingPriceData) / Number(priceScaleFactor); + const rewardsPrice = + Number(rewardsPriceData) / Number(priceScaleFactor); + + const totalAmount = + rewardType === 'borrow' + ? rewardingAssetData.totalBorrow + : rewardingAssetData.totalSupply; + const rate = rewardingScaleFactor / Number(totalAmount); + const rewardForUnit = + rate * + (((Number(season?.rewards_amount) / rewardsScaleFactor) * + (rewardsPrice ?? 0)) / + rewardingPrice) * + rewardingScaleFactor; + const seasonStart = new Date(season?.campaign_start ?? 0); + const seasonEnd = new Date(season?.campaign_end ?? 0); + const totalSecsInCurrentSeason = + (Number(seasonEnd) - Number(seasonStart)) / 1000; + const isLeap = isLeapYear(new Date().getFullYear()); + const totalSecsInYear = (isLeap ? 366 : 365) * 24 * 60 * 60; + const apy = + ((rewardForUnit * totalSecsInYear) / + totalSecsInCurrentSeason / + rewardingScaleFactor) * + 100; + + return { + apy, + rewardType, + rewardingAssetId, + rewardsAssetId, + pool: rewardPool, + }; + }) + ?.filter(Boolean); + + return currentSeasons || []; + } catch (error) { + console.error(error); + return undefined; + } +} // ignore pools with TVL below the threshold const MIN_TVL_USD = 100000; @@ -350,6 +455,7 @@ function calculatePresentValue(index, principalValue) { const getApy = async () => { console.log("Requesting prices") let prices = await getPrices(); + let distributions = await getDistributions(); const client = new TonClient({ endpoint: "https://toncenter.com/api/v2/jsonRPC" }); @@ -365,6 +471,9 @@ const getApy = async () => { console.log(e); } }); + + const rewardApys = calculateRewardApy(distributions, 'main', data,prices); + return Object.entries(assets).map(([tokenSymbol, asset]) => { const { assetId, token } = asset; console.log("Process symbol", tokenSymbol, asset, assetId, token) @@ -398,6 +507,24 @@ const getApy = async () => { console.log(tokenSymbol, "supplyApy", supplyApy * 100); console.log(tokenSymbol, "borrowApy", borrowApy * 100); + const apyRewardData = rewardApys.find( + (rewardApy) => + rewardApy.rewardingAssetId == assetId && + rewardApy.rewardType == 'supply' + ); + + const apyReward = apyRewardData ? apyRewardData.apy : undefined; + + const apyRewardBorrowData = rewardApys.find( + (rewardApy) => + rewardApy.rewardingAssetId == assetId && + rewardApy.rewardType == 'borrow' + ); + + const apyRewardBorrow = apyRewardBorrowData + ? apyRewardBorrowData.apy + : undefined; + return { pool: `evaa-${assetId}-ton`.toLowerCase(), chain: 'Ton', @@ -405,6 +532,8 @@ const getApy = async () => { symbol: tokenSymbol, tvlUsd: totalSupplyUsd - totalBorrowUsd, apyBase: supplyApy * 100, + apyReward, + // apyRewardBorrow, underlyingTokens: [token], url: `https://app.evaa.finance/token/${tokenSymbol}`, totalSupplyUsd: totalSupplyUsd, From 40d24032913bd7b8d7d29fd777b42ffa3c7fd273 Mon Sep 17 00:00:00 2001 From: Danil Shaymurzin Date: Mon, 30 Dec 2024 19:01:17 +0500 Subject: [PATCH 2/6] feat: add rewardTokens --- src/adaptors/evaa-protocol/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/adaptors/evaa-protocol/index.js b/src/adaptors/evaa-protocol/index.js index cc20b0463a..34cb0f5a51 100644 --- a/src/adaptors/evaa-protocol/index.js +++ b/src/adaptors/evaa-protocol/index.js @@ -514,6 +514,7 @@ const getApy = async () => { ); const apyReward = apyRewardData ? apyRewardData.apy : undefined; + const rewardTokens = apyRewardData ? [apyRewardData.rewardsAssetId] : undefined; const apyRewardBorrowData = rewardApys.find( (rewardApy) => @@ -533,6 +534,7 @@ const getApy = async () => { tvlUsd: totalSupplyUsd - totalBorrowUsd, apyBase: supplyApy * 100, apyReward, + rewardTokens, // apyRewardBorrow, underlyingTokens: [token], url: `https://app.evaa.finance/token/${tokenSymbol}`, From 5edd5fd020c1304b62e3c10f03cac738a099069c Mon Sep 17 00:00:00 2001 From: Danil Shaymurzin Date: Mon, 30 Dec 2024 19:17:49 +0500 Subject: [PATCH 3/6] feat: merkleProof oracle getPrices system --- src/adaptors/evaa-protocol/getPrices.js | 376 ++++++++++++++++++++++++ src/adaptors/evaa-protocol/index.js | 33 +-- 2 files changed, 378 insertions(+), 31 deletions(-) create mode 100644 src/adaptors/evaa-protocol/getPrices.js diff --git a/src/adaptors/evaa-protocol/getPrices.js b/src/adaptors/evaa-protocol/getPrices.js new file mode 100644 index 0000000000..ec5d0666a5 --- /dev/null +++ b/src/adaptors/evaa-protocol/getPrices.js @@ -0,0 +1,376 @@ +const fetch = require('node-fetch'); +const { Cell, Slice, Dictionary, beginCell } = require('@ton/core'); +const { signVerify } = require('@ton/crypto'); + +const ORACLES = [ + { + id: 0, + address: + '0xd3a8c0b9fd44fd25a49289c631e3ac45689281f2f8cf0744400b4c65bed38e5d', + pubkey: Buffer.from( + 'b404f4a2ebb62f2623b370c89189748a0276c071965b1646b996407f10d72eb9', + 'hex' + ), + }, + { + id: 1, + address: + '0x2c21cabdaa89739de16bde7bc44e86401fac334a3c7e55305fe5e7563043e191', + pubkey: Buffer.from( + '9ad115087520d91b6b45d6a8521eb4616ee6914af07fabdc2e9d1826dbb17078', + 'hex' + ), + }, + { + id: 2, + address: + '0x2eb258ce7b5d02466ab8a178ad8b0ba6ffa7b58ef21de3dc3b6dd359a1e16af0', + pubkey: Buffer.from( + 'e503e02e8a9226b34e7c9deb463cbf7f19bce589362eb448a69a8ee7b2fca631', + 'hex' + ), + }, + { + id: 3, + address: + '0xf9a0769954b4430bca95149fb3d876deb7799d8f74852e0ad4ccc5778ce68b52', + pubkey: Buffer.from( + '9cbf8374cf1f2cf17110134871d580198416e101683f4a61f54cf2a3e4e32070', + 'hex' + ), + }, +]; + +const TTL_ORACLE_DATA_SEC = 120; +const MINIMAL_ORACLES = 3; + +function verifyPricesTimestamp(priceData) { + const timestamp = Date.now() / 1000; + const pricesTime = priceData.timestamp; + return timestamp - pricesTime < TTL_ORACLE_DATA_SEC; +} + +function verifyPricesSign(priceData) { + const message = priceData.dataCell.refs[0].hash(); + const signature = priceData.signature; + const publicKey = priceData.pubkey; + + return signVerify(message, signature, publicKey); +} + +function getMedianPrice(pricesData, assetId) { + try { + const usingPrices = pricesData.filter((x) => x.dict.has(assetId)); + const sorted = usingPrices + .map((x) => x.dict.get(assetId)) + .sort((a, b) => Number(a) - Number(b)); + + if (sorted.length === 0) { + return null; + } + + const mid = Math.floor(sorted.length / 2); + if (sorted.length % 2 === 0) { + return (sorted[mid - 1] + sorted[mid]) / 2n; + } else { + return sorted[mid]; + } + } catch { + return null; + } +} + +function packAssetsData(assetsData) { + if (assetsData.length === 0) { + throw new Error('No assets data to pack'); + } + return assetsData.reduceRight( + (acc, { assetId, medianPrice }) => + beginCell() + .storeUint(assetId, 256) + .storeCoins(medianPrice) + .storeMaybeRef(acc) + .endCell(), + null + ); +} + +function packPrices(assetsDataCell, oraclesDataCell) { + return beginCell() + .storeRef(assetsDataCell) + .storeRef(oraclesDataCell) + .endCell(); +} + +function readUnaryLength(slice) { + let res = 0; + while (slice.loadBit()) { + res++; + } + return res; +} + +function doGenerateMerkleProof(prefix, slice, n, keys) { + // Reading label + const originalCell = slice.asCell(); + + if (keys.length == 0) { + // no keys to prove, prune the whole subdict + return convertToPrunedBranch(originalCell); + } + + let lb0 = slice.loadBit() ? 1 : 0; + let prefixLength = 0; + let pp = prefix; + + if (lb0 === 0) { + // Short label detected + + // Read + prefixLength = readUnaryLength(slice); + + // Read prefix + for (let i = 0; i < prefixLength; i++) { + pp += slice.loadBit() ? '1' : '0'; + } + } else { + let lb1 = slice.loadBit() ? 1 : 0; + if (lb1 === 0) { + // Long label detected + prefixLength = slice.loadUint(Math.ceil(Math.log2(n + 1))); + for (let i = 0; i < prefixLength; i++) { + pp += slice.loadBit() ? '1' : '0'; + } + } else { + // Same label detected + let bit = slice.loadBit() ? '1' : '0'; + prefixLength = slice.loadUint(Math.ceil(Math.log2(n + 1))); + for (let i = 0; i < prefixLength; i++) { + pp += bit; + } + } + } + + if (n - prefixLength === 0) { + return originalCell; + } else { + let sl = originalCell.beginParse(); + let left = sl.loadRef(); + let right = sl.loadRef(); + // NOTE: Left and right branches are implicitly contain prefixes '0' and '1' + if (!left.isExotic) { + const leftKeys = keys.filter((key) => { + return pp + '0' === key.slice(0, pp.length + 1); + }); + left = doGenerateMerkleProof( + pp + '0', + left.beginParse(), + n - prefixLength - 1, + leftKeys + ); + } + if (!right.isExotic) { + const rightKeys = keys.filter((key) => { + return pp + '1' === key.slice(0, pp.length + 1); + }); + right = doGenerateMerkleProof( + pp + '1', + right.beginParse(), + n - prefixLength - 1, + rightKeys + ); + } + + return beginCell().storeSlice(sl).storeRef(left).storeRef(right).endCell(); + } +} + +function generateMerkleProofDirect(dict, keys, keyObject) { + keys.forEach((key) => { + if (!dict.has(key)) { + throw new Error( + `Trying to generate merkle proof for a missing key "${key}"` + ); + } + }); + const s = beginCell().storeDictDirect(dict).asSlice(); + return doGenerateMerkleProof( + '', + s, + keyObject.bits, + keys.map((key) => + keyObject.serialize(key).toString(2).padStart(keyObject.bits, '0') + ) + ); +} + +function endExoticCell(b) { + let c = b.endCell(); + return new Cell({ exotic: true, bits: c.bits, refs: c.refs }); +} + +function convertToMerkleProof(c) { + return endExoticCell( + beginCell() + .storeUint(3, 8) + .storeBuffer(c.hash(0)) + .storeUint(c.depth(0), 16) + .storeRef(c) + ); +} + +function createOracleDataProof(oracle, data, signature, assets) { + let prunedDict = generateMerkleProofDirect( + data.prices, + assets, + Dictionary.Keys.BigUint(256) + ); + let prunedData = beginCell() + .storeUint(data.timestamp, 32) + .storeMaybeRef(prunedDict) + .endCell(); + let merkleProof = convertToMerkleProof(prunedData); + let oracleDataProof = beginCell() + .storeUint(oracle.id, 32) + .storeRef(merkleProof) + .storeBuffer(signature) + .asSlice(); + return oracleDataProof; +} + +function packOraclesData(oraclesData, assets) { + if (oraclesData.length == 0) { + throw new Error('no oracles data to pack'); + } + let proofs = oraclesData + .sort((d1, d2) => d1.oracle.id - d2.oracle.id) + .map(({ oracle, data, signature }) => + createOracleDataProof(oracle, data, signature, assets) + ); + return proofs.reduceRight( + (acc, val) => beginCell().storeSlice(val).storeMaybeRef(acc).endCell(), + null + ); +} + +async function getPrices(endpoint = 'api.stardust-mainnet.iotaledger.net') { + try { + const prices = await Promise.all( + ORACLES.map(async (oracle) => { + try { + const outputResponse = await fetch( + `https://${endpoint}/api/indexer/v1/outputs/nft/${oracle.address}`, + { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(5000), + } + ); + const outputData = await outputResponse.json(); + const priceResponse = await fetch( + `https://${endpoint}/api/core/v2/outputs/${outputData.items[0]}`, + { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(5000), + } + ); + const priceData = await priceResponse.json(); + + const data = JSON.parse( + decodeURIComponent( + priceData.output.features[0].data + .replace('0x', '') + .replace(/[0-9a-f]{2}/g, '%$&') + ) + ); + + const pricesCell = Cell.fromBoc( + Buffer.from(data.packedPrices, 'hex') + )[0]; + const signature = Buffer.from(data.signature, 'hex'); + const publicKey = Buffer.from(data.publicKey, 'hex'); + const timestamp = Number(data.timestamp); + + return { + dict: pricesCell + .beginParse() + .loadRef() + .beginParse() + .loadDictDirect( + Dictionary.Keys.BigUint(256), + Dictionary.Values.BigVarUint(4) + ), + dataCell: beginCell() + .storeRef(pricesCell) + .storeBuffer(signature) + .endCell(), + oracleId: oracle.id, + signature, + pubkey: publicKey, + timestamp, + }; + } catch (error) { + console.error( + `Error fetching prices from oracle ${oracle.id}:`, + error + ); + return null; + } + }) + ); + + const validPrices = prices.filter( + (price) => + price && verifyPricesTimestamp(price) && verifyPricesSign(price) + ); + + if (validPrices.length < MINIMAL_ORACLES) { + throw new Error('Not enough valid price data'); + } + + const sortedByTimestamp = validPrices + .slice() + .sort((a, b) => b.timestamp - a.timestamp); + const newerPrices = sortedByTimestamp + .slice(0, MINIMAL_ORACLES) + .sort((a, b) => a.oracleId - b.oracleId); + + const allAssetIds = new Set( + newerPrices.flatMap((p) => Array.from(p.dict.keys())) + ); + + const medianData = Array.from(allAssetIds) + .map((assetId) => ({ + assetId, + medianPrice: getMedianPrice(newerPrices, assetId), + })) + .filter((x) => x.medianPrice !== null); + + const packedMedianData = packAssetsData(medianData); + + const oraclesData = newerPrices.map((x) => ({ + oracle: { id: x.oracleId, pubkey: x.pubkey }, + data: { timestamp: x.timestamp, prices: x.dict }, + signature: x.signature, + })); + + const packedOracleData = packOraclesData( + oraclesData, + medianData.map((x) => x.assetId) + ); + + const dict = Dictionary.empty(); + for (const { assetId, medianPrice } of medianData) { + dict.set(assetId, medianPrice); + } + + return { + dict, + dataCell: packPrices(packedMedianData, packedOracleData), + }; + } catch (error) { + console.error('Error processing prices:', error); + return undefined; + } +} + +module.exports = getPrices; diff --git a/src/adaptors/evaa-protocol/index.js b/src/adaptors/evaa-protocol/index.js index 34cb0f5a51..ea6c74f60d 100644 --- a/src/adaptors/evaa-protocol/index.js +++ b/src/adaptors/evaa-protocol/index.js @@ -3,8 +3,9 @@ const utils = require('../utils'); const fetch = require('node-fetch') const { TonClient } = require("@ton/ton"); const { Address, Cell, Slice, Dictionary, beginCell } = require("@ton/core"); +const { signVerify } = require('@ton/crypto'); const crypto = require("crypto"); -const NFT_ID = '0xfb9874544d76ca49c5db9cc3e5121e4c018bc8a2fb2bfe8f2a38c5b9963492f5'; +const getPrices = require('./getPrices'); function sha256Hash(input) { const hash = crypto.createHash("sha256"); @@ -306,36 +307,6 @@ function calculateCurrentRates(assetConfig, assetData) { }; } -async function getPrices(endpoint = "api.stardust-mainnet.iotaledger.net") { - try { - let result = await fetch(`https://${endpoint}/api/indexer/v1/outputs/nft/${NFT_ID}`, { - headers: { accept: 'application/json' }, - }); - let outputId = await result.json(); - - result = await fetch(`https://${endpoint}/api/core/v2/outputs/${outputId.items[0]}`, { - headers: { accept: 'application/json' }, - }); - - let resData = await result.json(); - - const data = JSON.parse( - decodeURIComponent(resData.output.features[0].data.replace('0x', '').replace(/[0-9a-f]{2}/g, '%$&')), - ); - - const pricesCell = Cell.fromBoc(Buffer.from(data['packedPrices'], 'hex'))[0]; - const signature = Buffer.from(data['signature'], 'hex'); - - return { - dict: pricesCell.beginParse().loadDictDirect(Dictionary.Keys.BigUint(256), Dictionary.Values.BigUint(64)), - dataCell: beginCell().storeRef(pricesCell).storeBuffer(signature).endCell(), - }; - } catch (error) { - console.error(error); - return undefined; - } -} - function isLeapYear(year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } From 41c3ed5b07b009d1b6b774f455c7047d8dee260c Mon Sep 17 00:00:00 2001 From: Danil Shaymurzin Date: Mon, 30 Dec 2024 19:43:16 +0500 Subject: [PATCH 4/6] feat: rewardApy & rewardTokens prop --- src/adaptors/evaa-protocol/index.js | 111 +-------------- src/adaptors/evaa-protocol/rewardApy.js | 177 ++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 108 deletions(-) create mode 100644 src/adaptors/evaa-protocol/rewardApy.js diff --git a/src/adaptors/evaa-protocol/index.js b/src/adaptors/evaa-protocol/index.js index ea6c74f60d..500139e3a8 100644 --- a/src/adaptors/evaa-protocol/index.js +++ b/src/adaptors/evaa-protocol/index.js @@ -6,6 +6,7 @@ const { Address, Cell, Slice, Dictionary, beginCell } = require("@ton/core"); const { signVerify } = require('@ton/crypto'); const crypto = require("crypto"); const getPrices = require('./getPrices'); +const { getDistributions, calculateRewardApy } = require('./rewardApy'); function sha256Hash(input) { const hash = crypto.createHash("sha256"); @@ -307,112 +308,6 @@ function calculateCurrentRates(assetConfig, assetData) { }; } -function isLeapYear(year) { - return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; -} - -async function getDistributions(endpoint = 'evaa.space') { - try { - let result = await fetch(`https://${endpoint}/query/distributions/list`, { - headers: { accept: 'application/json' }, - }); - let resData = await result.json(); - return resData; - } catch (error) { - console.error(error); - return undefined; - } -} - -function calculateRewardApy(distributionsResp, pool, data, prices) { - try { - if ( - !distributionsResp?.distributions || - distributionsResp.distributions.length === 0 - ) { - console.log('Invalid distributions data:', distributionsResp); - return undefined; - } - - const currentCampaign = distributionsResp?.distributions.find( - (campaign) => campaign.started && !campaign.expired - ); - - if (!currentCampaign) return 0; - - const currentSeasons = currentCampaign?.seasons - ?.map((season) => { - if (!season.started || season.expired) { - return; - } - - const rewardingAssetId = BigInt(season?.rewarding_asset_id ?? 0); - const rewardsAssetId = BigInt(season?.rewards_asset_id ?? 0); - - const rewardingAssetData = data.assetsData.get(rewardingAssetId); - const rewardsAssetData = data.assetsData.get(rewardsAssetId); - const rewardPool = season?.pool; - if (!rewardingAssetData || !rewardsAssetData || rewardPool !== pool) { - return; - } - - const rewardType = season?.reward_type; - - const rewardingAssetConfig = data.assetsConfig.get(rewardingAssetId); - const rewardsAssetConfig = data.assetsConfig.get(rewardsAssetId); - - const rewardingScaleFactor = - 10 ** Number(rewardingAssetConfig.decimals); - const rewardsScaleFactor = 10 ** Number(rewardsAssetConfig.decimals); - - const rewardingPriceData = prices.dict.get(rewardingAssetId); - const rewardsPriceData = prices.dict.get(rewardsAssetId); - - const rewardingPrice = - Number(rewardingPriceData) / Number(priceScaleFactor); - const rewardsPrice = - Number(rewardsPriceData) / Number(priceScaleFactor); - - const totalAmount = - rewardType === 'borrow' - ? rewardingAssetData.totalBorrow - : rewardingAssetData.totalSupply; - const rate = rewardingScaleFactor / Number(totalAmount); - const rewardForUnit = - rate * - (((Number(season?.rewards_amount) / rewardsScaleFactor) * - (rewardsPrice ?? 0)) / - rewardingPrice) * - rewardingScaleFactor; - const seasonStart = new Date(season?.campaign_start ?? 0); - const seasonEnd = new Date(season?.campaign_end ?? 0); - const totalSecsInCurrentSeason = - (Number(seasonEnd) - Number(seasonStart)) / 1000; - const isLeap = isLeapYear(new Date().getFullYear()); - const totalSecsInYear = (isLeap ? 366 : 365) * 24 * 60 * 60; - const apy = - ((rewardForUnit * totalSecsInYear) / - totalSecsInCurrentSeason / - rewardingScaleFactor) * - 100; - - return { - apy, - rewardType, - rewardingAssetId, - rewardsAssetId, - pool: rewardPool, - }; - }) - ?.filter(Boolean); - - return currentSeasons || []; - } catch (error) { - console.error(error); - return undefined; - } -} - // ignore pools with TVL below the threshold const MIN_TVL_USD = 100000; @@ -481,7 +376,7 @@ const getApy = async () => { const apyRewardData = rewardApys.find( (rewardApy) => rewardApy.rewardingAssetId == assetId && - rewardApy.rewardType == 'supply' + rewardApy.rewardType.toLowerCase() === 'supply' ); const apyReward = apyRewardData ? apyRewardData.apy : undefined; @@ -490,7 +385,7 @@ const getApy = async () => { const apyRewardBorrowData = rewardApys.find( (rewardApy) => rewardApy.rewardingAssetId == assetId && - rewardApy.rewardType == 'borrow' + rewardApy.rewardType.toLowerCase() === 'borrow' ); const apyRewardBorrow = apyRewardBorrowData diff --git a/src/adaptors/evaa-protocol/rewardApy.js b/src/adaptors/evaa-protocol/rewardApy.js new file mode 100644 index 0000000000..a68ccdb5ec --- /dev/null +++ b/src/adaptors/evaa-protocol/rewardApy.js @@ -0,0 +1,177 @@ +const fetch = require('node-fetch'); + +function isLeapYear(year) { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} + +async function getDistributions(endpoint = 'evaa.space') { + try { + let result = await fetch(`https://${endpoint}/query/distributions/list`, { + headers: { accept: 'application/json' }, + }); + let resData = await result.json(); + return resData; + } catch (error) { + console.error(error); + return undefined; + } +} +const isLeap = isLeapYear(new Date().getFullYear()); +const totalSecsInYear = (isLeap ? 366 : 365) * 24 * 60 * 60; + +function calcApy( + rewardAmount, + totalAmount, + rewardingAssetPrice, + rewardsAssetPrice, + rewardingScaleFactor, + rewardsScaleFactor, + totalSecsInCurrentSeason +) { + const rate = rewardingScaleFactor / Number(totalAmount); + const rewardForUnit = + rate * + (((Number(rewardAmount) / rewardsScaleFactor) * (rewardsAssetPrice ?? 0)) / + (rewardingAssetPrice || 1)) * + rewardingScaleFactor; + + return ( + ((rewardForUnit * totalSecsInYear) / + (totalSecsInCurrentSeason * rewardingScaleFactor)) * + 100 + ); +} + +function calculateRewardApy(distributionsResp, pool, data, prices) { + try { + if ( + !distributionsResp?.distributions || + distributionsResp.distributions.length === 0 + ) { + console.log('Invalid distributions data:', distributionsResp); + return []; + } + + const currentCampaign = distributionsResp?.distributions.find( + (campaign) => campaign.started && !campaign.expired + ); + + if (!currentCampaign) { + return []; + } + + const seasonsApy = currentCampaign.seasons + ?.filter((season) => season.started && !season.expired) + ?.filter((season) => season.pool === pool) + ?.map((season) => { + const rewardingAssetId = BigInt(season?.rewarding_asset_id ?? 0); + const rewardsAssetId = BigInt(season?.rewards_asset_id ?? 0); + + const rewardingAssetData = data.assetsData.get(rewardingAssetId); + const rewardsAssetData = data.assetsData.get(rewardsAssetId); + + if (!rewardingAssetData || !rewardsAssetData) { + return undefined; + } + + const rewardType = + season?.reward_type?.toLowerCase() === 'borrow' ? 'Borrow' : 'Supply'; + + let rewardAmount = Number(season?.rewards_amount) || 0; + + if (rewardType === 'Borrow' && season?.borrow_budget) { + rewardAmount = season.borrow_budget; + } else if (rewardType === 'Supply' && season?.supply_budget) { + rewardAmount = season.supply_budget; + } + + const totalAmountSupply = + rewardingAssetData.totalSupply?.original ?? + rewardingAssetData.totalSupply; + const totalAmountBorrow = + rewardingAssetData.totalBorrow?.original ?? + rewardingAssetData.totalBorrow; + + const totalAmount = + rewardType === 'Borrow' ? totalAmountBorrow : totalAmountSupply; + + if (!totalAmount || totalAmount === '0') { + return []; + } + + const rewardingAssetConfig = data.assetsConfig.get(rewardingAssetId); + const rewardsAssetConfig = data.assetsConfig.get(rewardsAssetId); + + const rewardingScaleFactor = + 10 ** Number(rewardingAssetConfig?.decimals ?? 0); + const rewardsScaleFactor = + 10 ** Number(rewardsAssetConfig?.decimals ?? 0); + + const rewardingPriceData = prices.dict.get(rewardingAssetId); + const rewardsPriceData = prices.dict.get(rewardsAssetId); + + const rewardingAssetPrice = Number(rewardingPriceData); + const rewardsAssetPrice = Number(rewardsPriceData); + + const seasonStart = new Date(season?.campaign_start ?? 0); + const seasonEnd = new Date(season?.campaign_end ?? 0); + const totalSecsInCurrentSeason = (seasonEnd - seasonStart) / 1000; + + if (totalSecsInCurrentSeason <= 0) { + return undefined; + } + + const baseApy = calcApy( + rewardAmount, + totalAmount, + rewardingAssetPrice, + rewardsAssetPrice, + rewardingScaleFactor, + rewardsScaleFactor, + totalSecsInCurrentSeason + ); + + const result = [ + { + apy: baseApy, + rewardType, + rewardingAssetId, + rewardsAssetId, + }, + ]; + + if ( + rewardType === 'Borrow' && + season?.supply_budget && + season.supply_budget > 0 + ) { + const supplyApy = calcApy( + season.supply_budget, + totalAmountSupply, + rewardingAssetPrice, + rewardsAssetPrice, + rewardingScaleFactor, + rewardsScaleFactor, + totalSecsInCurrentSeason + ); + result.push({ + apy: supplyApy, + rewardType: 'Supply', + rewardingAssetId, + rewardsAssetId, + }); + } + + return result; + }); + return seasonsApy.flat(); + } catch (error) { + console.error(error); + return undefined; + } +} + +module.exports = { + getDistributions, + calculateRewardApy, +}; From ce2df8467d9f1f2ddc0b385eae076bf8ef1ce4f7 Mon Sep 17 00:00:00 2001 From: Danil Shaymurzin Date: Mon, 30 Dec 2024 20:15:23 +0500 Subject: [PATCH 5/6] fix: type capability for calculateRewardApy func --- src/adaptors/evaa-protocol/rewardApy.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/adaptors/evaa-protocol/rewardApy.js b/src/adaptors/evaa-protocol/rewardApy.js index a68ccdb5ec..3b1671328c 100644 --- a/src/adaptors/evaa-protocol/rewardApy.js +++ b/src/adaptors/evaa-protocol/rewardApy.js @@ -71,7 +71,7 @@ function calculateRewardApy(distributionsResp, pool, data, prices) { const rewardsAssetData = data.assetsData.get(rewardsAssetId); if (!rewardingAssetData || !rewardsAssetData) { - return undefined; + return []; } const rewardType = @@ -118,7 +118,7 @@ function calculateRewardApy(distributionsResp, pool, data, prices) { const totalSecsInCurrentSeason = (seasonEnd - seasonStart) / 1000; if (totalSecsInCurrentSeason <= 0) { - return undefined; + return []; } const baseApy = calcApy( @@ -167,7 +167,7 @@ function calculateRewardApy(distributionsResp, pool, data, prices) { return seasonsApy.flat(); } catch (error) { console.error(error); - return undefined; + return []; } } From 0bd4d153cc850ab63db96a71462b9bc2658dd1e5 Mon Sep 17 00:00:00 2001 From: Danil Shaymurzin Date: Mon, 30 Dec 2024 20:15:52 +0500 Subject: [PATCH 6/6] fix: tokenSymbol return for rewardTokens --- src/adaptors/evaa-protocol/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/adaptors/evaa-protocol/index.js b/src/adaptors/evaa-protocol/index.js index 500139e3a8..19db20432f 100644 --- a/src/adaptors/evaa-protocol/index.js +++ b/src/adaptors/evaa-protocol/index.js @@ -33,6 +33,12 @@ const assets = { tsTON: { assetId: sha256Hash("tsTON"), token: 'EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav' }, }; +function findAssetKeyByBigIntId(searchAssetId) { + return Object.entries(assets).find(([key, value]) => + BigInt(value.assetId) === searchAssetId + )?.[0]; +} + const MASTER_CONSTANTS = { FACTOR_SCALE: BigInt(1e12), @@ -380,7 +386,9 @@ const getApy = async () => { ); const apyReward = apyRewardData ? apyRewardData.apy : undefined; - const rewardTokens = apyRewardData ? [apyRewardData.rewardsAssetId] : undefined; + const rewardTokens = apyRewardData + ? [findAssetKeyByBigIntId(apyRewardData.rewardsAssetId)] + : undefined; const apyRewardBorrowData = rewardApys.find( (rewardApy) =>