diff --git a/modules/network/apr-config-types.ts b/modules/network/apr-config-types.ts new file mode 100644 index 000000000..3b344bf80 --- /dev/null +++ b/modules/network/apr-config-types.ts @@ -0,0 +1,179 @@ +export interface IbAprConfig { + aave?: AaveAprConfig; + ankr?: AnkrAprConfig; + beefy?: BeefyAprConfig; + euler?: EulerAprConfig; + gearbox?: GearBoxAprConfig; + idle?: IdleAprConfig; + ovix?: OvixAprConfig; + reaper?: ReaperAprConfig; + tessera?: TesseraAprConfig; + tetu?: TetuAprConfig; + tranchess?: TranchessAprConfig; + yearn?: YearnAprConfig; + defaultHandlers?: DefaultHandlerAprConfig; + fixedAprHandler?: FixedAprConfig; +} + +export interface AaveAprConfig { + [version: string]: { + subgraphUrl: string; + tokens: { + [underlyingAssetName: string]: { + underlyingAssetAddress: string; + aTokenAddress: string; + wrappedTokens: { + [wrappedTokenName: string]: string; + }; + isIbYield?: boolean; + }; + }; + }; +} + +export interface AnkrAprConfig { + sourceUrl: string; + tokens: { + [underlyingAssetName: string]: { + address: string; + serviceName: string; + isIbYield?: boolean; + }; + }; +} + +export interface BeefyAprConfig { + sourceUrl: string; + tokens: { + [tokenName: string]: { + address: string; + // To get the vaultId, get the vault address from the token contract(token.vault()), + // and search for the vault address in the link: https://api.beefy.finance/vaults + vaultId: string; + isIbYield?: boolean; + }; + }; +} + +export interface EulerAprConfig { + subgraphUrl: string; + tokens: { + [tokenName: string]: { + address: string; + isIbYield?: boolean; + }; + }; +} + +export interface GearBoxAprConfig { + sourceUrl: string; + tokens: { + [tokenName: string]: { + address: string; + isIbYield?: boolean; + }; + }; +} + +export interface IdleAprConfig { + sourceUrl: string; + authorizationHeader: string; + tokens: { + [tokenName: string]: { + address: string; + wrapped4626Address: string; + isIbYield?: boolean; + }; + }; +} + +export interface OvixAprConfig { + tokens: { + [tokenName: string]: { + yieldAddress: string; + wrappedAddress: string; + isIbYield?: boolean; + }; + }; +} + +export interface ReaperAprConfig { + subgraphSource?: { + subgraphUrl: string; + tokens: { + [tokenName: string]: { + address: string; + isSftmX?: boolean; + isWstETH?: boolean; + isIbYield?: boolean; + }; + }; + }; + onchainSource?: { + averageAPRAcrossLastNHarvests: number; + tokens: { + [tokenName: string]: { + address: string; + isSftmX?: boolean; + isWstETH?: boolean; + isIbYield?: boolean; + }; + }; + }; +} + +export interface TesseraAprConfig { + tokens: { + [tokenName: string]: { + tesseraPoolAddress: string; + tokenAddress: string; + isIbYield?: boolean; + }; + }; +} + +export interface TetuAprConfig { + sourceUrl: string; + tokens: { + [tokenName: string]: { + address: string; + isIbYield?: boolean; + }; + }; +} + +export interface TranchessAprConfig { + sourceUrl: string; + tokens: { + [tokenName: string]: { + address: string; + underlyingAssetName: string; + isIbYield?: boolean; + }; + }; +} + +export interface YearnAprConfig { + sourceUrl: string; + isIbYield?: boolean; +} + +export interface DefaultHandlerAprConfig { + [tokenName: string]: { + sourceUrl: string; + tokenAddress: string; + path?: string; + scale?: number; + group?: string; + isIbYield?: boolean; + }; +} + +export interface FixedAprConfig { + [tokenName: string]: { + address: string; + apr: number; + group?: string; + isIbYield?: boolean; + }; +} diff --git a/modules/network/arbitrum.ts b/modules/network/arbitrum.ts index 366c401e0..23269b661 100644 --- a/modules/network/arbitrum.ts +++ b/modules/network/arbitrum.ts @@ -1,8 +1,6 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; import { tokenService } from '../token/token.service'; -import { WstethAprService } from '../pool/lib/apr-data-sources/optimism/wsteth-apr.service'; -import { ReaperCryptAprService } from '../pool/lib/apr-data-sources/reaper-crypt-apr.service'; import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; @@ -17,6 +15,7 @@ import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { coingeckoService } from '../coingecko/coingecko.service'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; import { env } from '../../app/env'; const arbitrumNetworkData: NetworkData = { @@ -103,16 +102,79 @@ const arbitrumNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, - reaper: { - linearPoolFactories: ['0xc101dca301a4011c1f925e9622e749e550a1b667'], - linearPoolIdsFromErc4626Factory: [], - averageAPRAcrossLastNHarvests: 2, - multistratAprSubgraphUrl: '', + ibAprConfig: { + aave: { + v3: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3-arbitrum', + tokens: { + USDC: { + underlyingAssetAddress: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', + aTokenAddress: '0x625e7708f30ca75bfd92586e17077590c60eb4cd', + wrappedTokens: { + waUSDC: '0xe719aef17468c7e10c0c205be62c990754dff7e5', + stataArbUSDC: '0x3a301e7917689b8e8a19498b8a28fc912583490c', + stataArbUSDCn: '0xbde67e089886ec0e615d6f054bc6f746189a3d56', + }, + }, + USDT: { + underlyingAssetAddress: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', + aTokenAddress: '0x6ab707aca953edaefbc4fd23ba73294241490620', + wrappedTokens: { + waUSDT: '0x3c7680dfe7f732ca0279c39ff30fe2eafdae49db', + stataArbUSDT: '0x8b5541b773dd781852940490b0c3dc1a8cdb6a87', + }, + }, + DAI: { + underlyingAssetAddress: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', + aTokenAddress: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee', + wrappedTokens: { + waDAI: '0x345a864ac644c82c2d649491c905c71f240700b2', + stataArbDAI: '0x426e8778bf7f54b0e4fc703dcca6f26a4e5b71de', + }, + }, + wETH: { + underlyingAssetAddress: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + aTokenAddress: '0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8', + wrappedTokens: { + waWETH: '0x18c100415988bef4354effad1188d1c22041b046', + stataArbWETH: '0x18468b6eba332285c6d9bb03fe7fb52e108c4596', + }, + }, + }, + }, + }, + defaultHandlers: { + wstETH: { + tokenAddress: '0x5979d7b546e38e414f7e9822514be443a4800529', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + }, + }, + beefy: { + linearPools: [''], }, lido: { wstEthAprEndpoint: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', wstEthContract: '0x5979d7b546e38e414f7e9822514be443a4800529', }, + datastudio: { + main: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '11anHUEb9snGwvB-errb5HvO8TvoLTRJhkDdD80Gxw1Q', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + canary: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '1HnJOuRQXGy06tNgqjYMzQNIsaCSCC01Yxe_lZhXBDpY', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + }, monitoring: { main: { alarmTopicArn: 'arn:aws:sns:ca-central-1:118697801881:api_alarms', @@ -128,15 +190,7 @@ export const arbitrumNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: arbitrumNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ - new WstethAprService(tokenService, arbitrumNetworkData.lido!.wstEthContract), - new ReaperCryptAprService( - arbitrumNetworkData.reaper!.multistratAprSubgraphUrl, - arbitrumNetworkData.reaper!.linearPoolFactories, - arbitrumNetworkData.reaper!.linearPoolIdsFromErc4626Factory, - arbitrumNetworkData.reaper!.averageAPRAcrossLastNHarvests, - arbitrumNetworkData.stader ? arbitrumNetworkData.stader.sFtmxContract : undefined, - arbitrumNetworkData.lido ? arbitrumNetworkData.lido.wstEthContract : undefined, - ), + new IbTokensAprService(arbitrumNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(arbitrumNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/avalanche.ts b/modules/network/avalanche.ts index cbd261d0e..ec7993f44 100644 --- a/modules/network/avalanche.ts +++ b/modules/network/avalanche.ts @@ -16,6 +16,7 @@ import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph import { coingeckoService } from '../coingecko/coingecko.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { env } from '../../app/env'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; const avalancheNetworkData: NetworkData = { chain: { @@ -94,6 +95,86 @@ const avalancheNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, + ibAprConfig: { + aave: { + v3: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3-avalanche', + tokens: { + USDC: { + underlyingAssetAddress: '0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e', + aTokenAddress: '0x625e7708f30ca75bfd92586e17077590c60eb4cd', + wrappedTokens: { + stataAvaUSDC: '0xe7839ea8ea8543c7f5d9c9d7269c661904729fe7', + }, + }, + USDT: { + underlyingAssetAddress: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + aTokenAddress: '0x6ab707aca953edaefbc4fd23ba73294241490620', + wrappedTokens: { + stataAvaUSDT: '0x759a2e28d4c3ad394d3125d5ab75a6a5d6782fd9', + }, + }, + DAI: { + underlyingAssetAddress: '0xd586e7f844cea2f87f50152665bcbc2c279d8d70', + aTokenAddress: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee', + wrappedTokens: { + stataAvaDAI: '0x234c4b76f749dfffd9c18ea7cc0972206b42d019', + }, + }, + wETH: { + underlyingAssetAddress: '0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab', + aTokenAddress: '0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8', + wrappedTokens: { + stataAvaWETH: '0x41bafe0091d55378ed921af3784622923651fdd8', + }, + }, + wAVAX: { + underlyingAssetAddress: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', + aTokenAddress: '0x6d80113e533a2c0fe82eabd35f1875dcea89ea97', + wrappedTokens: { + stataAvaWAVAX: '0xa291ae608d8854cdbf9838e28e9badcf10181669', + }, + }, + wBTC: { + underlyingAssetAddress: '0x50b7545627a5162f82a992c33b87adc75187b218', + aTokenAddress: '0x078f358208685046a11c85e8ad32895ded33a249', + wrappedTokens: { + stataAvaWBTC: '0xb516f74eb030cebd5f616b1a33f88e1213b93c2c', + }, + }, + }, + }, + }, + ankr: { + sourceUrl: 'https://api.staking.ankr.com/v1alpha/metrics', + tokens: { + ankrAVAX: { + address: '0xc3344870d52688874b06d844e0c36cc39fc727f6', + serviceName: 'avax', + isIbYield: true, + }, + }, + }, + }, + beefy: { + linearPools: [''], + }, + datastudio: { + main: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '11anHUEb9snGwvB-errb5HvO8TvoLTRJhkDdD80Gxw1Q', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + canary: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '1HnJOuRQXGy06tNgqjYMzQNIsaCSCC01Yxe_lZhXBDpY', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + }, monitoring: { main: { alarmTopicArn: 'arn:aws:sns:ca-central-1:118697801881:api_alarms', @@ -109,6 +190,7 @@ export const avalancheNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: avalancheNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ + new IbTokensAprService(avalancheNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(avalancheNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/base.ts b/modules/network/base.ts index 7d1027bb4..635f00693 100644 --- a/modules/network/base.ts +++ b/modules/network/base.ts @@ -1,9 +1,6 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; import { tokenService } from '../token/token.service'; -import { WstethAprService } from '../pool/lib/apr-data-sources/optimism/wsteth-apr.service'; -import { ReaperCryptAprService } from '../pool/lib/apr-data-sources/reaper-crypt-apr.service'; -import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; @@ -18,6 +15,7 @@ import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { coingeckoService } from '../coingecko/coingecko.service'; import { env } from '../../app/env'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; const baseNetworkData: NetworkData = { chain: { @@ -76,6 +74,17 @@ const baseNetworkData: NetworkData = { swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, }, + ibAprConfig: { + defaultHandlers: { + cbETH: { + tokenAddress: '0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22', + sourceUrl: 'https://api.exchange.coinbase.com/wrapped-assets/CBETH/', + path: 'apy', + scale: 1, + isIbYield: true, + }, + }, + }, multicall: '0xca11bde05977b3631167028862be2a173976ca11', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', avgBlockSpeed: 2, @@ -95,10 +104,6 @@ const baseNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, - lido: { - wstEthAprEndpoint: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', - wstEthContract: '', - }, monitoring: { main: { alarmTopicArn: 'arn:aws:sns:ca-central-1:118697801881:api_alarms', @@ -114,7 +119,7 @@ export const baseNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: baseNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ - new WstethAprService(tokenService, baseNetworkData.lido!.wstEthContract), + new IbTokensAprService(baseNetworkData.ibAprConfig), new BoostedPoolAprService(), new SwapFeeAprService(baseNetworkData.balancer.swapProtocolFeePercentage), new GaugeAprService(gaugeSubgraphService, tokenService, [baseNetworkData.bal!.address]), diff --git a/modules/network/fantom.ts b/modules/network/fantom.ts index c3cea9af4..19db4cddf 100644 --- a/modules/network/fantom.ts +++ b/modules/network/fantom.ts @@ -2,9 +2,6 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; import { SpookySwapAprService } from '../pool/lib/apr-data-sources/fantom/spooky-swap-apr.service'; import { tokenService } from '../token/token.service'; -import { YearnVaultAprService } from '../pool/lib/apr-data-sources/fantom/yearn-vault-apr.service'; -import { StaderStakedFtmAprService } from '../pool/lib/apr-data-sources/fantom/stader-staked-ftm-apr.service'; -import { ReaperCryptAprService } from '../pool/lib/apr-data-sources/reaper-crypt-apr.service'; import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; @@ -24,11 +21,10 @@ import { UserSyncMasterchefFarmBalanceService } from '../user/lib/user-sync-mast import { UserSyncReliquaryFarmBalanceService } from '../user/lib/user-sync-reliquary-farm-balance.service'; import { every } from '../../worker/intervals'; import { SanityContentService } from '../content/sanity-content.service'; -import { AnkrStakedFtmAprService } from '../pool/lib/apr-data-sources/fantom/ankr-staked-ftm-apr.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { coingeckoService } from '../coingecko/coingecko.service'; -import { AnkrStakedEthAprService } from '../pool/lib/apr-data-sources/fantom/ankr-staked-eth-apr.service'; import { env } from '../../app/env'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; const fantomNetworkData: NetworkData = { chain: { @@ -171,31 +167,95 @@ const fantomNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, - yearn: { - vaultsEndpoint: 'https://d28fcsszptni1s.cloudfront.net/v1/chains/250/vaults/all', + ibAprConfig: { + ankr: { + sourceUrl: 'https://api.staking.ankr.com/v1alpha/metrics', + tokens: { + ankrETH: { + address: '0x12d8ce035c5de3ce39b1fdd4c1d5a745eaba3b8c', + serviceName: 'eth', + isIbYield: true, + }, + ankrFTM: { + address: '0xcfc785741dc0e98ad4c9f6394bb9d43cd1ef5179', + serviceName: 'ftm', + isIbYield: true, + }, + }, + }, + reaper: { + subgraphSource: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/byte-masons/multi-strategy-vaults-fantom', + tokens: { + rfwBTC: { + address: '0xfa985463b7fa975d06cde703ec72efccf293c605', + }, + rffUSDT: { + address: '0xaea55c0e84af6e5ef8c9b7042fb6ab682516214a', + }, + rfWFTM: { + address: '0x963ffcd14d471e279245ee1570ad64ca78d8e67e', + }, + rfWETH: { + address: '0xc052627bc73117d2cb3569f133419550156bdfa1', + }, + rfDAI: { + address: '0x16e4399fa9ba6e58f12bf2d2bc35f8bde8a9a4ab', + }, + rfUSDC: { + address: '0xd55c59da5872de866e39b1e3af2065330ea8acd6', + }, + rfUSDCCrypt: { + // Not named as Multi-Strategy in the contract, but is multi-strategy + address: '0x4455aef4b5d8ffe3436184e8a1ec99607f9a4340', + }, + rfWFTMCrypt: { + // Not named Multi-Strategy in the contract, but is multi-strategy + address: '0xe4a54b6a175cf3f6d7a5e8ab7544c3e6e364dbf9', + }, + rfWETHCrypt: { + // Not named Multi-Strategy in the contract, but is multi-strategy + address: '0x152d62dccc2c7c7930c4483cc2a24fefd23c24c2', + }, + rfDAICrypt: { + // Not named Multi-Strategy in the contract, but is multi-strategy + address: '0x5427f192137405e6a4143d1c3321359bab2dbd87', + }, + rfWBTCCrypt: { + // Not named Multi-Strategy in the contract, but is multi-strategy + address: '0x660c6ec76bd83f53263681f83cbeb35042dcd1cc', + }, + }, + }, + onchainSource: { + averageAPRAcrossLastNHarvests: 5, + tokens: { + rfGrainSFTMX: { + address: '0xab30a4956c7d838234e24f1c3e50082c0607f35f', + isSftmX: true, + }, + rfGrainFTM: { + address: '0xc5b29d59d0b4717aa0dd8d11597d9fd3a05d86bb', + }, + }, + }, + }, + yearn: { + sourceUrl: 'https://d28fcsszptni1s.cloudfront.net/v1/chains/250/vaults/all', + }, + fixedAprHandler: { + sFTMx: { + address: '0xd7028092c830b5c8fce061af2e593413ebbc1fc1', + apr: 0.046, + isIbYield: true, + }, + }, }, copper: { proxyAddress: '0xbc8a71c75ffbd2807c021f4f81a8832392def93c', }, - reaper: { - linearPoolFactories: ['0xd448c4156b8de31e56fdfc071c8d96459bb28119'], - linearPoolIdsFromErc4626Factory: [ - '0x55e0499d268858a5e804d7864dc2a6b4ef194c630000000000000000000005b1', - '0xa9a1f2f7407ce27bcef35d04c47e079e7d6d399e0000000000000000000005b6', - '0xa8bcdca345e61bad9bb539933a4009f7a6f4b7ea0000000000000000000006eb', - '0x654def39262548cc958d07c82622e23c52411c820000000000000000000006ec', - '0xd3f155d7f421414dc4177e54e4308274dfa8b9680000000000000000000006ed', - '0xb8b0e5e9f8b740b557e7c26fcbc753523a718a870000000000000000000006ee', - '0xdc910e2647caae5f63a760b70a2308e1c90d88860000000000000000000006ef', - '0x92502cd8e00f5b8e737b2ba203fdd7cd27b23c8f000000000000000000000718', - '0xc385e76e575b2d71eb877c27dcc1608f77fada99000000000000000000000719', - '0x685056d3a4e574b163d0fa05a78f1b0b3aa04a8000000000000000000000071a', - '0x3c1420df122ac809b9d1ba77906f833764d6450100000000000000000000071b', - '0xa0051ab2c3eb7f17758428b02a07cf72eb0ef1a300000000000000000000071c', - '0x442988091cdc18acb8912cd3fe062cda9233f9dc00000000000000000000071d', - ], - averageAPRAcrossLastNHarvests: 5, - multistratAprSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/byte-masons/multi-strategy-vaults-fantom', + beefy: { + linearPools: [''], }, spooky: { xBooContract: '0x841fad6eae12c286d1fd18d1d525dffa75c7effe', @@ -238,19 +298,8 @@ export const fantomNetworkConfig: NetworkConfig = { contentService: new SanityContentService(), provider: new ethers.providers.JsonRpcProvider({ url: fantomNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ + new IbTokensAprService(fantomNetworkData.ibAprConfig), // new SpookySwapAprService(tokenService, fantomNetworkData.spooky!.xBooContract), - new YearnVaultAprService(tokenService, fantomNetworkData.yearn!.vaultsEndpoint), - new StaderStakedFtmAprService(tokenService, fantomNetworkData.stader!.sFtmxContract), - new AnkrStakedFtmAprService(tokenService, fantomNetworkData.ankr!.ankrFtmContract), - new AnkrStakedEthAprService(tokenService, fantomNetworkData.ankr!.ankrEthContract), - new ReaperCryptAprService( - fantomNetworkData.reaper!.multistratAprSubgraphUrl, - fantomNetworkData.reaper!.linearPoolFactories, - fantomNetworkData.reaper!.linearPoolIdsFromErc4626Factory, - fantomNetworkData.reaper!.averageAPRAcrossLastNHarvests, - fantomNetworkData.stader ? fantomNetworkData.stader.sFtmxContract : undefined, - fantomNetworkData.lido ? fantomNetworkData.lido.wstEthContract : undefined, - ), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(fantomNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/gnosis.ts b/modules/network/gnosis.ts index a585dff7d..82a75020d 100644 --- a/modules/network/gnosis.ts +++ b/modules/network/gnosis.ts @@ -16,6 +16,7 @@ import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph import { coingeckoService } from '../coingecko/coingecko.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { env } from '../../app/env'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; const gnosisNetworkData: NetworkData = { chain: { @@ -92,6 +93,32 @@ const gnosisNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, + ibAprConfig: { + defaultHandlers: { + wstETH: { + tokenAddress: '0x6c76971f98945ae98dd7d4dfca8711ebea946ea6', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + }, + }, + datastudio: { + main: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '11anHUEb9snGwvB-errb5HvO8TvoLTRJhkDdD80Gxw1Q', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + canary: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '1HnJOuRQXGy06tNgqjYMzQNIsaCSCC01Yxe_lZhXBDpY', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + }, monitoring: { main: { alarmTopicArn: 'arn:aws:sns:ca-central-1:118697801881:api_alarms', @@ -110,6 +137,7 @@ export const gnosisNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: gnosisNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ + new IbTokensAprService(gnosisNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(gnosisNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/mainnet.ts b/modules/network/mainnet.ts index beda26ee8..e95c15113 100644 --- a/modules/network/mainnet.ts +++ b/modules/network/mainnet.ts @@ -1,8 +1,6 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; import { tokenService } from '../token/token.service'; -import { WstethAprService } from '../pool/lib/apr-data-sources/optimism/wsteth-apr.service'; -import { ReaperCryptAprService } from '../pool/lib/apr-data-sources/reaper-crypt-apr.service'; import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; @@ -17,9 +15,17 @@ import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; import { coingeckoService } from '../coingecko/coingecko.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; import { env } from '../../app/env'; -const mainnetNetworkData: NetworkData = { +const underlyingTokens = { + USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + USDT: '0xdac17f958d2ee523a2206206994597c13d831ec7', + DAI: '0x6b175474e89094c44da98b954eedeac495271d0f', + wETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', +}; + +export const mainnetNetworkData: NetworkData = { chain: { slug: 'ethereum', id: 1, @@ -104,9 +110,214 @@ const mainnetNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, - lido: { - wstEthAprEndpoint: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', - wstEthContract: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', + ibAprConfig: { + aave: { + v2: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v2', + tokens: { + USDC: { + underlyingAssetAddress: underlyingTokens.USDC, + aTokenAddress: '0xbcca60bb61934080951369a648fb03df4f96263c', + wrappedTokens: { + waUSDC: '0xd093fa4fb80d09bb30817fdcd442d4d02ed3e5de', + }, + }, + USDT: { + underlyingAssetAddress: underlyingTokens.USDT, + aTokenAddress: '0x3ed3b47dd13ec9a98b44e6204a523e766b225811', + wrappedTokens: { + waUSDT: '0xf8fd466f12e236f4c96f7cce6c79eadb819abf58', + }, + }, + DAI: { + underlyingAssetAddress: underlyingTokens.DAI, + aTokenAddress: '0x028171bca77440897b824ca71d1c56cac55b68a3', + wrappedTokens: { + waDAI: '0x02d60b84491589974263d922d9cc7a3152618ef6', + }, + }, + }, + }, + v3: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3', + tokens: { + USDC: { + underlyingAssetAddress: underlyingTokens.USDC, + aTokenAddress: '0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c', + wrappedTokens: { + waUSDC: '0x57d20c946a7a3812a7225b881cdcd8431d23431c', + stataEthUSDC: '0x02c2d189b45ce213a40097b62d311cf0dd16ec92', + }, + }, + USDT: { + underlyingAssetAddress: underlyingTokens.USDT, + aTokenAddress: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a', + wrappedTokens: { + waUSDT: '0xa7e0e66f38b8ad8343cff67118c1f33e827d1455', + stataEthUSDT: '0x65799b9fd4206cdaa4a1db79254fcbc2fd2ffee6', + }, + }, + DAI: { + underlyingAssetAddress: underlyingTokens.DAI, + aTokenAddress: '0x018008bfb33d285247a21d44e50697654f754e63', + wrappedTokens: { + waDAI: '0x098256c06ab24f5655c5506a6488781bd711c14b', + stataEthDAI: '0x098256c06ab24f5655c5506a6488781bd711c14b', + }, + }, + wETH: { + underlyingAssetAddress: underlyingTokens.wETH, + aTokenAddress: '0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8', + wrappedTokens: { + waWETH: '0x59463bb67ddd04fe58ed291ba36c26d99a39fbc6', + stataEthWETH: '0x03928473f25bb2da6bc880b07ecbadc636822264', + }, + }, + }, + }, + }, + ankr: { + sourceUrl: 'https://api.staking.ankr.com/v1alpha/metrics', + tokens: { + ankrETH: { + address: '0xe95a203b1a91a908f9b9ce46459d101078c2c3cb', + serviceName: 'eth', + isIbYield: true, + }, + }, + }, + euler: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/euler-xyz/euler-mainnet', + tokens: { + eUSDC: { address: '0xeb91861f8a4e1c12333f42dce8fb0ecdc28da716' }, + eDAI: { address: '0xe025e3ca2be02316033184551d4d3aa22024d9dc' }, + eUSDT: { address: '0x4d19f33948b99800b6113ff3e83bec9b537c85d2' }, + eFRAX: { address: '0x5484451a88a35cd0878a1be177435ca8a0e4054e' }, + }, + }, + gearbox: { + sourceUrl: 'https://mainnet.gearbox.foundation/api/pools', + tokens: { + dDAI: { address: '0x6cfaf95457d7688022fc53e7abe052ef8dfbbdba' }, + dUSDC: { address: '0xc411db5f5eb3f7d552f9b8454b2d74097ccde6e3' }, + }, + }, + idle: { + sourceUrl: 'https://api.idle.finance/junior-rates/', + authorizationHeader: + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6IkFwcDciLCJpYXQiOjE2NzAyMzc1Mjd9.L12KJEt8fW1Cvy3o7Nl4OJ2wtEjzlObaAYJ9aC_CY6M', + tokens: { + idleDAI: { + address: '0xec9482040e6483b7459cc0db05d51dfa3d3068e1', + wrapped4626Address: '0x0c80f31b840c6564e6c5e18f386fad96b63514ca', + }, + idleUSDC: { + address: '0xdc7777c771a6e4b3a82830781bdde4dbc78f320e', + wrapped4626Address: '0xc3da79e0de523eef7ac1e4ca9abfe3aac9973133', + }, + idleUSDT: { + address: '0xfa3afc9a194babd56e743fa3b7aa2ccbed3eaaad', + wrapped4626Address: '0x544897a3b944fdeb1f94a0ed973ea31a80ae18e1', + }, + }, + }, + tessera: { + tokens: { + sAPE: { + tesseraPoolAddress: '0x5954ab967bc958940b7eb73ee84797dc8a2afbb9', + tokenAddress: '0x7966c5bae631294d7cffcea5430b78c2f76db6fa', + }, + }, + }, + tranchess: { + sourceUrl: 'https://tranchess.com/eth/api/v3/funds', + tokens: { + qETH: { + address: '0x93ef1ea305d11a9b2a3ebb9bb4fcc34695292e7d', + underlyingAssetName: 'WETH', + }, + }, + }, + defaultHandlers: { + vETH: { + tokenAddress: '0x4bc3263eb5bb2ef7ad9ab6fb68be80e43b43801f', + sourceUrl: 'https://apy.liebi.com/veth', + path: 'veth', + }, + stETH: { + tokenAddress: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + wstETH: { + tokenAddress: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + cbETH: { + tokenAddress: '0xbe9895146f7af43049ca1c1ae358b0541ea49704', + sourceUrl: 'https://api.exchange.coinbase.com/wrapped-assets/CBETH/', + path: 'apy', + scale: 1, + isIbYield: true, + }, + sfrxETH: { + tokenAddress: '0xac3e018457b222d93114458476f3e3416abbe38f', + sourceUrl: 'https://api.frax.finance/v2/frxeth/summary/latest', + path: 'sfrxethApr', + isIbYield: true, + }, + StaFirETH: { + tokenAddress: '0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593', + sourceUrl: 'https://drop-api.stafi.io/reth/v1/poolData', + path: 'data.stakeApr', + isIbYield: true, + }, + rETH: { + tokenAddress: '0xae78736cd615f374d3085123a210448e74fc6393', + sourceUrl: 'https://rocketpool.net/api/mainnet/payload', + path: 'rethAPR', + isIbYield: true, + }, + USDR: { + tokenAddress: '0xaf0d9d65fc54de245cda37af3d18cbec860a4d4b', + sourceUrl: 'http://usdr-api.us-east-1.elasticbeanstalk.com/usdr/apy', + path: 'usdr', + isIbYield: true, + }, + swETH: { + tokenAddress: '0xf951e335afb289353dc249e82926178eac7ded78', + sourceUrl: 'https://v3.svc.swellnetwork.io/api/tokens/sweth/apr', + isIbYield: true, + }, + wjAURA: { + tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + sourceUrl: 'https://data.jonesdao.io/api/v1/jones/apy-wjaura', + path: 'wjauraApy', + isIbYield: true, + }, + }, + }, + beefy: { + linearPools: [''], + }, + datastudio: { + main: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '11anHUEb9snGwvB-errb5HvO8TvoLTRJhkDdD80Gxw1Q', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + canary: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '1HnJOuRQXGy06tNgqjYMzQNIsaCSCC01Yxe_lZhXBDpY', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, }, monitoring: { main: { @@ -123,7 +334,7 @@ export const mainnetNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: mainnetNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ - new WstethAprService(tokenService, mainnetNetworkData.lido!.wstEthContract), + new IbTokensAprService(mainnetNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(mainnetNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/network-config-types.ts b/modules/network/network-config-types.ts index f283d559b..f8c742b00 100644 --- a/modules/network/network-config-types.ts +++ b/modules/network/network-config-types.ts @@ -6,6 +6,7 @@ import { TokenPriceHandler } from '../token/token-types'; import { BaseProvider } from '@ethersproject/providers'; import { GqlChain } from '../../schema'; import { ContentService } from '../content/content-types'; +import { AaveAprConfig, IbAprConfig } from './apr-config-types'; export interface NetworkConfig { data: NetworkData; @@ -105,6 +106,7 @@ export interface NetworkData { address: string; excludedFarmIds: string[]; }; + ibAprConfig: IbAprConfig; reliquary?: { address: string; excludedFarmIds: string[]; @@ -112,18 +114,9 @@ export interface NetworkData { copper?: { proxyAddress: string; }; - reaper?: { - linearPoolFactories: string[]; - linearPoolIdsFromErc4626Factory: string[]; - averageAPRAcrossLastNHarvests: number; - multistratAprSubgraphUrl: string; - }; beefy?: { linearPools: string[]; }; - yearn?: { - vaultsEndpoint: string; - }; lido?: { wstEthContract: string; wstEthAprEndpoint: string; diff --git a/modules/network/optimism.ts b/modules/network/optimism.ts index 3a08a41db..8cb437ad8 100644 --- a/modules/network/optimism.ts +++ b/modules/network/optimism.ts @@ -1,10 +1,6 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; -import { RocketPoolStakedEthAprService } from '../pool/lib/apr-data-sources/optimism/rocket-pool-staked-eth-apr.service'; import { tokenService } from '../token/token.service'; -import { WstethAprService } from '../pool/lib/apr-data-sources/optimism/wsteth-apr.service'; -import { OvernightAprService } from '../pool/lib/apr-data-sources/optimism/overnight-apr.service'; -import { ReaperCryptAprService } from '../pool/lib/apr-data-sources/reaper-crypt-apr.service'; import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; @@ -20,7 +16,7 @@ import { SanityContentService } from '../content/sanity-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; import { coingeckoService } from '../coingecko/coingecko.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; -import { BeefyVaultAprService } from '../pool/lib/apr-data-sources/beefy-vault-apr.service copy'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; import { env } from '../../app/env'; const optimismNetworkData: NetworkData = { @@ -118,22 +114,97 @@ const optimismNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, - reaper: { - linearPoolFactories: [ - '0x19968d4b7126904fd665ed25417599df9604df83', - '0xe4b88e745dce9084b9fc2439f85a9a4c5cd6f361', - ], - linearPoolIdsFromErc4626Factory: [ - '0x20715545c15c76461861cb0d6ba96929766d05a50000000000000000000000e8', - '0xf970659221bb9d01b615321b63a26e857ffc030b0000000000000000000000e9', - '0xa5d4802b4ce6b745b0c9e1b4a79c093d197869c80000000000000000000000ea', - '0x2e2b8b82123789d895fd79913f6dfa51f5b5a0e60000000000000000000000eb', - '0x48ace81c09382bfc08ed102e7eadd37e3b0497520000000000000000000000ec', - '0x8025586ac5fb265a23b9492e7414beccc2059ec30000000000000000000000ed', - '0x3e9cbffd270ae67abb09d28988e7e785498c73730000000000000000000000ee', - ], - averageAPRAcrossLastNHarvests: 2, - multistratAprSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/byte-masons/multi-strategy-vaults-optimism', + ibAprConfig: { + beefy: { + sourceUrl: 'https://api.beefy.finance/apy/breakdown?_=', + tokens: { + wmooExactlySupplyUSDC: { + address: '0xe5e9168b45a90c1e5730da6184cc5901c6e4353f', + vaultId: 'exactly-supply-usdc', + }, + wmooExactlySupplyETH: { + address: '0x44b1cea4f597f493e2fd0833a9c04dfb1e479ef0', + vaultId: 'exactly-supply-eth', + }, + // To get the vaultId, get the vault address from the token contract(token.vault()), + // and search for the vault address in the link: https://api.beefy.finance/vaults + }, + }, + reaper: { + subgraphSource: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/byte-masons/multi-strategy-vaults-optimism', + tokens: { + rfUSDT: { + address: '0x51868bb8b71fb423b87129908fa039b880c8612d', + }, + rfWETH: { + address: '0x1bad45e92dce078cf68c2141cd34f54a02c92806', + }, + rfOP: { + address: '0xcecd29559a84e4d4f6467b36bbd4b9c3e6b89771', + }, + rfwstETH: { + address: '0xb19f4d65882f6c103c332f0bc012354548e9ce0e', + isWstETH: true, + }, + rfWBTC: { + address: '0xf6533b6fcb3f42d2fc91da7c379858ae6ebc7448', + }, + rfDAI: { + address: '0xc0f5da4fb484ce6d8a6832819299f7cd0d15726e', + }, + rfUSDC: { + address: '0x508734b52ba7e04ba068a2d4f67720ac1f63df47', + }, + }, + }, + onchainSource: { + averageAPRAcrossLastNHarvests: 2, + tokens: { + rfsoUSDC: { + address: '0x875456b73cbc58aa1be98dfe3b0459e0c0bf7b0e', + }, + rfsoUSDT: { + address: '0x1e1bf73db9b278a95c9fe9205759956edea8b6ae', + }, + rfsoDAI: { + address: '0x19ca00d242e96a30a1cad12f08c375caa989628f', + }, + rfsoWBTC: { + address: '0x73e51b0368ef8bd0070b12dd992c54aa53bcb5f4', + }, + rfsoWSTETH: { + address: '0x3573de618ae4a740fb24215d93f4483436fbb2b6', + }, + }, + }, + }, + defaultHandlers: { + wstEth: { + tokenAddress: '0x1f32b1c2345538c0c6f582fcb022739c4a194ebb', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + rETH: { + tokenAddress: '0x9bcef72be871e61ed4fbbc7630889bee758eb81d', + sourceUrl: 'https://rocketpool.net/api/mainnet/payload', + path: 'rethAPR', + isIbYield: true, + }, + overnightDAIPlus: { + tokenAddress: '0x0b8f31480249cc717081928b8af733f45f6915bb', + sourceUrl: 'https://api.overnight.fi/optimism/dai+/fin-data/avg-apr/week', + path: 'value', + group: 'OVERNIGHT', + }, + overnightUSDPlus: { + tokenAddress: '0xa348700745d249c3b49d2c2acac9a5ae8155f826', + sourceUrl: 'https://api.overnight.fi/optimism/usd+/fin-data/avg-apr/week', + path: 'value', + group: 'OVERNIGHT', + }, + }, }, beefy: { linearPools: [ @@ -141,10 +212,6 @@ const optimismNetworkData: NetworkData = { '0x72d6df381cac8c2283c0b13fe5262a1f5e8e8d1b0000000000000000000000cb', ], }, - lido: { - wstEthAprEndpoint: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', - wstEthContract: '0x1f32b1c2345538c0c6f582fcb022739c4a194ebb', - }, rocket: { rEthContract: '0x9bcef72be871e61ed4fbbc7630889bee758eb81d', }, @@ -182,18 +249,7 @@ export const optimismNetworkConfig: NetworkConfig = { contentService: new SanityContentService(), provider: new ethers.providers.JsonRpcProvider({ url: optimismNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ - new RocketPoolStakedEthAprService(tokenService, optimismNetworkData.rocket!.rEthContract), - new WstethAprService(tokenService, optimismNetworkData.lido!.wstEthContract), - new OvernightAprService(optimismNetworkData.overnight!.aprEndpoint, tokenService), - new ReaperCryptAprService( - optimismNetworkData.reaper!.multistratAprSubgraphUrl, - optimismNetworkData.reaper!.linearPoolFactories, - optimismNetworkData.reaper!.linearPoolIdsFromErc4626Factory, - optimismNetworkData.reaper!.averageAPRAcrossLastNHarvests, - optimismNetworkData.stader ? optimismNetworkData.stader.sFtmxContract : undefined, - optimismNetworkData.lido ? optimismNetworkData.lido.wstEthContract : undefined, - ), - new BeefyVaultAprService(optimismNetworkData.beefy!.linearPools, tokenService), + new IbTokensAprService(optimismNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(optimismNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/polygon.ts b/modules/network/polygon.ts index bc320d17a..3646e3e27 100644 --- a/modules/network/polygon.ts +++ b/modules/network/polygon.ts @@ -6,7 +6,6 @@ import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; import { GaugeAprService } from '../pool/lib/apr-data-sources/ve-bal-gauge-apr.service'; import { GaugeStakingService } from '../pool/lib/staking/gauge-staking.service'; -import { BeetsPriceHandlerService } from '../token/lib/token-price-handlers/beets-price-handler.service'; import { BptPriceHandlerService } from '../token/lib/token-price-handlers/bpt-price-handler.service'; import { LinearWrappedTokenPriceHandlerService } from '../token/lib/token-price-handlers/linear-wrapped-token-price-handler.service'; import { SwapsPriceHandlerService } from '../token/lib/token-price-handlers/swaps-price-handler.service'; @@ -16,6 +15,7 @@ import { GithubContentService } from '../content/github-content.service'; import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph.service'; import { coingeckoService } from '../coingecko/coingecko.service'; import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; import { env } from '../../app/env'; const polygonNetworkData: NetworkData = { @@ -102,6 +102,136 @@ const polygonNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, + ibAprConfig: { + aave: { + v2: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/aave/aave-v2-matic', + tokens: { + USDC: { + underlyingAssetAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + aTokenAddress: '0x1a13f4ca1d028320a707d99520abfefca3998b7f', + wrappedTokens: { + waUSDC: '0x221836a597948dce8f3568e044ff123108acc42a', + }, + }, + USDT: { + underlyingAssetAddress: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + aTokenAddress: '0x60d55f02a771d515e077c9c2403a1ef324885cec', + wrappedTokens: { + waUSDT: '0x19c60a251e525fa88cd6f3768416a8024e98fc19', + }, + }, + DAI: { + underlyingAssetAddress: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', + aTokenAddress: '0x27f8d03b3a2196956ed754badc28d73be8830a6e', + wrappedTokens: { + waDAI: '0xee029120c72b0607344f35b17cdd90025e647b00', + }, + }, + }, + }, + v3: { + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3-polygon', + tokens: { + USDC: { + underlyingAssetAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + aTokenAddress: '0x625e7708f30ca75bfd92586e17077590c60eb4cd', + wrappedTokens: { + waUSDC: '0xac69e38ed4298490906a3f8d84aefe883f3e86b5', + stataPolUSDC: '0xc04296aa4534f5a3bab2d948705bc89317b2f1ed', + }, + }, + USDT: { + underlyingAssetAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7', + aTokenAddress: '0x23878914efe38d27c4d67ab83ed1b93a74d4086a', + wrappedTokens: { + waUSDT: '0x715d73a88f2f0115d87cfe5e0f25d756b2f9679f', + stataPolUSDT: '0x31f5ac91804a4c0b54c0243789df5208993235a1', + }, + }, + DAI: { + underlyingAssetAddress: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', + aTokenAddress: '0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee', + wrappedTokens: { + waDAI: '0xdb6df721a6e7fdb97363079b01f107860ac156f9', + stataPolDAI: '0xfcf5d4b313e06bb3628eb4fe73320e94039dc4b7', + }, + }, + wETH: { + underlyingAssetAddress: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', + aTokenAddress: '0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8', + wrappedTokens: { + waWETH: '0xa5bbf0f46b9dc8a43147862ba35c8134eb45f1f5', + stataPolWETH: '0xd08b78b11df105d2861568959fca28e30c91cf68', + }, + }, + wMATIC: { + underlyingAssetAddress: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', + aTokenAddress: '0x6d80113e533a2c0fe82eabd35f1875dcea89ea97', + wrappedTokens: { + waWMATIC: '0x0d6135b2cfbae3b1c58368a93b855fa54fa5aae1', + stataPolWMATIC: '0x6f3913333f2d4b7b01d17bedbce1e4c758b94465', + }, + }, + }, + }, + }, + tetu: { + sourceUrl: 'https://api.tetu.io/api/v1/reader/compoundAPRs?network=MATIC', + tokens: { + tUSDC: { address: '0x113f3d54c31ebc71510fd664c8303b34fbc2b355' }, + tUSDT: { address: '0x236975da9f0761e9cf3c2b0f705d705e22829886' }, + tDAI: { address: '0xace2ac58e1e5a7bfe274916c4d82914d490ed4a5' }, + tetuStQI: { address: '0x4cd44ced63d9a6fef595f6ad3f7ced13fceac768' }, + }, + }, + defaultHandlers: { + wstETH: { + tokenAddress: '0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + stMATIC: { + tokenAddress: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4', + sourceUrl: 'https://polygon.lido.fi/api/stats', + path: 'apr', + isIbYield: true, + }, + MATICX: { + tokenAddress: '0xfa68fb4628dff1028cfec22b4162fccd0d45efb6', + sourceUrl: 'https://universe.staderlabs.com/polygon/apy', + path: 'value', + isIbYield: true, + }, + wbETH: { + tokenAddress: '0xa2e3356610840701bdf5611a53974510ae27e2e1', + sourceUrl: + 'https://www.binance.com/bapi/earn/v1/public/pos/cftoken/project/rewardRateList?projectId=BETH', + path: 'data.0.rewardRate', + isIbYield: true, + }, + }, + }, + beefy: { + linearPools: [''], + }, + datastudio: { + main: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '11anHUEb9snGwvB-errb5HvO8TvoLTRJhkDdD80Gxw1Q', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + canary: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '1HnJOuRQXGy06tNgqjYMzQNIsaCSCC01Yxe_lZhXBDpY', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + }, monitoring: { main: { alarmTopicArn: 'arn:aws:sns:ca-central-1:118697801881:api_alarms', @@ -117,6 +247,7 @@ export const polygonNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: polygonNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ + new IbTokensAprService(polygonNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(polygonNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/network/zkevm.ts b/modules/network/zkevm.ts index 7d5a74fdb..9ab16362a 100644 --- a/modules/network/zkevm.ts +++ b/modules/network/zkevm.ts @@ -1,7 +1,6 @@ import { BigNumber, ethers } from 'ethers'; import { DeploymentEnv, NetworkConfig, NetworkData } from './network-config-types'; import { tokenService } from '../token/token.service'; -import { WstethAprService } from '../pool/lib/apr-data-sources/optimism/wsteth-apr.service'; import { PhantomStableAprService } from '../pool/lib/apr-data-sources/phantom-stable-apr.service'; import { BoostedPoolAprService } from '../pool/lib/apr-data-sources/boosted-pool-apr.service'; import { SwapFeeAprService } from '../pool/lib/apr-data-sources/swap-fee-apr.service'; @@ -17,6 +16,7 @@ import { gaugeSubgraphService } from '../subgraphs/gauge-subgraph/gauge-subgraph import { CoingeckoPriceHandlerService } from '../token/lib/token-price-handlers/coingecko-price-handler.service'; import { coingeckoService } from '../coingecko/coingecko.service'; import { env } from '../../app/env'; +import { IbTokensAprService } from '../pool/lib/apr-data-sources/ib-tokens-apr.service'; const zkevmNetworkData: NetworkData = { chain: { @@ -99,6 +99,47 @@ const zkevmNetworkData: NetworkData = { swapGas: BigNumber.from('1000000'), }, }, + ibAprConfig: { + ovix: { + tokens: { + USDT: { + yieldAddress: '0xad41c77d99e282267c1492cdefe528d7d5044253', + wrappedAddress: '0x550d3bb1f77f97e4debb45d4f817d7b9f9a1affb', + }, + USDC: { + yieldAddress: '0x68d9baa40394da2e2c1ca05d30bf33f52823ee7b', + wrappedAddress: '0x3a6789fc7c05a83cfdff5d2f9428ad9868b4ff85', + }, + }, + }, + defaultHandlers: { + wstETH: { + tokenAddress: '0x5d8cff95d7a57c0bf50b30b43c7cc0d52825d4a9', + sourceUrl: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', + path: 'data.smaApr', + isIbYield: true, + }, + }, + }, + beefy: { + linearPools: [''], + }, + datastudio: { + main: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '11anHUEb9snGwvB-errb5HvO8TvoLTRJhkDdD80Gxw1Q', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + canary: { + user: 'datafeed-service@datastudio-366113.iam.gserviceaccount.com', + sheetId: '1HnJOuRQXGy06tNgqjYMzQNIsaCSCC01Yxe_lZhXBDpY', + databaseTabName: 'Database v2', + compositionTabName: 'Pool Composition v2', + emissionDataTabName: 'EmissionData', + }, + }, monitoring: { main: { alarmTopicArn: 'arn:aws:sns:ca-central-1:118697801881:api_alarms', @@ -114,6 +155,7 @@ export const zkevmNetworkConfig: NetworkConfig = { contentService: new GithubContentService(), provider: new ethers.providers.JsonRpcProvider({ url: zkevmNetworkData.rpcUrl, timeout: 60000 }), poolAprServices: [ + new IbTokensAprService(zkevmNetworkData.ibAprConfig), new PhantomStableAprService(), new BoostedPoolAprService(), new SwapFeeAprService(zkevmNetworkData.balancer.swapProtocolFeePercentage), diff --git a/modules/pool/lib/apr-data-sources/apr-types.ts b/modules/pool/lib/apr-data-sources/apr-types.ts deleted file mode 100644 index b51e5b268..000000000 --- a/modules/pool/lib/apr-data-sources/apr-types.ts +++ /dev/null @@ -1,71 +0,0 @@ -export interface YearnVault { - inception: number; - address: string; - symbol: string; - name: string; - display_name: string; - icon: string; - token: YearnVaultToken; - tvl: YearnVaultTvl; - apy: YearnVaultApy; - strategies: YearnStrategy[]; - endorsed: boolean; - version: string; - decimals: number; - type: YearnVaultType; - emergency_shutdown: boolean; - updated: number; - migration: YearnVaultMigration; -} - -interface YearnVaultApy { - type: YearnVaultApyType; - gross_apr: number; - net_apy: number; - fees: YearnFees; - points: YearnPoints; - composite: null; -} - -interface YearnFees { - performance: number; - withdrawal: number | null; - management: number | null; - keep_crv: null; - cvx_keep_crv: null; -} - -interface YearnPoints { - week_ago: number; - month_ago: number; - inception: number; -} - -type YearnVaultApyType = 'error' | 'v2:averaged'; - -interface YearnVaultMigration { - available: boolean; - address: string; -} - -interface YearnStrategy { - address: string; - name: string; -} - -interface YearnVaultToken { - name: string; - symbol: string; - address: string; - decimals: number; - display_name: string; - icon: string; -} - -interface YearnVaultTvl { - total_assets: number; - price: number | null; - tvl: number | null; -} - -type YearnVaultType = 'v2'; diff --git a/modules/pool/lib/apr-data-sources/beefy-vault-apr.service copy.ts b/modules/pool/lib/apr-data-sources/beefy-vault-apr.service copy.ts deleted file mode 100644 index a9aacf123..000000000 --- a/modules/pool/lib/apr-data-sources/beefy-vault-apr.service copy.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { isSameAddress } from '@balancer-labs/sdk'; -import * as Sentry from '@sentry/node'; -import { prisma } from '../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; -import { TokenService } from '../../../token/token.service'; -import { getContractAt } from '../../../web3/contract'; -import { PoolAprService } from '../../pool-types'; -import BeefyWrapper from './abi/BeefyWrapper.json'; -import axios from 'axios'; -import moment from 'moment-timezone'; -import { networkContext } from '../../../network/network-context.service'; - -interface VaultInformation { - id: string; - name: string; - token: string; - tokenAddress: string; - tokenDecimals: number; - earnedToken: string; - earnedTokenAddress: string; - earnContractAddress: string; - oracle: string; - oracleId: string; - status: string; - platformId: string; - assets: string[]; - risks: string[]; - strategyTypeId: string; - buyTokenUrl: string; - network: string; - createdAt: number; - chain: string; - strategy: string; - lastHarvest: number; - pricePerFullShare: string; -} - -type VaultApr = Record< - string, - { - vaultApr: number; - compoundingsPerYear: number; - beefyPerformanceFee: number; - vaultApy: number; - lpFee: number; - tradingApr: number; - totalApy: number; - } ->; - -export class BeefyVaultAprService implements PoolAprService { - constructor(private readonly beefyLinearPools: string[], private readonly tokenService: TokenService) {} - - public getAprServiceName(): string { - return 'BeefyVaultAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - - for (const pool of pools) { - if (!this.beefyLinearPools.includes(pool.id || '') || !pool.linearData || !pool.dynamicData) { - continue; - } - - const itemId = `${pool.id}-beefy-vault`; - - const linearData = pool.linearData; - const wrappedToken = pool.tokens[linearData.wrappedIndex]; - const mainToken = pool.tokens[linearData.mainIndex]; - - const beefyWrapper = getContractAt(wrappedToken.address, BeefyWrapper); - const beefyVaultAddress: string = await beefyWrapper.vault(); - - const beefyVaultBaseApr = await this.getBeefyVaultBaseApr(beefyVaultAddress); - - const tokenPrice = this.tokenService.getPriceForToken(tokenPrices, mainToken.address); - const wrappedTokens = parseFloat(wrappedToken.dynamicData?.balance || '0'); - const priceRate = parseFloat(wrappedToken.dynamicData?.priceRate || '1.0'); - const poolWrappedLiquidity = wrappedTokens * priceRate * tokenPrice; - const totalLiquidity = pool.dynamicData.totalLiquidity; - let apr = totalLiquidity > 0 ? beefyVaultBaseApr * (poolWrappedLiquidity / totalLiquidity) : 0; - - await prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - create: { - id: itemId, - chain: networkContext.chain, - poolId: pool.id, - title: `${wrappedToken.token.symbol} APR`, - apr: apr, - group: 'BEEFY', - type: 'LINEAR_BOOSTED', - }, - update: { title: `${wrappedToken.token.symbol} APR`, apr: apr }, - }); - } - } - - private async getBeefyVaultBaseApr(beefyVaultAddress: string): Promise { - const vaultEndpoint = 'https://api.beefy.finance/vaults?_='; - const aprEndpoint = 'https://api.beefy.finance/apy/breakdown?_='; - - const now = moment().startOf('minute').unix(); - - const { data: vaultData } = await axios.get(vaultEndpoint + `${now}`); - const beefyVault = vaultData.find((vault) => isSameAddress(vault.earnContractAddress, beefyVaultAddress)); - - if (beefyVault?.id) { - const { data: aprData } = await axios.get(aprEndpoint + `${now}`); - return aprData[beefyVault.id].vaultApr; - } - return 0; - } -} diff --git a/modules/pool/lib/apr-data-sources/boosted-pool-apr.service.ts b/modules/pool/lib/apr-data-sources/boosted-pool-apr.service.ts index 29e97a906..fecb8d601 100644 --- a/modules/pool/lib/apr-data-sources/boosted-pool-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/boosted-pool-apr.service.ts @@ -1,5 +1,5 @@ import { PoolAprService } from '../../pool-types'; -import { PrismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; +import { PrismaPoolWithTokens, prismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; import { prisma } from '../../../../prisma/prisma-client'; import { collectsYieldFee } from '../pool-utils'; import { networkContext } from '../../../network/network-context.service'; @@ -9,14 +9,41 @@ export class BoostedPoolAprService implements PoolAprService { return 'BoostedPoolAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const boostedPools = pools.filter( - (pool) => - (pool.type === 'PHANTOM_STABLE' || pool.type === 'WEIGHTED') && - pool.tokens.find((token) => token.nestedPool), + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { + // need to do multiple queries otherwise the nesting is too deep for many pools. Error: stack depth limit exceeded + const boostedPools = pools.filter((pool) => pool.type === 'PHANTOM_STABLE' || pool.type === 'WEIGHTED'); + + const boostedPoolsWithNestedPool = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: boostedPools.map((pool) => pool.id) } }, + include: { + tokens: { + orderBy: { index: 'asc' }, + include: { + nestedPool: true, + }, + }, + }, + }); + + const filteredBoostedPools = boostedPoolsWithNestedPool.filter((pool) => + pool.tokens.find((token) => token.nestedPool), ); - for (const pool of boostedPools) { + const filteredBoostedPoolsExpanded = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: filteredBoostedPools.map((pool) => pool.id) } }, + include: { + dynamicData: true, + tokens: { + orderBy: { index: 'asc' }, + include: { + dynamicData: true, + nestedPool: true, + }, + }, + }, + }); + + for (const pool of filteredBoostedPoolsExpanded) { const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee ? parseFloat(pool.dynamicData.protocolYieldFee) : networkContext.data.balancer.yieldProtocolFeePercentage; @@ -51,7 +78,7 @@ export class BoostedPoolAprService implements PoolAprService { !pool.dynamicData || !token.dynamicData || !token.nestedPool || - !token.nestedPool.dynamicData || + !token.nestedPool.type || token.dynamicData.balanceUSD === 0 ) { continue; diff --git a/modules/pool/lib/apr-data-sources/fantom/ankr-staked-eth-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/ankr-staked-eth-apr.service.ts deleted file mode 100644 index ae9bc1d74..000000000 --- a/modules/pool/lib/apr-data-sources/fantom/ankr-staked-eth-apr.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import axios from 'axios'; -import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import { TokenService } from '../../../../token/token.service'; -import { PoolAprService } from '../../../pool-types'; -import { collectsYieldFee } from '../../pool-utils'; -import { liquidStakedBaseAprService } from '../liquid-staked-base-apr.service'; -import { networkContext } from '../../../../network/network-context.service'; - -export class AnkrStakedEthAprService implements PoolAprService { - constructor(private readonly tokenService: TokenService, private readonly ankrEthAddress: string) {} - - public getAprServiceName(): string { - return 'AnkrStakedEthAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - const ankrEthPrice = this.tokenService.getPriceForToken(tokenPrices, this.ankrEthAddress); - - const ankrEthBaseApr = await liquidStakedBaseAprService.getAnkrEthBaseApr(); - - let operations: any[] = []; - for (const pool of pools) { - const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee - ? parseFloat(pool.dynamicData.protocolYieldFee) - : networkContext.data.balancer.yieldProtocolFeePercentage; - const ankrEthToken = pool.tokens.find((token) => token.address === this.ankrEthAddress); - const ankrEthTokenBalance = ankrEthToken?.dynamicData?.balance; - if (ankrEthTokenBalance && pool.dynamicData) { - const ankrEthPercentage = - (parseFloat(ankrEthTokenBalance) * ankrEthPrice) / pool.dynamicData.totalLiquidity; - const poolAnkrEthApr = pool.dynamicData.totalLiquidity > 0 ? ankrEthBaseApr * ankrEthPercentage : 0; - const userApr = - pool.type === 'META_STABLE' - ? poolAnkrEthApr * (1 - networkContext.data.balancer.swapProtocolFeePercentage) - : poolAnkrEthApr * (1 - protocolYieldFeePercentage); - operations.push( - prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: `${pool.id}-ankreth-apr`, chain: networkContext.chain } }, - update: { apr: collectsYieldFee(pool) ? userApr : poolAnkrEthApr }, - create: { - id: `${pool.id}-ankreth-apr`, - chain: networkContext.chain, - poolId: pool.id, - apr: collectsYieldFee(pool) ? userApr : poolAnkrEthApr, - title: 'ankrETH APR', - type: 'IB_YIELD', - }, - }), - ); - } - } - await Promise.all(operations); - } -} diff --git a/modules/pool/lib/apr-data-sources/fantom/ankr-staked-ftm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/ankr-staked-ftm-apr.service.ts deleted file mode 100644 index 38b54573b..000000000 --- a/modules/pool/lib/apr-data-sources/fantom/ankr-staked-ftm-apr.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import axios from 'axios'; -import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import { networkContext } from '../../../../network/network-context.service'; -import { TokenService } from '../../../../token/token.service'; -import { PoolAprService } from '../../../pool-types'; -import { collectsYieldFee } from '../../pool-utils'; -import { liquidStakedBaseAprService } from '../liquid-staked-base-apr.service'; - -export class AnkrStakedFtmAprService implements PoolAprService { - constructor(private readonly tokenService: TokenService, private readonly ankrFtmAddress: string) {} - - public getAprServiceName(): string { - return 'AnkrStakedFtmAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - const ankrFtmPrice = this.tokenService.getPriceForToken(tokenPrices, this.ankrFtmAddress); - - const ankrFtmBaseApr = await liquidStakedBaseAprService.getAnkrFtmBaseApr(); - - let operations: any[] = []; - for (const pool of pools) { - const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee - ? parseFloat(pool.dynamicData.protocolYieldFee) - : networkContext.data.balancer.yieldProtocolFeePercentage; - const ankrFtmToken = pool.tokens.find((token) => token.address === this.ankrFtmAddress); - const ankrFtmTokenBalance = ankrFtmToken?.dynamicData?.balance; - - if (ankrFtmTokenBalance && pool.dynamicData) { - const ankrFtmPercentage = - (parseFloat(ankrFtmTokenBalance) * ankrFtmPrice) / pool.dynamicData.totalLiquidity; - const poolAnkrFtmApr = pool.dynamicData.totalLiquidity > 0 ? ankrFtmBaseApr * ankrFtmPercentage : 0; - - const userApr = - pool.type === 'META_STABLE' - ? poolAnkrFtmApr * (1 - networkContext.data.balancer.swapProtocolFeePercentage) - : poolAnkrFtmApr * (1 - protocolYieldFeePercentage); - - operations.push( - prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: `${pool.id}-ankrftm-apr`, chain: networkContext.chain } }, - update: { apr: collectsYieldFee(pool) ? userApr : poolAnkrFtmApr }, - create: { - id: `${pool.id}-ankrftm-apr`, - chain: networkContext.chain, - poolId: pool.id, - apr: collectsYieldFee(pool) ? userApr : poolAnkrFtmApr, - title: 'ankrFTM APR', - type: 'IB_YIELD', - }, - }), - ); - } - } - await Promise.all(operations); - } -} diff --git a/modules/pool/lib/apr-data-sources/fantom/stader-staked-ftm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/stader-staked-ftm-apr.service.ts deleted file mode 100644 index c1f2905f4..000000000 --- a/modules/pool/lib/apr-data-sources/fantom/stader-staked-ftm-apr.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import { TokenService } from '../../../../token/token.service'; -import { PoolAprService } from '../../../pool-types'; -import { networkContext } from '../../../../network/network-context.service'; -import { collectsYieldFee } from '../../pool-utils'; -import { liquidStakedBaseAprService } from '../liquid-staked-base-apr.service'; - -export class StaderStakedFtmAprService implements PoolAprService { - constructor(private readonly tokenService: TokenService, private readonly sftmxAddress: string) {} - - public getAprServiceName(): string { - return 'StaderStakedFtmAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - const sftmxPrice = this.tokenService.getPriceForToken(tokenPrices, this.sftmxAddress); - const sftmxBaseApr = await liquidStakedBaseAprService.getSftmxBaseApr(); - - let operations: any[] = []; - - for (const pool of pools) { - const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee - ? parseFloat(pool.dynamicData.protocolYieldFee) - : networkContext.data.balancer.yieldProtocolFeePercentage; - const sftmxToken = pool.tokens.find((token) => token.address === this.sftmxAddress); - const sftmxTokenBalance = sftmxToken?.dynamicData?.balance; - - if (sftmxTokenBalance && pool.dynamicData) { - const sftmxPercentage = (parseFloat(sftmxTokenBalance) * sftmxPrice) / pool.dynamicData.totalLiquidity; - const sftmxApr = pool.dynamicData.totalLiquidity > 0 ? sftmxBaseApr * sftmxPercentage : 0; - - const userApr = - pool.type === 'META_STABLE' - ? sftmxApr * (1 - networkContext.data.balancer.swapProtocolFeePercentage) - : sftmxApr * (1 - protocolYieldFeePercentage); - - operations.push( - prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: `${pool.id}-sftmx-apr`, chain: networkContext.chain } }, - update: { apr: collectsYieldFee(pool) ? userApr : sftmxApr }, - create: { - id: `${pool.id}-sftmx-apr`, - chain: networkContext.chain, - poolId: pool.id, - apr: collectsYieldFee(pool) ? userApr : sftmxApr, - title: 'sFTMx APR', - type: 'IB_YIELD', - }, - }), - ); - } - } - await Promise.all(operations); - } -} diff --git a/modules/pool/lib/apr-data-sources/fantom/tarot-vault-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/tarot-vault-apr.service.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/modules/pool/lib/apr-data-sources/fantom/yearn-vault-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/yearn-vault-apr.service.ts deleted file mode 100644 index f82692300..000000000 --- a/modules/pool/lib/apr-data-sources/fantom/yearn-vault-apr.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { PoolAprService } from '../../../pool-types'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import axios from 'axios'; -import { prisma } from '../../../../../prisma/prisma-client'; -import { TokenService } from '../../../../token/token.service'; -import { YearnVault } from '../apr-types'; -import { networkContext } from '../../../../network/network-context.service'; - -export class YearnVaultAprService implements PoolAprService { - constructor(private readonly tokenService: TokenService, private readonly vaultsEndpoint: string) {} - - public getAprServiceName(): string { - return 'YearnVaultAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const { data } = await axios.get(this.vaultsEndpoint); - const tokenPrices = await this.tokenService.getTokenPrices(); - - for (const pool of pools) { - const itemId = `${pool.id}-yearn-vault`; - - if (!pool.linearData || !pool.dynamicData) { - continue; - } - - const linearData = pool.linearData; - const wrappedToken = pool.tokens[linearData.wrappedIndex]; - const mainToken = pool.tokens[linearData.mainIndex]; - - const vault = data.find((vault) => vault.address.toLowerCase() === wrappedToken.address.toLowerCase()); - - if (!vault) { - continue; - } - - const tokenPrice = this.tokenService.getPriceForToken(tokenPrices, mainToken.address); - const wrappedTokens = parseFloat(wrappedToken.dynamicData?.balance || '0'); - const priceRate = parseFloat(wrappedToken.dynamicData?.priceRate || '1.0'); - const poolWrappedLiquidity = wrappedTokens * priceRate * tokenPrice; - const totalLiquidity = pool.dynamicData.totalLiquidity; - const apr = totalLiquidity > 0 ? vault.apy.net_apy * (poolWrappedLiquidity / totalLiquidity) : 0; - - await prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - create: { - id: itemId, - chain: networkContext.chain, - poolId: pool.id, - title: `${vault.symbol} APR`, - apr, - group: 'YEARN', - type: 'LINEAR_BOOSTED', - }, - update: { apr, group: 'YEARN', type: 'LINEAR_BOOSTED' }, - }); - } - } -} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/ib-linear-apr-handlers.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/ib-linear-apr-handlers.ts new file mode 100644 index 000000000..0bf9305eb --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/ib-linear-apr-handlers.ts @@ -0,0 +1,158 @@ +import { AaveAprHandler } from './sources/aave-apr-handler'; +import { AnkrAprHandler } from './sources/ankr-apr-handler'; +import { DefaultAprHandler } from './sources/default-apr-handler'; +import { EulerAprHandler } from './sources/euler-apr-handler'; +import { GearboxAprHandler } from './sources/gearbox-apr-handler'; +import { IdleAprHandler } from './sources/idle-apr-handler'; +import { OvixAprHandler } from './sources/ovix-apr-handler'; +import { TesseraAprHandler } from './sources/tessera-apr-handler'; +import { TetuAprHandler } from './sources/tetu-apr-handler'; +import { TranchessAprHandler } from './sources/tranchess-apr-handler'; +import { YearnAprHandler } from './sources/yearn-apr-handler'; +import { ReaperCryptAprHandler } from './sources/reaper-crypt-apr-handler'; +import { BeefyAprHandler } from './sources/beefy-apr-handler'; +import { IbAprConfig } from '../../../../network/apr-config-types'; + +export class IbLinearAprHandlers { + private handlers: AprHandler[] = []; + fixedAprTokens?: { [tokenName: string]: { address: string; apr: number; group?: string; isIbYield?: boolean } }; + + constructor(aprConfig: IbAprConfig) { + this.handlers = this.buildAprHandlers(aprConfig); + this.fixedAprTokens = aprConfig.fixedAprHandler; + } + + buildAprHandlers(aprConfig: IbAprConfig) { + const handlers: AprHandler[] = []; + if (aprConfig.aave) { + for (const config of Object.values(aprConfig.aave)) { + const aaveHandler = new AaveAprHandler(config); + handlers.push(aaveHandler); + } + } + if (aprConfig.ankr) { + const ankrHandler = new AnkrAprHandler(aprConfig.ankr); + handlers.push(ankrHandler); + } + if (aprConfig.beefy) { + const beefyHandler = new BeefyAprHandler(aprConfig.beefy); + handlers.push(beefyHandler); + } + if (aprConfig.euler) { + const eulerHandler = new EulerAprHandler(aprConfig.euler); + handlers.push(eulerHandler); + } + if (aprConfig.gearbox) { + const gearboxHandler = new GearboxAprHandler(aprConfig.gearbox); + handlers.push(gearboxHandler); + } + if (aprConfig.idle) { + const idleHandler = new IdleAprHandler(aprConfig.idle); + handlers.push(idleHandler); + } + if (aprConfig.ovix) { + const ovixHandler = new OvixAprHandler({ + ...aprConfig.ovix, + }); + handlers.push(ovixHandler); + } + if (aprConfig.reaper) { + const reaperCryptHandler = new ReaperCryptAprHandler({ ...aprConfig.reaper }); + handlers.push(reaperCryptHandler); + } + if (aprConfig.tessera) { + const tesseraHandler = new TesseraAprHandler({ + ...aprConfig.tessera, + }); + handlers.push(tesseraHandler); + } + if (aprConfig.tetu) { + const tetuHandler = new TetuAprHandler(aprConfig.tetu); + handlers.push(tetuHandler); + } + if (aprConfig.tranchess) { + const tranchessHandler = new TranchessAprHandler(aprConfig.tranchess); + handlers.push(tranchessHandler); + } + if (aprConfig.yearn) { + const yearnHandler = new YearnAprHandler(aprConfig.yearn); + handlers.push(yearnHandler); + } + if (aprConfig.defaultHandlers) { + for (const handlerConfig of Object.values(aprConfig.defaultHandlers)) { + const handler = new DefaultAprHandler(handlerConfig); + handlers.push(handler); + } + } + return handlers; + } + + // Any IB Yield tokens (such as rETH, wstETH) need to be added here. Linear Wrapped Tokens must NOT be added here. + buildIbYieldTokens(aprConfig: IbAprConfig): string[] { + const ibYieldTokenNamesForDefaultHandler = [ + 'rEth', + 'stETH', + 'wstETH', + 'cbETH', + 'sfrxETH', + 'USDR', + 'swETH', + 'wjAURA', + 'qETH', + 'ankrETH', + 'ankrFTM', + 'sFTMx', + 'stMATIC', + 'MATICX', + 'wbETH', + ].map((token) => token.toLowerCase()); + + return [ + ...Object.values(aprConfig?.ankr?.tokens || {}).map((token) => token.address), + ...Object.keys(aprConfig?.defaultHandlers || {}).filter((handler) => + ibYieldTokenNamesForDefaultHandler.includes(handler.toLowerCase()), + ), + ...Object.keys(aprConfig?.fixedAprHandler || {}).filter((handler) => + ibYieldTokenNamesForDefaultHandler.includes(handler.toLowerCase()), + ), + ]; + } + + async fetchAprsFromAllHandlers(): Promise { + let aprs: TokenApr[] = []; + for (const handler of this.handlers) { + const fetchedResponse: { [key: string]: { apr: number; isIbYield: boolean } } = await handler.getAprs(); + for (const [address, { apr, isIbYield }] of Object.entries(fetchedResponse)) { + aprs.push({ + apr, + isIbYield, + group: handler.group, + address, + }); + } + } + if (this.fixedAprTokens) { + for (const { address, apr, isIbYield, group } of Object.values(this.fixedAprTokens)) { + aprs.push({ + apr, + isIbYield: isIbYield ?? false, + group, + address, + }); + } + } + return aprs; + } +} + +export interface AprHandler { + group: string | undefined; + getAprs(): Promise<{ [tokenAddress: string]: { apr: number; isIbYield: boolean } }>; +} + +export type TokenApr = { + apr: number; + address: string; + group?: string; + isIbYield: boolean; +}; diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/aave-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/aave-apr-handler.ts new file mode 100644 index 000000000..f2b72156b --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/aave-apr-handler.ts @@ -0,0 +1,107 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { AprHandler } from '../ib-linear-apr-handlers'; + +export class AaveAprHandler implements AprHandler { + tokens: { + [assetName: string]: { + underlyingAssetAddress: string; + aTokenAddress: string; + wrappedTokens: { + [tokenName: string]: string; + }; + isIbYield?: boolean; + }; + }; + subgraphUrl: string; + + readonly group = 'AAVE'; + + readonly query = `query getReserves($aTokens: [String!], $underlyingAssets: [Bytes!]) { + reserves( + where: { + aToken_in: $aTokens + underlyingAsset_in: $underlyingAssets + isActive: true + } + ) { + id + underlyingAsset + liquidityRate + } + }`; + + constructor(aprHandlerConfig: { + subgraphUrl: string; + tokens: { + [underlyingAssetName: string]: { + underlyingAssetAddress: string; + aTokenAddress: string; + wrappedTokens: { + [wrappedTokenName: string]: string; + }; + isIbYield?: boolean; + }; + }; + }) { + this.tokens = aprHandlerConfig.tokens; + this.subgraphUrl = aprHandlerConfig.subgraphUrl; + } + + async getAprs() { + try { + const requestQuery = { + operationName: 'getReserves', + query: this.query, + variables: { + aTokens: Object.values(this.tokens).map(({ aTokenAddress }) => aTokenAddress), + underlyingAssets: Object.values(this.tokens).map( + ({ underlyingAssetAddress }) => underlyingAssetAddress, + ), + }, + }; + const { data } = await axios({ + url: this.subgraphUrl, + method: 'post', + data: requestQuery, + headers: { 'Content-Type': 'application/json' }, + }); + const { + data: { reserves }, + } = data as ReserveResponse; + + const aprsByUnderlyingAddress = Object.fromEntries( + reserves.map((r) => [ + r.underlyingAsset, + // Converting from aave ray number (27 digits) to float + Number(r.liquidityRate.slice(0, 27)) / 1e27, + ]), + ); + const aprEntries = Object.values(this.tokens) + .map(({ wrappedTokens, underlyingAssetAddress, isIbYield }) => { + const apr = aprsByUnderlyingAddress[underlyingAssetAddress]; + return Object.values(wrappedTokens).map((wrappedTokenAddress) => ({ + [wrappedTokenAddress]: { apr, isIbYield: isIbYield ?? false }, + })); + }) + .flat() + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); + return aprEntries; + } catch (e) { + console.error(`Failed to fetch Aave APR in subgraph ${this.subgraphUrl}:`, e); + Sentry.captureException(`Aave IB APR handler failed: ${e}`); + return {}; + } + } +} + +interface ReserveResponse { + data: { + reserves: [ + { + underlyingAsset: string; + liquidityRate: string; + }, + ]; + }; +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/ReaperCrypt.json b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/ReaperCrypt.json new file mode 100644 index 000000000..5ea0973e1 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/ReaperCrypt.json @@ -0,0 +1,329 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_token", "type": "address" }, + { "internalType": "string", "name": "_name", "type": "string" }, + { "internalType": "string", "name": "_symbol", "type": "string" }, + { "internalType": "uint256", "name": "_depositFee", "type": "uint256" }, + { "internalType": "uint256", "name": "_tvlCap", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "total", "type": "uint256" } + ], + "name": "DepositsIncremented", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "address", "name": "user", "type": "address" }], + "name": "TermsAccepted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "uint256", "name": "newTvlCap", "type": "uint256" }], + "name": "TvlCapUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "user", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "total", "type": "uint256" } + ], + "name": "WithdrawalsIncremented", + "type": "event" + }, + { + "inputs": [], + "name": "PERCENT_DIVISOR", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "agreeToTerms", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "available", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "balance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "constructionTime", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "cumulativeDeposits", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "cumulativeWithdrawals", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "depositAll", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "depositFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "earn", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "getPricePerFullShare", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "hasReadAndAcceptedTerms", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "_token", "type": "address" }], + "name": "inCaseTokensGetStuck", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "_strategy", "type": "address" }], + "name": "initialize", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "initialized", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "removeTvlCap", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "strategy", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tvlCap", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "fee", "type": "uint256" }], + "name": "updateDepositFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_newTvlCap", "type": "uint256" }], + "name": "updateTvlCap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_shares", "type": "uint256" }], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "withdrawAll", "outputs": [], "stateMutability": "nonpayable", "type": "function" } +] diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/ReaperCryptStrategy.json b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/ReaperCryptStrategy.json new file mode 100644 index 000000000..2a29a1f41 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/ReaperCryptStrategy.json @@ -0,0 +1,586 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_vault", "type": "address" }, + { "internalType": "address[]", "name": "_feeRemitters", "type": "address[]" }, + { "internalType": "address[]", "name": "_strategists", "type": "address[]" }, + { "internalType": "address[]", "name": "_multisigRoles", "type": "address[]" }, + { "internalType": "contract IAToken", "name": "_aWant", "type": "address" }, + { "internalType": "address[]", "name": "_opToWantPath", "type": "address[]" }, + { "internalType": "uint256", "name": "_targetLtv", "type": "uint256" }, + { "internalType": "uint256", "name": "_maxLtv", "type": "uint256" }, + { "internalType": "uint8", "name": "_eModeCategory", "type": "uint8" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "newCallFee", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "newTreasuryFee", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "newStrategistFee", "type": "uint256" } + ], + "name": "FeesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "address", "name": "account", "type": "address" }], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "harvester", "type": "address" }], + "name": "StratHarvest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "address", "name": "newStrategistRemitter", "type": "address" }], + "name": "StrategistRemitterUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "uint256", "name": "newFee", "type": "uint256" }], + "name": "TotalFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "address", "name": "account", "type": "address" }], + "name": "Unpaused", + "type": "event" + }, + { + "inputs": [], + "name": "AAVE_ADDRESSES_PROVIDER", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "AAVE_DATA_PROVIDER", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "AAVE_REWARDS_CONTROLLER", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ADDRESSES_PROVIDER", + "outputs": [{ "internalType": "contract IPoolAddressesProvider", "name": "", "type": "address" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "ADMIN", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GUARDIAN", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "KEEPER", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_FEE", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ONE_YEAR", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OP", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERCENT_DIVISOR", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "POOL", + "outputs": [{ "internalType": "contract IPool", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "STRATEGIST", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "STRATEGIST_MAX_FEE", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "USDC", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VELO_ROUTER", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "aWant", + "outputs": [{ "internalType": "contract IAToken", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }], + "name": "authorizedDelever", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }], + "name": "authorizedSendToVault", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }], + "name": "authorizedWithdrawUnderlying", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "int256", "name": "_n", "type": "int256" }], + "name": "averageAPRAcrossLastNHarvests", + "outputs": [{ "internalType": "int256", "name": "", "type": "int256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_startIndex", "type": "uint256" }, + { "internalType": "uint256", "name": "_endIndex", "type": "uint256" } + ], + "name": "calculateAPRUsingLogs", + "outputs": [{ "internalType": "int256", "name": "", "type": "int256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "callFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "deposit", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { "internalType": "address[]", "name": "", "type": "address[]" }, + { "internalType": "uint256[]", "name": "", "type": "uint256[]" }, + { "internalType": "uint256[]", "name": "", "type": "uint256[]" }, + { "internalType": "address", "name": "initiator", "type": "address" }, + { "internalType": "bytes", "name": "", "type": "bytes" } + ], + "name": "executeOperation", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleAdmin", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "getRoleMember", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleMemberCount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSupplyAndBorrow", + "outputs": [ + { "internalType": "uint256", "name": "supply", "type": "uint256" }, + { "internalType": "uint256", "name": "borrow", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "harvest", + "outputs": [{ "internalType": "uint256", "name": "callerFee", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "harvestLog", + "outputs": [ + { "internalType": "uint256", "name": "timestamp", "type": "uint256" }, + { "internalType": "uint256", "name": "vaultSharePrice", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "harvestLogCadence", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "harvestLogLength", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "hasRole", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastHarvestTimestamp", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLtv", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minLeverageAmount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "opToUsdcPath", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "opToWantPath", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "panic", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [], "name": "pause", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "rewardClaimingTokens", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_newTargetLtv", "type": "uint256" }, + { "internalType": "uint256", "name": "_newMaxLtv", "type": "uint256" } + ], + "name": "setLTVs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "_newWithdrawSlippageTolerance", "type": "uint256" }, + { "internalType": "uint256", "name": "_newMinLeverageAmount", "type": "uint256" } + ], + "name": "setLeverageParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address[]", "name": "_path", "type": "address[]" }], + "name": "setOpToUsdcPath", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address[]", "name": "_path", "type": "address[]" }], + "name": "setOpToWantPath", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "strategistFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "strategistRemitter", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "targetLtv", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "treasury", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "treasuryFee", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "unpause", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { "internalType": "uint256", "name": "_callFee", "type": "uint256" }, + { "internalType": "uint256", "name": "_treasuryFee", "type": "uint256" }, + { "internalType": "uint256", "name": "_strategistFee", "type": "uint256" } + ], + "name": "updateFees", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_newCadenceInSeconds", "type": "uint256" }], + "name": "updateHarvestLogCadence", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "_newStrategistRemitter", "type": "address" }], + "name": "updateStrategistRemitter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_totalFee", "type": "uint256" }], + "name": "updateTotalFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newTreasury", "type": "address" }], + "name": "updateTreasury", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "want", + "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawSlippageTolerance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/oErc20.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/oErc20.ts new file mode 100644 index 000000000..6dde7bf68 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/oErc20.ts @@ -0,0 +1,15 @@ +export const abi = [ + { + inputs: [], + name: "borrowRatePerTimestamp", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + } +] as const diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/reaperStrategy.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/reaperStrategy.ts new file mode 100644 index 000000000..1a89f3754 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/reaperStrategy.ts @@ -0,0 +1,21 @@ +export const abi = [ + { + inputs: [ + { + internalType: 'int256', + name: '_n', + type: 'int256', + }, + ], + name: 'averageAPRAcrossLastNHarvests', + outputs: [ + { + internalType: 'int256', + name: '', + type: 'int256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/tesseraPool.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/tesseraPool.ts new file mode 100644 index 000000000..a3fd8bb01 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/abis/tesseraPool.ts @@ -0,0 +1,154 @@ +export const abi = [ + { + inputs: [], + name: 'getPoolsUI', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'stakedAmount', type: 'uint256' }, + { + components: [ + { + internalType: 'uint48', + name: 'startTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'endTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint96', + name: 'rewardsPerHour', + type: 'uint96', + }, + { + internalType: 'uint96', + name: 'capPerPosition', + type: 'uint96', + }, + ], + internalType: 'struct ApeCoinStaking.TimeRange', + name: 'currentTimeRange', + type: 'tuple', + }, + ], + internalType: 'struct ApeCoinStaking.PoolUI', + name: '', + type: 'tuple', + }, + { + components: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'stakedAmount', type: 'uint256' }, + { + components: [ + { + internalType: 'uint48', + name: 'startTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'endTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint96', + name: 'rewardsPerHour', + type: 'uint96', + }, + { + internalType: 'uint96', + name: 'capPerPosition', + type: 'uint96', + }, + ], + internalType: 'struct ApeCoinStaking.TimeRange', + name: 'currentTimeRange', + type: 'tuple', + }, + ], + internalType: 'struct ApeCoinStaking.PoolUI', + name: '', + type: 'tuple', + }, + { + components: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'stakedAmount', type: 'uint256' }, + { + components: [ + { + internalType: 'uint48', + name: 'startTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'endTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint96', + name: 'rewardsPerHour', + type: 'uint96', + }, + { + internalType: 'uint96', + name: 'capPerPosition', + type: 'uint96', + }, + ], + internalType: 'struct ApeCoinStaking.TimeRange', + name: 'currentTimeRange', + type: 'tuple', + }, + ], + internalType: 'struct ApeCoinStaking.PoolUI', + name: '', + type: 'tuple', + }, + { + components: [ + { internalType: 'uint256', name: 'poolId', type: 'uint256' }, + { internalType: 'uint256', name: 'stakedAmount', type: 'uint256' }, + { + components: [ + { + internalType: 'uint48', + name: 'startTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'endTimestampHour', + type: 'uint48', + }, + { + internalType: 'uint96', + name: 'rewardsPerHour', + type: 'uint96', + }, + { + internalType: 'uint96', + name: 'capPerPosition', + type: 'uint96', + }, + ], + internalType: 'struct ApeCoinStaking.TimeRange', + name: 'currentTimeRange', + type: 'tuple', + }, + ], + internalType: 'struct ApeCoinStaking.PoolUI', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/ankr-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/ankr-apr-handler.ts new file mode 100644 index 000000000..2f89875ed --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/ankr-apr-handler.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; +import { AprHandler } from '../ib-linear-apr-handlers'; +import { AnkrAprConfig } from '../../../../../network/apr-config-types'; + +export class AnkrAprHandler implements AprHandler { + tokens: { + [underlyingAssetName: string]: { + address: string; + serviceName: string; + isIbYield?: boolean; + }; + }; + url: string; + readonly group = undefined; + + constructor(aprHandlerConfig: AnkrAprConfig) { + this.tokens = aprHandlerConfig.tokens; + this.url = aprHandlerConfig.sourceUrl; + } + + async getAprs(): Promise<{ [tokenAddress: string]: { apr: number; isIbYield: boolean } }> { + try { + const { data } = await axios.get(this.url); + const services = (data as { services: { serviceName: string; apy: string }[] }).services; + const aprs = Object.fromEntries( + Object.values(this.tokens).map(({ address, serviceName, isIbYield }) => { + const service = services.find((service) => service.serviceName === serviceName); + if (!service) { + return [address, 0]; + } + return [address, { apr: parseFloat(service.apy) / 1e2, isIbYield: isIbYield ?? false }]; + }), + ); + return aprs; + } catch (error) { + console.error('Failed to fetch Ankr APR:', error); + Sentry.captureException(`Ankr IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/beefy-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/beefy-apr-handler.ts new file mode 100644 index 000000000..1a6e50fef --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/beefy-apr-handler.ts @@ -0,0 +1,49 @@ +import { BeefyAprConfig } from '../../../../../network/apr-config-types'; +import { AprHandler } from '../ib-linear-apr-handlers'; +import axios from 'axios'; +import * as Sentry from '@sentry/node'; + +export class BeefyAprHandler implements AprHandler { + tokens: { + [tokenName: string]: { + address: string; + vaultId: string; + isIbYield?: boolean; + }; + }; + sourceUrl: string; + group: string | undefined = 'BEEFY'; + + constructor(aprConfig: BeefyAprConfig) { + this.tokens = aprConfig.tokens; + this.sourceUrl = aprConfig.sourceUrl; + } + + async getAprs(): Promise<{ [p: string]: { apr: number; isIbYield: boolean } }> { + try { + const { data: aprData } = await axios.get(this.sourceUrl); + const aprs: { [tokenAddress: string]: { apr: number; isIbYield: boolean } } = {}; + for (const { address, vaultId, isIbYield } of Object.values(this.tokens)) { + aprs[address] = { apr: aprData[vaultId].vaultApr, isIbYield: isIbYield ?? false }; + } + return aprs; + } catch (error) { + console.error(`Beefy IB APR hanlder failed: `, error); + Sentry.captureException(`Beefy IB APR handler failed: ${error}`); + return {}; + } + } +} + +type VaultApr = Record< + string, + { + vaultApr: number; + compoundingsPerYear: number; + beefyPerformanceFee: number; + vaultApy: number; + lpFee: number; + tradingApr: number; + totalApy: number; + } +>; diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/default-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/default-apr-handler.ts new file mode 100644 index 000000000..e48dc0b4e --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/default-apr-handler.ts @@ -0,0 +1,55 @@ +import axios from 'axios'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import * as Sentry from '@sentry/node'; + +export class DefaultAprHandler implements AprHandler { + tokenAddress: string; + url: string; + path: string; + scale: number; + group: string | undefined = undefined; + isIbYield: boolean | undefined; + + constructor(aprHandlerConfig: { + sourceUrl: string; + tokenAddress: string; + path?: string; + scale?: number; + group?: string; + isIbYield?: boolean; + }) { + this.tokenAddress = aprHandlerConfig.tokenAddress; + this.url = aprHandlerConfig.sourceUrl; + this.path = aprHandlerConfig.path ?? ''; + this.scale = aprHandlerConfig.scale ?? 100; + this.group = aprHandlerConfig.group; + this.isIbYield = aprHandlerConfig.isIbYield; + } + + async getAprs() { + try { + const { data } = await axios.get(this.url, { headers: { 'User-Agent': 'cf' } }); + const value = this.path === '' ? data : this.getValueFromPath(data, this.path); + const scaledValue = parseFloat(value) / this.scale; + + return { [this.tokenAddress]: { apr: scaledValue, isIbYield: this.isIbYield ?? false } }; + } catch (error) { + console.error(`Failed to fetch APRs in url ${this.url}:`, error); + Sentry.captureException(`Failed to fetch default IB APRs in url ${this.url}: ${error}`); + return {}; + } + } + + getValueFromPath(obj: any, path: string) { + if (path === '') { + return obj; + } + const parts = path.split('.'); + let value = obj; + for (const part of parts) { + value = value[part]; + } + return value; + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/euler-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/euler-apr-handler.ts new file mode 100644 index 000000000..f6edc5479 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/euler-apr-handler.ts @@ -0,0 +1,83 @@ +import axios from 'axios'; +import { AprHandler } from '../ib-linear-apr-handlers'; +import { EulerAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +export class EulerAprHandler implements AprHandler { + tokens: { + [key: string]: { + address: string; + isIbYield?: boolean; + }; + }; + subgraphUrl: string; + readonly group = 'EULER'; + + readonly query = ` + query getAssetsAPY($eTokenAddress_in: [String!]) { + assets( + where: { + eTokenAddress_in: $eTokenAddress_in + } + ) { + eTokenAddress + supplyAPY + } + } +`; + + constructor(aprHandlerConfig: EulerAprConfig) { + this.tokens = aprHandlerConfig.tokens; + this.subgraphUrl = aprHandlerConfig.subgraphUrl; + } + + async getAprs() { + try { + const requestQuery = { + operationName: 'getAssetsAPY', + query: this.query, + variables: { + eTokenAddress_in: Object.values(this.tokens).map(({ address }) => address), + }, + }; + + const { data } = await axios({ + url: this.subgraphUrl, + method: 'POST', + data: JSON.stringify(requestQuery), + }); + + const { + data: { assets }, + } = data as EulerResponse; + + const aprEntries = assets.map(({ eTokenAddress, supplyAPY }) => [ + eTokenAddress.toLowerCase(), + // supplyAPY is 1e27 and apr is in bps (1e4), so all we need is to format to 1e23 + { + apr: Number(supplyAPY.slice(0, 27)) / 1e27, + isIbYield: + Object.values(this.tokens).find( + ({ address }) => address.toLowerCase() === eTokenAddress.toLowerCase(), + )?.isIbYield ?? false, + }, + ]); + + return Object.fromEntries(aprEntries); + } catch (error) { + console.error(`Euler IB APR handler failed: `, error); + Sentry.captureException(`Euler IB APR handler failed: ${error}`); + } + } +} + +interface EulerResponse { + data: { + assets: [ + { + eTokenAddress: string; + supplyAPY: string; + }, + ]; + }; +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/gearbox-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/gearbox-apr-handler.ts new file mode 100644 index 000000000..96e77624d --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/gearbox-apr-handler.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/node'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import { GearBoxAprConfig } from '../../../../../network/apr-config-types'; + +export class GearboxAprHandler implements AprHandler { + url: string; + tokens: { [key: string]: { address: string; isIbYield?: boolean } }; + readonly group = 'GEARBOX'; + + constructor(aprHandlerConfig: GearBoxAprConfig) { + this.tokens = aprHandlerConfig.tokens; + this.url = aprHandlerConfig.sourceUrl; + } + + async getAprs() { + try { + const { data } = await axios.get(this.url); + const json = data as { data: { dieselToken: string; depositAPY_RAY: string }[] }; + + const aprEntries = json.data + .filter((t) => + Object.values(this.tokens) + .map(({ address }) => address) + .includes(t.dieselToken.toLowerCase()), + ) + .map(({ dieselToken, depositAPY_RAY }) => { + const tokenObj = Object.values(this.tokens).find( + ({ address }) => address === dieselToken.toLowerCase(), + ); + return [ + dieselToken, + { apr: Number(depositAPY_RAY.slice(0, 27)) / 1e27, isIbYield: tokenObj?.isIbYield ?? false }, + ]; + }); + return Object.fromEntries(aprEntries); + } catch (error) { + console.error('Failed to fetch Gearbox APR:', error); + Sentry.captureException(`Gearbox IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/idle-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/idle-apr-handler.ts new file mode 100644 index 000000000..3782bf6e4 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/idle-apr-handler.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import { IdleAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +export class IdleAprHandler implements AprHandler { + tokens: { + [tokenName: string]: { + address: string; + wrapped4626Address: string; + isIbYield?: boolean; + }; + }; + url: string; + authorizationHeader: string; + readonly group = 'IDLE'; + + constructor(aprHandlerConfig: IdleAprConfig) { + this.tokens = aprHandlerConfig.tokens; + this.url = aprHandlerConfig.sourceUrl; + this.authorizationHeader = aprHandlerConfig.authorizationHeader; + } + + async getAprs() { + try { + const aprPromises = Object.values(this.tokens).map(async ({ address, wrapped4626Address, isIbYield }) => { + const { data } = await axios.get([this.url, address, '?isRisk=false&order=desc&limit=1'].join(''), { + headers: { + Authorization: this.authorizationHeader, + }, + }); + const [json] = data as { idleRate: string }[]; + const value = Number(json.idleRate) / 1e20; + return [wrapped4626Address, { apr: value, isIbYield: isIbYield ?? false }]; + }); + const res = Array(Object.keys(this.tokens).length); + for (const [index, aprPromise] of aprPromises.entries()) { + res[index] = await aprPromise; + } + return Object.fromEntries(res); + } catch (error) { + console.error('Failed to fetch Idle APR:', error); + Sentry.captureException(`Idle IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/ovix-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/ovix-apr-handler.ts new file mode 100644 index 000000000..3f3d0a7e1 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/ovix-apr-handler.ts @@ -0,0 +1,44 @@ +import { BigNumber, Contract } from 'ethers'; +import { abi } from './abis/oErc20'; +import * as Sentry from '@sentry/node'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import { networkContext } from '../../../../../network/network-context.service'; +import { OvixAprConfig } from '../../../../../network/apr-config-types'; + +export class OvixAprHandler implements AprHandler { + tokens: { + [tokenName: string]: { + yieldAddress: string; + wrappedAddress: string; + isIbYield?: boolean; + }; + }; + readonly group = 'OVIX'; + + constructor(aprHandlerConfig: OvixAprConfig) { + this.tokens = aprHandlerConfig.tokens; + } + + async getAprs() { + try { + const aprEntries = Object.values(this.tokens).map(async ({ yieldAddress, wrappedAddress, isIbYield }) => { + const contract = new Contract(yieldAddress, abi, networkContext.provider); + const borrowRate = await contract.borrowRatePerTimestamp(); + return [ + wrappedAddress, + { + apr: Math.pow(1 + (borrowRate as BigNumber).toNumber() / 1e18, 365 * 24 * 60 * 60) - 1, + isIbYield: isIbYield ?? false, + }, + ]; + }); + + return Object.fromEntries(await Promise.all(aprEntries)); + } catch (error) { + console.error('Failed to fetch Ovix APR:', error); + Sentry.captureException(`Ovix IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/reaper-crypt-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/reaper-crypt-apr-handler.ts new file mode 100644 index 000000000..a9d0196d7 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/reaper-crypt-apr-handler.ts @@ -0,0 +1,147 @@ +import { AprHandler } from '../ib-linear-apr-handlers'; +import { getContractAt } from '../../../../../web3/contract'; +import ReaperCryptStrategyAbi from './abis/ReaperCryptStrategy.json'; +import axios from 'axios'; +import ReaperCryptAbi from './abis/ReaperCrypt.json'; +import { ReaperAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +const APR_PERCENT_DIVISOR = 10_000; + +const sFTMxBaseApr = 0.046; +export class ReaperCryptAprHandler implements AprHandler { + tokensWithSubgraphSource?: { + [tokenName: string]: { + address: string; + isSftmX?: boolean; + isWstETH?: boolean; + isIbYield?: boolean; + }; + }; + tokensWithOnChainSource?: { + [tokenName: string]: { + address: string; + isSftmX?: boolean; + isWstETH?: boolean; + isIbYield?: boolean; + }; + }; + subgraphUrl?: string; + averageAPRAcrossLastNHarvests?: number; + wstETHBaseApr: number = 0; + + readonly query = `query getVaults($ids: [ID!]) { + vaults(where:{id_in: $ids}){ + id + apr + } + }`; + readonly group = 'REAPER'; + + constructor(aprConfig: ReaperAprConfig) { + this.tokensWithSubgraphSource = aprConfig.subgraphSource?.tokens; + this.tokensWithOnChainSource = aprConfig.onchainSource?.tokens; + this.subgraphUrl = aprConfig.subgraphSource?.subgraphUrl; + this.averageAPRAcrossLastNHarvests = aprConfig.onchainSource?.averageAPRAcrossLastNHarvests; + } + + async getAprs(): Promise<{ [p: string]: { apr: number; isIbYield: boolean } }> { + let multiStrategyAprs = {}; + let singleStrategyAprs = {}; + this.wstETHBaseApr = await this.getWstEthBaseApr(); + if (this.tokensWithSubgraphSource !== undefined) { + multiStrategyAprs = await this.getAprFromSubgraph(this.tokensWithSubgraphSource); + } + if (this.tokensWithOnChainSource !== undefined) { + singleStrategyAprs = await this.getOnChainCryptApr(this.tokensWithOnChainSource); + } + return { ...multiStrategyAprs, ...singleStrategyAprs }; + } + + private async getOnChainCryptApr(tokens: { + [tokenName: string]: { address: string; isSftmX?: boolean; isWstETH?: boolean; isIbYield?: boolean }; + }): Promise<{ [tokenAddress: string]: { apr: number; isIbYield: boolean } }> { + const aprs: { [tokenAddress: string]: { apr: number; isIbYield: boolean } } = {}; + for (const { address, isSftmX, isWstETH, isIbYield } of Object.values(tokens)) { + try { + const tokenContract = getContractAt(address, ReaperCryptAbi); + const strategyAddress = await tokenContract.strategy(); + const strategyContract = getContractAt(strategyAddress, ReaperCryptStrategyAbi); + let avgAprAcrossXHarvests = 0; + + avgAprAcrossXHarvests = + (await strategyContract.averageAPRAcrossLastNHarvests(this.averageAPRAcrossLastNHarvests)) / + APR_PERCENT_DIVISOR; + // TODO hanlde this outside + if (isSftmX) { + avgAprAcrossXHarvests = avgAprAcrossXHarvests * (1 + sFTMxBaseApr); + } + if (isWstETH) { + avgAprAcrossXHarvests = avgAprAcrossXHarvests * (1 + this.wstETHBaseApr); + } + aprs[address] = { apr: avgAprAcrossXHarvests, isIbYield: isIbYield ?? false }; + } catch (error) { + console.error(`Reaper IB APR handler failed for onChain source: `, error); + Sentry.captureException(`Reaper IB APR handler failed for onChain source: ${error}`); + return {}; + } + } + + return aprs; + } + + private async getAprFromSubgraph(tokens: { + [tokenName: string]: { address: string; isSftmX?: boolean; isWstETH?: boolean; isIbYield?: boolean }; + }): Promise<{ [tokenAddress: string]: number }> { + try { + const requestQuery = { + operationName: 'getVaults', + query: this.query, + variables: { + ids: Object.values(tokens).map(({ address }) => address), + }, + }; + const { + data: { data }, + }: { data: { data: MultiStratResponse } } = await axios({ + method: 'post', + url: this.subgraphUrl, + data: JSON.stringify(requestQuery), + }); + return data.vaults.reduce((acc, { id, apr }) => { + const token = Object.values(tokens).find((token) => token.address.toLowerCase() === id.toLowerCase()); + if (!token) { + return acc; + } + let tokenApr = parseFloat(apr) / APR_PERCENT_DIVISOR; + if (token.isSftmX) { + tokenApr = tokenApr * (1 + sFTMxBaseApr); + } + if (token.isWstETH) { + tokenApr = tokenApr * (1 + this.wstETHBaseApr); + } + return { + ...acc, + [id]: { apr: tokenApr, isIbYield: token.isIbYield ?? false }, + }; + }, {}); + } catch (error) { + console.error(`Reaper IB APR handler failed for subgraph source: `, error); + Sentry.captureException(`Reaper IB APR handler failed for subgraph source: ${error}`); + return {}; + } + } + + private async getWstEthBaseApr(): Promise { + const { data } = await axios.get<{ + data: { aprs: [{ timeUnix: number; apr: number }]; smaApr: number }; + }>('https://eth-api.lido.fi/v1/protocol/steth/apr/sma'); + return data.data.smaApr / 100; + } +} +type MultiStratResponse = { + vaults: { + id: string; + apr: string; + }[]; +}; diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tessera-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tessera-apr-handler.ts new file mode 100644 index 000000000..644e3662a --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tessera-apr-handler.ts @@ -0,0 +1,48 @@ +import { Contract } from 'ethers'; +import { abi } from './abis/tesseraPool'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import { networkContext } from '../../../../../network/network-context.service'; +import { TesseraAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +export class TesseraAprHandler implements AprHandler { + tokens: { + [tokenName: string]: { + tesseraPoolAddress: string; + tokenAddress: string; + isIbYield?: boolean; + }; + }; + readonly group = 'TESSERA'; + + constructor(aprHandlerConfig: TesseraAprConfig) { + this.tokens = aprHandlerConfig.tokens; + } + + async getAprs() { + try { + let aprEntries = []; + for (const { tesseraPoolAddress, tokenAddress, isIbYield } of Object.values(this.tokens)) { + try { + const contract = new Contract(tesseraPoolAddress, abi, networkContext.provider); + const poolsUI = await contract.getPoolsUI(); + + const pool = poolsUI[0]; + const staked = BigInt(pool.stakedAmount); + const reward = BigInt(pool.currentTimeRange.rewardsPerHour) * BigInt(24 * 365); + const apr = Number(reward.toString()) / Number(staked.toString()); + aprEntries.push([tokenAddress, { apr, isIbYield: isIbYield ?? false }]); + } catch (error) { + console.error('Failed to fetch Tessera Ape Coin APR:', error); + aprEntries.push([tokenAddress, { apr: 0, isIbYield: isIbYield ?? false }]); + } + } + return Object.fromEntries(aprEntries); + } catch (error) { + console.error('Failed to fetch Tessera APR:', error); + Sentry.captureException(`Tessera IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tetu-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tetu-apr-handler.ts new file mode 100644 index 000000000..2db4746f9 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tetu-apr-handler.ts @@ -0,0 +1,47 @@ +import axios from 'axios'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import { TetuAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +export class TetuAprHandler implements AprHandler { + sourceUrl: string; + tokens: { + [tokenName: string]: { + address: string; + isIbYield?: boolean; + }; + }; + readonly group = 'TETU'; + + constructor(aprHandlerConfig: TetuAprConfig) { + this.sourceUrl = aprHandlerConfig.sourceUrl; + this.tokens = aprHandlerConfig.tokens; + } + + async getAprs() { + try { + const { data } = await axios.get(this.sourceUrl); + const json = data as { vault: string; apr: number }[]; + const aprs = json + .filter(({ vault }) => + Object.values(this.tokens) + .map(({ address }) => address) + .includes(vault.toLowerCase()), + ) + .map((t) => [ + t.vault, + { + apr: t.apr / 100, + isIbYield: + Object.values(this.tokens).find(({ address }) => address === t.vault)?.isIbYield ?? false, + }, + ]); + return Object.fromEntries(aprs); + } catch (error) { + console.error('Failed to fetch Tetu APR:', error); + Sentry.captureException(`Tetu IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tranchess-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tranchess-apr-handler.ts new file mode 100644 index 000000000..37bea641d --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tranchess-apr-handler.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; + +import { AprHandler } from '../ib-linear-apr-handlers'; +import { TranchessAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +export class TranchessAprHandler implements AprHandler { + url: string; + tokens: { + [tokenName: string]: { + address: string; + underlyingAssetName: string; + isIbYield?: boolean; + }; + }; + readonly group = 'TRANCHESS'; + + constructor(aprHandlerConfig: TranchessAprConfig) { + this.tokens = aprHandlerConfig.tokens; + this.url = aprHandlerConfig.sourceUrl; + } + + async getAprs() { + try { + const { data } = await axios.get('https://tranchess.com/eth/api/v3/funds'); + // const [{ weeklyAveragePnlPercentage }] = data as { weeklyAveragePnlPercentage: string }[]; + const aprEntries = Object.values(this.tokens).map(({ address, underlyingAssetName, isIbYield }) => { + const weeklyAveragePnlPercentage = ( + data as { weeklyAveragePnlPercentage: string; name: string }[] + ).filter(({ name }) => name === underlyingAssetName)[0].weeklyAveragePnlPercentage; + return [ + address, + { apr: (365 * Number(weeklyAveragePnlPercentage)) / 1e18, isIbYield: isIbYield ?? false }, + ]; + }); + // The key weeklyAveragePnlPercentage is the daily yield of qETH in 18 decimals, timing 365 should give you the APR. + return Object.fromEntries(aprEntries); + } catch (error) { + console.error('Failed to fetch Tranchess APR:', error); + Sentry.captureException(`Tranchess IB APR handler failed: ${error}`); + return {}; + } + } +} diff --git a/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/yearn-apr-handler.ts b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/yearn-apr-handler.ts new file mode 100644 index 000000000..1451b2f3d --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/yearn-apr-handler.ts @@ -0,0 +1,41 @@ +import { AprHandler } from '../ib-linear-apr-handlers'; +import axios from 'axios'; +import { YearnAprConfig } from '../../../../../network/apr-config-types'; +import * as Sentry from '@sentry/node'; + +export class YearnAprHandler implements AprHandler { + sourceUrl: string; + isIbYield?: boolean; + group: string = 'YEARN'; + + constructor(aprHandlerConfig: YearnAprConfig) { + this.sourceUrl = aprHandlerConfig.sourceUrl; + this.isIbYield = aprHandlerConfig.isIbYield; + } + async getAprs(): Promise<{ [p: string]: { apr: number; isIbYield: boolean } }> { + try { + const { data } = await axios.get(this.sourceUrl); + const aprs = Object.fromEntries( + data.map(({ address, apy: { net_apy } }) => { + return [address.toLowerCase(), { apr: net_apy, isIbYield: this.isIbYield ?? false }]; + }), + ); + return aprs; + } catch (error) { + console.error(`Yearn IB APR handler failed: `, error); + Sentry.captureException(`Yearn IB APR handler failed: ${error}`); + return {}; + } + } +} + +import { Dictionary } from 'lodash'; + +interface YearnVault { + address: string; + apy: YearnVaultApy; +} + +interface YearnVaultApy { + net_apy: number; +} diff --git a/modules/pool/lib/apr-data-sources/ib-tokens-apr.service.ts b/modules/pool/lib/apr-data-sources/ib-tokens-apr.service.ts new file mode 100644 index 000000000..a2f2db527 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-tokens-apr.service.ts @@ -0,0 +1,125 @@ +import { PoolAprService } from '../../pool-types'; +import { PrismaPoolWithTokens } from '../../../../prisma/prisma-types'; +import { prisma } from '../../../../prisma/prisma-client'; +import { networkContext } from '../../../network/network-context.service'; +import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util'; +import { PrismaPoolAprItemGroup, PrismaPoolAprType, PrismaPoolLinearData } from '@prisma/client'; +import { IbLinearAprHandlers as IbTokensAprHandlers, TokenApr } from './ib-linear-apr-handlers/ib-linear-apr-handlers'; +import { tokenService } from '../../../token/token.service'; +import { collectsYieldFee } from '../pool-utils'; +import { IbAprConfig } from '../../../network/apr-config-types'; + +export class IbTokensAprService implements PoolAprService { + private ibTokensAprHandlers: IbTokensAprHandlers; + + constructor(aprConfig: IbAprConfig) { + this.ibTokensAprHandlers = new IbTokensAprHandlers(aprConfig); + } + + getAprServiceName(): string { + return 'IbTokensAprService'; + } + + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { + const operations: any[] = []; + const tokenPrices = await tokenService.getTokenPrices(); + const aprs = await this.fetchYieldTokensApr(); + const poolsWithIbTokens = pools.filter((pool) => { + return pool.tokens.find((token) => { + return Array.from(aprs.keys()) + .map((key) => key.toLowerCase()) + .includes(token.address.toLowerCase()); + }); + }); + + const poolsWithIbTokensExpanded = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: poolsWithIbTokens.map((pool) => pool.id) } }, + include: { + dynamicData: true, + tokens: { + orderBy: { index: 'asc' }, + include: { + token: true, + dynamicData: true, + }, + }, + }, + }); + + for (const pool of poolsWithIbTokensExpanded) { + if (!pool.dynamicData) { + continue; + } + const totalLiquidity = pool.dynamicData?.totalLiquidity; + if (!totalLiquidity) { + continue; + } + + // TODO: We should check whether the token has a rate provider set, but we don't store this information yet + + for (const token of pool.tokens) { + const tokenApr = aprs.get(token.address); + if (!tokenApr) { + continue; + } + + const tokenPrice = tokenService.getPriceForToken(tokenPrices, token.address); + const tokenBalance = token.dynamicData?.balance; + + const tokenLiquidity = tokenPrice * parseFloat(tokenBalance || '0'); + const tokenPercentageInPool = tokenLiquidity / totalLiquidity; + + if (!tokenApr || !tokenPercentageInPool) { + continue; + } + + let aprInPoolAfterFees = tokenApr.apr * tokenPercentageInPool; + + if (collectsYieldFee(pool)) { + const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee + ? parseFloat(pool.dynamicData.protocolYieldFee) + : networkContext.data.balancer.yieldProtocolFeePercentage; + aprInPoolAfterFees = + pool.type === 'META_STABLE' + ? aprInPoolAfterFees * (1 - networkContext.data.balancer.swapProtocolFeePercentage) + : aprInPoolAfterFees * (1 - protocolYieldFeePercentage); + } + + const yieldType: PrismaPoolAprType = + tokenApr.isIbYield || pool.type !== 'LINEAR' ? 'IB_YIELD' : 'LINEAR_BOOSTED'; + + const itemId = `${pool.id}-${token.token.symbol}-yield-apr`; + + const data = { + id: itemId, + chain: networkContext.chain, + poolId: pool.id, + title: `${token.token.symbol} APR`, + apr: aprInPoolAfterFees, + group: tokenApr.group as PrismaPoolAprItemGroup, + type: yieldType, + }; + + operations.push( + prisma.prismaPoolAprItem.upsert({ + where: { id_chain: { id: itemId, chain: networkContext.chain } }, + create: data, + update: data, + }), + ); + } + } + await prismaBulkExecuteOperations(operations); + } + + private async fetchYieldTokensApr(): Promise> { + const data = await this.ibTokensAprHandlers.fetchAprsFromAllHandlers(); + return new Map( + data + .filter((tokenApr) => { + return !isNaN(tokenApr.apr); + }) + .map((apr) => [apr.address, apr]), + ); + } +} diff --git a/modules/pool/lib/apr-data-sources/liquid-staked-base-apr.service.ts b/modules/pool/lib/apr-data-sources/liquid-staked-base-apr.service.ts index eae278fe5..90bccafb8 100644 --- a/modules/pool/lib/apr-data-sources/liquid-staked-base-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/liquid-staked-base-apr.service.ts @@ -1,45 +1,10 @@ import axios from 'axios'; export class LiquidStakedBaseAprService { - public async getWstEthBaseApr(): Promise { - const { data } = await axios.get<{ - data: { aprs: [{ timeUnix: number; apr: number }]; smaApr: number }; - }>('https://eth-api.lido.fi/v1/protocol/steth/apr/sma'); - return data.data.smaApr / 100; - } - - public getSftmxBaseApr(): number { - return 0.046; - } - - public async getAnkrFtmBaseApr(): Promise { - const { data } = await axios.get<{ services: { serviceName: string; apy: string }[] }>( - 'https://api.staking.ankr.com/v1alpha/metrics', - {}, - ); - - const ankrFtmApy = data.services.find((service) => service.serviceName === 'ftm'); - return parseFloat(ankrFtmApy?.apy || '0') / 100; - } - - public async getAnkrEthBaseApr(): Promise { - const { data } = await axios.get<{ services: { serviceName: string; apy: string }[] }>( - 'https://api.staking.ankr.com/v1alpha/metrics', - {}, - ); - - const ankrEthApy = data.services.find((service) => service.serviceName === 'eth'); - return parseFloat(ankrEthApy?.apy || '0') / 100; - } - public async getXBooBaseApr(): Promise { const { data } = await axios.get('https://api.spooky.fi/api/xboo', {}); return parseFloat(data) / 100; } - - public getREthBaseApr(): number { - return 0.0425; - } } export const liquidStakedBaseAprService = new LiquidStakedBaseAprService(); diff --git a/modules/pool/lib/apr-data-sources/optimism/overnight-apr.service.ts b/modules/pool/lib/apr-data-sources/optimism/overnight-apr.service.ts deleted file mode 100644 index dea9486e3..000000000 --- a/modules/pool/lib/apr-data-sources/optimism/overnight-apr.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import axios from 'axios'; -import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import { TokenService } from '../../../../token/token.service'; -import { PoolAprService } from '../../../pool-types'; -import { networkContext } from '../../../../network/network-context.service'; - -type OvernightApr = { - value: number; - date: string; -}; - -export class OvernightAprService implements PoolAprService { - private readonly overnightTokens: Record = { - //these should always be stored in all lowercase - '0xa348700745d249c3b49d2c2acac9a5ae8155f826': 'usd+', - '0x0b8f31480249cc717081928b8af733f45f6915bb': 'dai+', - }; - private readonly wrappedTokenAddresses = Object.keys(this.overnightTokens); - - constructor(private readonly overnightAprEndpoint: string, private readonly tokenService: TokenService) {} - - public getAprServiceName(): string { - return 'OvernightAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - const overnightLinearPools = pools.filter( - (pool) => - pool.type === 'LINEAR' && - pool.tokens.some((token) => this.wrappedTokenAddresses.includes(token.address)), - ); - - for (const pool of overnightLinearPools) { - if (!pool.linearData || !pool.dynamicData) { - continue; - } - - const itemId = `${pool.id}-overnight`; - - const linearData = pool.linearData; - const wrappedToken = pool.tokens[linearData.wrappedIndex]; - const apiQuerySlug = this.overnightTokens[wrappedToken.token.address]; - - const { data: aprData } = await axios.get( - `${this.overnightAprEndpoint}/${apiQuerySlug}/fin-data/avg-apr/week`, - ); - - const mainToken = pool.tokens[linearData.mainIndex]; - - const mainTokenPrice = this.tokenService.getPriceForToken(tokenPrices, mainToken.address); - const wrappedTokens = parseFloat(wrappedToken.dynamicData?.balance || '0'); - const priceRate = parseFloat(wrappedToken.dynamicData?.priceRate || '1.0'); - const poolWrappedLiquidity = wrappedTokens * priceRate * mainTokenPrice; - const totalLiquidity = pool.dynamicData.totalLiquidity; - const apr = totalLiquidity > 0 ? (aprData.value / 100) * (poolWrappedLiquidity / totalLiquidity) : 0; - - await prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - create: { - id: itemId, - chain: networkContext.chain, - poolId: pool.id, - title: `${wrappedToken.token.symbol} APR`, - apr, - group: 'OVERNIGHT', - type: 'LINEAR_BOOSTED', - }, - update: { apr, group: 'OVERNIGHT', type: 'LINEAR_BOOSTED', title: `${wrappedToken.token.symbol} APR` }, - }); - } - } -} diff --git a/modules/pool/lib/apr-data-sources/optimism/rocket-pool-staked-eth-apr.service.ts b/modules/pool/lib/apr-data-sources/optimism/rocket-pool-staked-eth-apr.service.ts deleted file mode 100644 index 5027385d3..000000000 --- a/modules/pool/lib/apr-data-sources/optimism/rocket-pool-staked-eth-apr.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import { TokenService } from '../../../../token/token.service'; -import { PoolAprService } from '../../../pool-types'; -import { collectsYieldFee } from '../../pool-utils'; -import { networkContext } from '../../../../network/network-context.service'; -import { liquidStakedBaseAprService } from '../liquid-staked-base-apr.service'; - -export class RocketPoolStakedEthAprService implements PoolAprService { - constructor(private readonly tokenService: TokenService, private readonly rethAddress: string) {} - - public getAprServiceName(): string { - return 'RocketPoolStakedEthAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - const rethPrice = this.tokenService.getPriceForToken(tokenPrices, this.rethAddress); - const rethBaseApr = await liquidStakedBaseAprService.getREthBaseApr(); - - let operations: any[] = []; - for (const pool of pools) { - const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee - ? parseFloat(pool.dynamicData.protocolYieldFee) - : networkContext.data.balancer.yieldProtocolFeePercentage; - const rethToken = pool.tokens.find((token) => token.address === this.rethAddress); - const rethTokenBalance = rethToken?.dynamicData?.balance; - if (rethTokenBalance && pool.dynamicData) { - const rethPercentage = (parseFloat(rethTokenBalance) * rethPrice) / pool.dynamicData.totalLiquidity; - const rethApr = pool.dynamicData.totalLiquidity > 0 ? rethBaseApr * rethPercentage : 0; - const userApr = - pool.type === 'META_STABLE' - ? rethApr * (1 - networkContext.data.balancer.swapProtocolFeePercentage) - : rethApr * (1 - protocolYieldFeePercentage); - - operations.push( - prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: `${pool.id}-reth-apr`, chain: networkContext.chain } }, - update: { apr: collectsYieldFee(pool) ? userApr : rethApr }, - create: { - id: `${pool.id}-reth-apr`, - chain: networkContext.chain, - poolId: pool.id, - apr: collectsYieldFee(pool) ? userApr : rethApr, - title: 'rETH APR', - type: 'IB_YIELD', - }, - }), - ); - } - } - await Promise.all(operations); - } -} diff --git a/modules/pool/lib/apr-data-sources/optimism/wsteth-apr.service.ts b/modules/pool/lib/apr-data-sources/optimism/wsteth-apr.service.ts deleted file mode 100644 index ec306e15d..000000000 --- a/modules/pool/lib/apr-data-sources/optimism/wsteth-apr.service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import axios from 'axios'; -import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; -import { TokenService } from '../../../../token/token.service'; -import { PoolAprService } from '../../../pool-types'; -import { collectsYieldFee } from '../../pool-utils'; -import { networkContext } from '../../../../network/network-context.service'; -import { liquidStakedBaseAprService } from '../liquid-staked-base-apr.service'; - -export class WstethAprService implements PoolAprService { - constructor(private readonly tokenService: TokenService, private readonly wstethContractAddress: string) {} - - public getAprServiceName(): string { - return 'WstethAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await this.tokenService.getTokenPrices(); - const wstethPrice = this.tokenService.getPriceForToken(tokenPrices, this.wstethContractAddress); - - let wstethBaseApr: number | undefined; - for (const pool of pools) { - const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee - ? parseFloat(pool.dynamicData.protocolYieldFee) - : networkContext.data.balancer.yieldProtocolFeePercentage; - const itemId = `${pool.id}-lido-wsteth`; - - const wstethToken = pool.tokens.find((token) => token.address === this.wstethContractAddress.toLowerCase()); - const wstethTokenBalance = wstethToken?.dynamicData?.balance; - - if (wstethTokenBalance && pool.dynamicData) { - if (!wstethBaseApr) { - wstethBaseApr = await liquidStakedBaseAprService.getWstEthBaseApr(); - } - - const wstethPercentage = - (parseFloat(wstethTokenBalance) * wstethPrice) / pool.dynamicData.totalLiquidity; - const wstethApr = pool.dynamicData.totalLiquidity > 0 ? wstethBaseApr * wstethPercentage : 0; - const userApr = - pool.type === 'META_STABLE' - ? wstethApr * (1 - networkContext.data.balancer.swapProtocolFeePercentage) - : wstethApr * (1 - protocolYieldFeePercentage); - - await prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - create: { - id: itemId, - chain: networkContext.chain, - poolId: pool.id, - title: `stETH APR`, - apr: collectsYieldFee(pool) ? userApr : wstethApr, - type: 'IB_YIELD', - }, - update: { apr: collectsYieldFee(pool) ? userApr : wstethApr, title: `stETH APR` }, - }); - } - } - } -} diff --git a/modules/pool/lib/apr-data-sources/phantom-stable-apr.service.ts b/modules/pool/lib/apr-data-sources/phantom-stable-apr.service.ts index c7e0c7f00..06f738c6b 100644 --- a/modules/pool/lib/apr-data-sources/phantom-stable-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/phantom-stable-apr.service.ts @@ -1,5 +1,5 @@ import { PoolAprService } from '../../pool-types'; -import { PrismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; +import { PrismaPoolWithTokens, prismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; import { prisma } from '../../../../prisma/prisma-client'; import { collectsYieldFee } from '../pool-utils'; import { networkContext } from '../../../network/network-context.service'; @@ -9,10 +9,15 @@ export class PhantomStableAprService implements PoolAprService { return 'PhantomStableAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { const phantomStablePools = pools.filter((pool) => pool.type === 'PHANTOM_STABLE'); - for (const pool of phantomStablePools) { + const phantomStablePoolsExpanded = await prisma.prismaPool.findMany({ + ...prismaPoolWithExpandedNesting, + where: { chain: networkContext.chain, id: { in: phantomStablePools.map((pool) => pool.id) } }, + }); + + for (const pool of phantomStablePoolsExpanded) { const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee ? parseFloat(pool.dynamicData.protocolYieldFee) : networkContext.data.balancer.yieldProtocolFeePercentage; diff --git a/modules/pool/lib/apr-data-sources/reaper-crypt-apr.service.ts b/modules/pool/lib/apr-data-sources/reaper-crypt-apr.service.ts deleted file mode 100644 index e6947e4d5..000000000 --- a/modules/pool/lib/apr-data-sources/reaper-crypt-apr.service.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { isSameAddress } from '@balancer-labs/sdk'; -import * as Sentry from '@sentry/node'; -import { prisma } from '../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; -import { tokenService } from '../../../token/token.service'; -import { getContractAt } from '../../../web3/contract'; -import { PoolAprService } from '../../pool-types'; -import ReaperCryptAbi from './abi/ReaperCrypt.json'; -import ReaperCryptStrategyAbi from './abi/ReaperCryptStrategy.json'; -import { networkContext } from '../../../network/network-context.service'; -import { liquidStakedBaseAprService } from './liquid-staked-base-apr.service'; -import axios from 'axios'; -import { Contract } from 'ethers'; - -type MultiStratQueryResponse = { - data: { - vault: { - apr: string; - }; - }; -}; - -export class ReaperCryptAprService implements PoolAprService { - private readonly APR_PERCENT_DIVISOR = 10_000; - - constructor( - private readonly reaperMultistratSubgraphUrl: string, - private readonly linearPoolFactories: string[], - private readonly linearPoolsFromErc4626Factory: string[], - private readonly averageAPRAcrossLastNHarvests: number, - private readonly sFtmXAddress: string | undefined, - private readonly wstEthAddress: string | undefined, - ) {} - - public getAprServiceName(): string { - return 'ReaperCryptAprService'; - } - - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { - const tokenPrices = await tokenService.getTokenPrices(); - - for (const pool of pools) { - if ( - (!this.linearPoolFactories.includes(pool.factory || '') && - !this.linearPoolsFromErc4626Factory.includes(pool.id)) || - !pool.linearData || - !pool.dynamicData - ) { - continue; - } - const linearData = pool.linearData; - const wrappedToken = pool.tokens[linearData.wrappedIndex]; - const mainToken = pool.tokens[linearData.mainIndex]; - - const cryptContract = getContractAt(wrappedToken.address, ReaperCryptAbi); - - const cryptName = await cryptContract.name(); - - let cryptApr = 0; - let itemId = ''; - - if (cryptName.includes('Multi-Strategy')) { - cryptApr = await this.getMultiStrategyAprFromSubgraph(wrappedToken.address); - itemId = `${pool.id}-reaper-mutlistrat`; - } else { - try { - cryptApr = await this.getSingleStrategyCryptApr(cryptContract); - itemId = `${pool.id}-reaper-crypt`; - } catch (e) { - Sentry.captureException(e, { - tags: { - poolId: pool.id, - poolName: pool.name, - cryptContract: wrappedToken.address, - }, - }); - continue; - } - } - - const tokenPrice = tokenService.getPriceForToken(tokenPrices, mainToken.address); - const wrappedTokens = parseFloat(wrappedToken.dynamicData?.balance || '0'); - const priceRate = parseFloat(wrappedToken.dynamicData?.priceRate || '1.0'); - const poolWrappedLiquidity = wrappedTokens * priceRate * tokenPrice; - const totalLiquidity = pool.dynamicData!.totalLiquidity; - let apr = totalLiquidity > 0 ? cryptApr * (poolWrappedLiquidity / totalLiquidity) : 0; - - await prisma.prismaPoolAprItem.upsert({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - create: { - id: itemId, - chain: networkContext.chain, - poolId: pool.id, - title: `${wrappedToken.token.symbol} APR`, - apr: apr, - group: 'REAPER', - type: 'LINEAR_BOOSTED', - }, - update: { title: `${wrappedToken.token.symbol} APR`, apr: apr }, - }); - - // if we have sftmx as the main token in this linear pool, we want to take the linear APR top level and - // we also need to adapt the APR since the vault APR is denominated in sFTMx, so we need to apply the growth rate - // and add the sftmx base apr to the unwrapped portion - if (this.sFtmXAddress && isSameAddress(mainToken.address, this.sFtmXAddress)) { - const baseApr = await liquidStakedBaseAprService.getSftmxBaseApr(); - if (baseApr > 0) { - const boostedVaultApr = this.getBoostedVaultApr( - totalLiquidity, - cryptApr, - baseApr, - poolWrappedLiquidity, - ); - - await prisma.prismaPoolAprItem.update({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - data: { - apr: boostedVaultApr, - }, - }); - } - } - - if (this.wstEthAddress && isSameAddress(mainToken.address, this.wstEthAddress)) { - const baseApr = await liquidStakedBaseAprService.getWstEthBaseApr(); - if (baseApr > 0) { - const boostedVaultApr = this.getBoostedVaultApr( - totalLiquidity, - cryptApr, - baseApr, - poolWrappedLiquidity, - ); - - await prisma.prismaPoolAprItem.update({ - where: { id_chain: { id: itemId, chain: networkContext.chain } }, - data: { apr: boostedVaultApr }, - }); - } - } - } - } - - private async getSingleStrategyCryptApr(cryptContract: Contract): Promise { - const cryptStrategyAddress = await cryptContract.strategy(); - - const strategyContract = getContractAt(cryptStrategyAddress, ReaperCryptStrategyAbi); - let avgAprAcrossXHarvests = 0; - - avgAprAcrossXHarvests = - (await strategyContract.averageAPRAcrossLastNHarvests(this.averageAPRAcrossLastNHarvests)) / - this.APR_PERCENT_DIVISOR; - - return avgAprAcrossXHarvests; - } - - private async getMultiStrategyAprFromSubgraph(address: string): Promise { - const { data } = await axios.post(this.reaperMultistratSubgraphUrl, { - query: `query { - vault(id: "${address}"){ - apr - } - }`, - }); - - if (data.data.vault && data.data.vault.apr) { - return parseFloat(data.data.vault.apr) / this.APR_PERCENT_DIVISOR; - } - return 0; - } - - private getBoostedVaultApr( - totalLiquidity: number, - vaultHarvestApr: number, - IbBaseApr: number, - poolWrappedLiquidity: number, - ) { - return totalLiquidity > 0 - ? ((1 + vaultHarvestApr) * (1 + IbBaseApr) - 1) * (poolWrappedLiquidity / totalLiquidity) - : 0; - } -} diff --git a/modules/pool/lib/apr-data-sources/swap-fee-apr.service.ts b/modules/pool/lib/apr-data-sources/swap-fee-apr.service.ts index 3c032ffa1..e7a0bf07b 100644 --- a/modules/pool/lib/apr-data-sources/swap-fee-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/swap-fee-apr.service.ts @@ -1,5 +1,5 @@ import { PoolAprService } from '../../pool-types'; -import { PrismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; +import { PrismaPoolWithTokens } from '../../../../prisma/prisma-types'; import { prisma } from '../../../../prisma/prisma-client'; import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util'; import { networkContext } from '../../../network/network-context.service'; @@ -12,10 +12,17 @@ export class SwapFeeAprService implements PoolAprService { return 'SwapFeeAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { const operations: any[] = []; - for (const pool of pools) { + const poolsExpanded = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: pools.map((pool) => pool.id) } }, + include: { + dynamicData: true, + }, + }); + + for (const pool of poolsExpanded) { if (pool.dynamicData) { const apr = pool.dynamicData.totalLiquidity > 0 diff --git a/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts b/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts index ea9dcd694..72e45e25e 100644 --- a/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/ve-bal-gauge-apr.service.ts @@ -1,4 +1,4 @@ -import { PrismaPoolWithExpandedNesting } from '../../../../prisma/prisma-types'; +import { PrismaPoolWithTokens } from '../../../../prisma/prisma-types'; import { PoolAprService } from '../../pool-types'; import { TokenService } from '../../../token/token.service'; import { secondsPerYear } from '../../../common/time'; @@ -21,11 +21,28 @@ export class GaugeAprService implements PoolAprService { return 'GaugeAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { const operations: any[] = []; const gauges = await this.gaugeSubgraphService.getAllGaugesWithStatus(); const tokenPrices = await this.tokenService.getTokenPrices(); - for (const pool of pools) { + + const poolsExpanded = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: pools.map((pool) => pool.id) } }, + include: { + dynamicData: true, + staking: { + include: { + gauge: { + include: { + rewards: true, + }, + }, + }, + }, + }, + }); + + for (const pool of poolsExpanded) { let gauge; let preferredStaking; for (const stake of pool.staking) { diff --git a/modules/pool/lib/pool-apr-updater.service.ts b/modules/pool/lib/pool-apr-updater.service.ts index be99509d9..863a14242 100644 --- a/modules/pool/lib/pool-apr-updater.service.ts +++ b/modules/pool/lib/pool-apr-updater.service.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/node'; import { prisma } from '../../../prisma/prisma-client'; -import { prismaPoolWithExpandedNesting } from '../../../prisma/prisma-types'; +import { poolWithTokens } from '../../../prisma/prisma-types'; import { PoolAprService } from '../pool-types'; import _ from 'lodash'; import { prismaBulkExecuteOperations } from '../../../prisma/prisma-util'; @@ -15,7 +15,7 @@ export class PoolAprUpdaterService { public async updatePoolAprs() { const pools = await prisma.prismaPool.findMany({ - ...prismaPoolWithExpandedNesting, + ...poolWithTokens, where: { chain: networkContext.chain }, }); diff --git a/modules/pool/pool-types.ts b/modules/pool/pool-types.ts index db6c2c42d..091dc2087 100644 --- a/modules/pool/pool-types.ts +++ b/modules/pool/pool-types.ts @@ -1,8 +1,8 @@ import { PrismaPoolStakingType } from '@prisma/client'; -import { PrismaPoolWithExpandedNesting } from '../../prisma/prisma-types'; +import { PrismaPoolWithTokens } from '../../prisma/prisma-types'; export interface PoolAprService { - updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise; + updateAprForPools(pools: PrismaPoolWithTokens[]): Promise; getAprServiceName(): string; } diff --git a/modules/pool/pool.prisma b/modules/pool/pool.prisma index 174d3ed07..6fb00ce62 100644 --- a/modules/pool/pool.prisma +++ b/modules/pool/pool.prisma @@ -299,6 +299,16 @@ enum PrismaPoolAprItemGroup { OVERNIGHT REAPER YEARN + IDLE + TRANCHESS + GEARBOX + AAVE + ANKR + TESSERA + TETU + OVIX + EULER + DEFAULT } model PrismaPoolCategory { diff --git a/prisma/migrations/20230802001731_adding_apr_item_groups/migration.sql b/prisma/migrations/20230802001731_adding_apr_item_groups/migration.sql new file mode 100644 index 000000000..d6a91ea1f --- /dev/null +++ b/prisma/migrations/20230802001731_adding_apr_item_groups/migration.sql @@ -0,0 +1,18 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'IDLE'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'TRANCHESS'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'GEARBOX'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'AAVE'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'ANKR'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'TESSERA'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'TETU'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'OVIX'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'EULER'; +ALTER TYPE "PrismaPoolAprItemGroup" ADD VALUE 'DEFAULT'; diff --git a/prisma/prisma-types.ts b/prisma/prisma-types.ts index 1ed3b790a..d986de64e 100644 --- a/prisma/prisma-types.ts +++ b/prisma/prisma-types.ts @@ -1,6 +1,6 @@ import { Prisma, PrismaToken, PrismaTokenPrice, PrismaTokenTypeOption } from '@prisma/client'; -const poolWithTokens = Prisma.validator()({ +export const poolWithTokens = Prisma.validator()({ include: { tokens: true }, }); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 296704b77..76e03f730 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -345,6 +345,16 @@ enum PrismaPoolAprItemGroup { OVERNIGHT REAPER YEARN + IDLE + TRANCHESS + GEARBOX + AAVE + ANKR + TESSERA + TETU + OVIX + EULER + DEFAULT } model PrismaPoolCategory {