diff --git a/env.local b/env.local index de768a481..fb4c4c67c 100644 --- a/env.local +++ b/env.local @@ -35,7 +35,7 @@ SENTRY_AUTH_TOKEN= APOLLO_GRAPH_REF= APOLLO_API_KEY= -INFURA_API_KEY= +INFURA_API_KEY= # Admin routes ADMIN_API_KEY= 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 cdee1952d..04d0ad3ae 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 = { @@ -24,7 +23,7 @@ const arbitrumNetworkData: NetworkData = { slug: 'arbitrum', id: 42161, nativeAssetAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - wrappedNativeAssetAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + wrappedNativeAssetAddress: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', prismaId: 'ARBITRUM', gqlId: 'ARBITRUM', }, @@ -68,24 +67,23 @@ const arbitrumNetworkData: NetworkData = { delegationProxy: '0x81cfae226343b24ba12ec6521db2c79e7aeeb310', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0xaEb406b0E430BF5Ea2Dc0B9Fe62E4E53f74B3a33', - '0x85a80afee867aDf27B50BdB7b76DA70f1E853062', - '0x1c99324EDC771c82A0DCCB780CC7DDA0045E50e7', - '0x2498A2B0d6462d2260EAC50aE1C3e03F4829BA95', - '0xA8920455934Da4D853faac1f94Fe7bEf72943eF1', + '0xaeb406b0e430bf5ea2dc0b9fe62e4e53f74b3a33', + '0x85a80afee867adf27b50bdb7b76da70f1e853062', + '0x1c99324edc771c82a0dccb780cc7dda0045e50e7', + '0x2498a2b0d6462d2260eac50ae1c3e03f4829ba95', + '0xa8920455934da4d853faac1f94fe7bef72943ef1', ], weightedPoolV2Factories: [ - '0x8df6EfEc5547e31B0eb7d1291B511FF8a2bf987c', - '0xf1665E19bc105BE4EDD3739F88315cC699cc5b65', - '0xc7E5ED1054A24Ef31D827E6F86caA58B3Bc168d7', + '0x8df6efec5547e31b0eb7d1291b511ff8a2bf987c', + '0xf1665e19bc105be4edd3739f88315cc699cc5b65', + '0xc7e5ed1054a24ef31d827e6f86caa58b3bc168d7', ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0x7Ba29fE8E83dd6097A7298075C4AFfdBda3121cC', }, - multicall: '0x80C7DD17B01855a6D2347444a0FCC36136a314de', + multicall: '0x80c7dd17b01855a6d2347444a0fcc36136a314de', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', avgBlockSpeed: 1, sor: { @@ -104,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', @@ -129,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), @@ -217,15 +270,11 @@ export const arbitrumNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', @@ -247,7 +296,7 @@ export const arbitrumNetworkConfig: NetworkConfig = { }, { name: 'sync-vebal-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(2, 'minutes') : every(1, 'minutes'), + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(3, 'minutes'), }, { name: 'sync-vebal-totalSupply', diff --git a/modules/network/avalanche.ts b/modules/network/avalanche.ts index 41e9e2693..a4412e35f 100644 --- a/modules/network/avalanche.ts +++ b/modules/network/avalanche.ts @@ -16,13 +16,14 @@ 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: { slug: 'avalanche', id: 43114, nativeAssetAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - wrappedNativeAssetAddress: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', + wrappedNativeAssetAddress: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', prismaId: 'AVALANCHE', gqlId: 'AVALANCHE', }, @@ -66,15 +67,17 @@ const avalancheNetworkData: NetworkData = { delegationProxy: '', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0x3B1eb8EB7b43882b385aB30533D9A2BeF9052a98', - '0xE42FFA682A26EF8F25891db4882932711D42e467', + '0x3b1eb8eb7b43882b385ab30533d9a2bef9052a98', + '0xe42ffa682a26ef8f25891db4882932711d42e467', + ], + weightedPoolV2Factories: [ + '0x94f68b54191f62f781fe8298a8a5fa3ed772d227', + '0x230a59f4d9adc147480f03b0d3fffecd56c3289a', ], - weightedPoolV2Factories: ['0x230a59F4d9ADc147480f03B0D3fFfeCd56c3289a'], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0x67af5D428d38C5176a286a2371Df691cDD914Fb8', }, multicall: '0xca11bde05977b3631167028862be2a173976ca11', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', @@ -95,6 +98,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', @@ -110,6 +193,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), @@ -189,15 +273,11 @@ export const avalancheNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', diff --git a/modules/network/base.ts b/modules/network/base.ts index 480e5b342..5a51fd799 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: { @@ -63,19 +61,29 @@ const baseNetworkData: NetworkData = { }, protocolToken: 'bal', bal: { - address: '0x7c6b91d9be155a6db01f749217d76ff02a7227f2', + address: '0x4158734D47Fc9692176B5085E0F52ee0Da5d47F1 ', }, veBal: { address: '0xc128a9954e6c874ea3d62ce62b468ba073093f25', delegationProxy: '0xd87f44df0159dc78029ab9ca7d7e57e7249f5acd', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', - composableStablePoolFactories: ['0x8df317a729fcaA260306d7de28888932cb579b88'], - weightedPoolV2Factories: ['0x4C32a8a8fDa4E24139B51b456B42290f51d6A1c4'], + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', + composableStablePoolFactories: ['0x8df317a729fcaa260306d7de28888932cb579b88'], + weightedPoolV2Factories: ['0x4c32a8a8fda4e24139b51b456b42290f51d6a1c4'], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0x67af5D428d38C5176a286a2371Df691cDD914Fb8', + }, + ibAprConfig: { + defaultHandlers: { + cbETH: { + tokenAddress: '0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22', + sourceUrl: 'https://api.exchange.coinbase.com/wrapped-assets/CBETH/', + path: 'apy', + scale: 1, + isIbYield: true, + }, + }, }, multicall: '0xca11bde05977b3631167028862be2a173976ca11', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', @@ -96,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', @@ -115,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]), @@ -194,15 +198,11 @@ export const baseNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', diff --git a/modules/network/fantom.ts b/modules/network/fantom.ts index e6b77c22b..20b053d01 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: { @@ -90,6 +86,7 @@ const fantomNetworkData: NetworkData = { '0x9879abdea01a879644185341f7af7d8343556b7a', // multi tusd '0x3129662808bec728a27ab6a6b9afd3cbaca8a43c', // multi dola '0x0615dbba33fe61a31c7ed131bda6655ed76748b1', // multi ankr + '0xb7c2ddb1ebac1056231ef22c1b0a13988537a274', // new tarot ], }, tokenPrices: { @@ -113,25 +110,24 @@ const fantomNetworkData: NetworkData = { poolAddress: '0xcde5a11a4acb4ee4c805352cec57e236bdbc3837', }, balancer: { - vault: '0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce', + vault: '0x20dd72ed959b6147912c2e529f0a0c651c33c9ce', composableStablePoolFactories: [ - '0x5AdAF6509BCEc3219455348AC45d6D3261b1A990', - '0xB384A86F2Fd7788720db42f9daa60fc07EcBeA06', - '0x44814E3A603bb7F1198617995c5696C232F6e8Ed', - '0x911566c808bF00acB200B418564440A2Af177548', - '0x5c3094982cF3c97A06b7d62A6f7669F14a199B19', - '0x23F03a4fb344d8B98833d2ACe093cc305E03474f', + '0x5adaf6509bcec3219455348ac45d6d3261b1a990', + '0xb384a86f2fd7788720db42f9daa60fc07ecbea06', + '0x44814e3a603bb7f1198617995c5696c232f6e8ed', + '0x911566c808bf00acb200b418564440a2af177548', + '0x5c3094982cf3c97a06b7d62a6f7669f14a199b19', + '0x23f03a4fb344d8b98833d2ace093cc305e03474f', ], weightedPoolV2Factories: [ - '0xB2ED595Afc445b47Db7043bEC25e772bf0FA1fbb', - '0x8ea1c497c16726E097f62C8C9FBD944143F27090', - '0xea87F3dFfc679035653C0FBa70e7bfe46E3FB733', - '0xd678b6Acd834Cc969Bb19Ce82727f2a541fb7941', - '0xb841Df73861E65E6D61a80F503F095a91ce75e15', + '0xb2ed595afc445b47db7043bec25e772bf0fa1fbb', + '0x8ea1c497c16726e097f62c8c9fbd944143f27090', + '0xea87f3dffc679035653c0fba70e7bfe46e3fb733', + '0xd678b6acd834cc969bb19ce82727f2a541fb7941', + '0xb841df73861e65e6d61a80f503f095a91ce75e15', ], swapProtocolFeePercentage: 0.25, yieldProtocolFeePercentage: 0.25, - poolDataQueryContract: '0x9642Dbba0753B1518022d7617Be079f0d7EFD165', factoriesWithpoolSpecificProtocolFeePercentagesProvider: [ '0xb841df73861e65e6d61a80f503f095a91ce75e15', '0x5c3094982cf3c97a06b7d62a6f7669f14a199b19', @@ -140,7 +136,7 @@ const fantomNetworkData: NetworkData = { multicall: '0x66335d7ad8011f6aa3f48aadcb523b62b38ed961', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', masterchef: { - address: '0x8166994d9ebBe5829EC86Bd81258149B87faCfd3', + address: '0x8166994d9ebbe5829ec86bd81258149b87facfd3', excludedFarmIds: [ '34', //OHM bonding farm '28', //OHM bonding farm @@ -149,7 +145,7 @@ const fantomNetworkData: NetworkData = { ], }, reliquary: { - address: '0x1ed6411670c709F4e163854654BD52c74E66D7eC', + address: '0x1ed6411670c709f4e163854654bd52c74e66d7ec', excludedFarmIds: [ '0', // test with dummy token '1', // test with fresh beets pool BPT @@ -172,31 +168,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', + 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', @@ -239,19 +299,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 e1447dba0..fb807fff0 100644 --- a/modules/network/gnosis.ts +++ b/modules/network/gnosis.ts @@ -16,13 +16,14 @@ 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: { slug: 'gnosis', id: 100, nativeAssetAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - wrappedNativeAssetAddress: '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d', + wrappedNativeAssetAddress: '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d', prismaId: 'GNOSIS', gqlId: 'GNOSIS', }, @@ -64,15 +65,20 @@ const gnosisNetworkData: NetworkData = { delegationProxy: '0x7a2535f5fb47b8e44c02ef5d9990588313fe8f05', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0xf23b4DB826DbA14c0e857029dfF076b1c0264843', - '0x4bdCc2fb18AEb9e2d281b0278D946445070EAda7', + '0x76578ecf9a141296ec657847fb45b0585bcda3a6', + '0xc128468b7ce63ea702c1f104d55a2566b13d3abd', + '0xd87f44df0159dc78029ab9ca7d7e57e7249f5acd', + '0x4bdcc2fb18aeb9e2d281b0278d946445070eada7', + ], + weightedPoolV2Factories: [ + '0x6cad2ea22bfa7f4c14aae92e47f510cd5c509bc7', + '0xf302f9f50958c5593770fdf4d4812309ff77414f', + '0xc128a9954e6c874ea3d62ce62b468ba073093f25', ], - weightedPoolV2Factories: ['0x6CaD2ea22BFA7F4C14Aae92E47F510Cd5C509bc7'], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0x3f170631ed9821Ca51A59D996aB095162438DC10', }, multicall: '0xbb6fab6b627947dae0a75808250d8b2652952cb5', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', @@ -93,6 +99,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', @@ -111,6 +143,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), @@ -190,15 +223,11 @@ export const gnosisNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', diff --git a/modules/network/mainnet.ts b/modules/network/mainnet.ts index b99907547..440b9d9cc 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,14 +15,22 @@ 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, nativeAssetAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - wrappedNativeAssetAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + wrappedNativeAssetAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', prismaId: 'MAINNET', gqlId: 'MAINNET', }, @@ -65,25 +71,25 @@ const mainnetNetworkData: NetworkData = { address: '0xc128a9954e6c874ea3d62ce62b468ba073093f25', delegationProxy: '0x0000000000000000000000000000000000000000', }, - gaugeControllerAddress: '0xC128468b7Ce63eA702C1f104D55A2566b13D3ABD', - gaugeControllerHelperAddress: '0x8E5698dC4897DC12243c8642e77B4f21349Db97C', + gaugeControllerAddress: '0xc128468b7ce63ea702c1f104d55a2566b13d3abd', + gaugeControllerHelperAddress: '0x8e5698dc4897dc12243c8642e77b4f21349db97c', balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0xf9ac7B9dF2b3454E841110CcE5550bD5AC6f875F', - '0x85a80afee867aDf27B50BdB7b76DA70f1E853062', - '0xdba127fBc23fb20F5929C546af220A991b5C6e01', - '0xfADa0f4547AB2de89D1304A668C39B3E09Aa7c76', - '0xDB8d758BCb971e482B2C45f7F8a7740283A1bd3A', + '0xf9ac7b9df2b3454e841110cce5550bd5ac6f875f', + '0x85a80afee867adf27b50bdb7b76da70f1e853062', + '0xdba127fbc23fb20f5929c546af220a991b5c6e01', + '0xfada0f4547ab2de89d1304a668c39b3e09aa7c76', + '0xdb8d758bcb971e482b2c45f7f8a7740283a1bd3a', + '0xba1b4a90bad57470a2cba762a32955dc491f76e0', ], weightedPoolV2Factories: [ - '0xcC508a455F5b0073973107Db6a878DdBDab957bC', - '0x5Dd94Da3644DDD055fcf6B3E1aa310Bb7801EB8b', - '0x897888115Ada5773E02aA29F775430BFB5F34c51', + '0xcc508a455f5b0073973107db6a878ddbdab957bc', + '0x5dd94da3644ddd055fcf6b3e1aa310bb7801eb8b', + '0x897888115ada5773e02aa29f775430bfb5f34c51', ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0xf5CDdF6feD9C589f1Be04899F48f9738531daD59', excludedPoolDataQueryPoolIds: ['0xf71d0774b214c4cf51e33eb3d30ef98132e4dbaa00000000000000000000046e'], }, multicall: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', @@ -105,9 +111,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: { @@ -124,7 +335,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), @@ -204,15 +415,11 @@ export const mainnetNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', diff --git a/modules/network/network-config-types.ts b/modules/network/network-config-types.ts index 982b01dd2..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; @@ -96,7 +97,6 @@ export interface NetworkData { composableStablePoolFactories: string[]; yieldProtocolFeePercentage: number; swapProtocolFeePercentage: number; - poolDataQueryContract: string; excludedPoolDataQueryPoolIds?: string[]; factoriesWithpoolSpecificProtocolFeePercentagesProvider?: string[]; }; @@ -106,6 +106,7 @@ export interface NetworkData { address: string; excludedFarmIds: string[]; }; + ibAprConfig: IbAprConfig; reliquary?: { address: string; excludedFarmIds: string[]; @@ -113,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 a31f67dd7..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 = { @@ -80,23 +76,22 @@ const optimismNetworkData: NetworkData = { delegationProxy: '0x9da18982a33fd0c7051b19f0d7c76f2d5e7e017c', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0xf145caFB67081895EE80eB7c04A30Cf87f07b745', - '0xe2E901AB09f37884BA31622dF3Ca7FC19AA443Be', - '0x1802953277FD955f9a254B80Aa0582f193cF1d77', - '0x043A2daD730d585C44FB79D2614F295D2d625412', + '0xf145cafb67081895ee80eb7c04a30cf87f07b745', + '0xe2e901ab09f37884ba31622df3ca7fc19aa443be', + '0x1802953277fd955f9a254b80aa0582f193cf1d77', + '0x043a2dad730d585c44fb79d2614f295d2d625412', ], weightedPoolV2Factories: [ - '0xad901309d9e9DbC5Df19c84f729f429F0189a633', - '0xA0DAbEBAAd1b243BBb243f933013d560819eB66f', - '0x230a59F4d9ADc147480f03B0D3fFfeCd56c3289a', + '0xad901309d9e9dbc5df19c84f729f429f0189a633', + '0xa0dabebaad1b243bbb243f933013d560819eb66f', + '0x230a59f4d9adc147480f03b0d3fffecd56c3289a', ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0x6B5dA774890Db7B7b96C6f44e6a4b0F657399E2e', }, - multicall: '0x2DC0E2aa608532Da689e89e237dF582B783E552C', + multicall: '0x2dc0e2aa608532da689e89e237df582b783e552c', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', masterchef: { address: '0x0000000000000000000000000000000000000000', @@ -119,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: [ @@ -142,10 +212,6 @@ const optimismNetworkData: NetworkData = { '0x72d6df381cac8c2283c0b13fe5262a1f5e8e8d1b0000000000000000000000cb', ], }, - lido: { - wstEthAprEndpoint: 'https://eth-api.lido.fi/v1/protocol/steth/apr/sma', - wstEthContract: '0x1f32b1c2345538c0c6f582fcb022739c4a194ebb', - }, rocket: { rEthContract: '0x9bcef72be871e61ed4fbbc7630889bee758eb81d', }, @@ -183,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 513516018..349462ad7 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 = { @@ -23,7 +23,7 @@ const polygonNetworkData: NetworkData = { slug: 'polygon', id: 137, nativeAssetAddress: '0x0000000000000000000000000000000000001010', - wrappedNativeAssetAddress: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + wrappedNativeAssetAddress: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', prismaId: 'POLYGON', gqlId: 'POLYGON', }, @@ -67,24 +67,23 @@ const polygonNetworkData: NetworkData = { delegationProxy: '0x0f08eef2c785aa5e7539684af04755dec1347b7c', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0x136FD06Fa01eCF624C7F2B3CB15742c1339dC2c4', - '0x85a80afee867aDf27B50BdB7b76DA70f1E853062', - '0x7bc6C0E73EDAa66eF3F6E2f27b0EE8661834c6C9', - '0x6Ab5549bBd766A43aFb687776ad8466F8b42f777', - '0xe2fa4e1d17725e72dcdAfe943Ecf45dF4B9E285b', + '0x136fd06fa01ecf624c7f2b3cb15742c1339dc2c4', + '0x85a80afee867adf27b50bdb7b76da70f1e853062', + '0x7bc6c0e73edaa66ef3f6e2f27b0ee8661834c6c9', + '0x6ab5549bbd766a43afb687776ad8466f8b42f777', + '0xe2fa4e1d17725e72dcdafe943ecf45df4b9e285b', ], weightedPoolV2Factories: [ - '0x0e39C3D9b2ec765eFd9c5c70BB290B1fCD8536E3', - '0x82e4cFaef85b1B6299935340c964C942280327f4', - '0xFc8a407Bba312ac761D8BFe04CE1201904842B76', + '0x0e39c3d9b2ec765efd9c5c70bb290b1fcd8536e3', + '0x82e4cfaef85b1b6299935340c964c942280327f4', + '0xfc8a407bba312ac761d8bfe04ce1201904842b76', ], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0x84813aA3e079A665C0B80F944427eE83cBA63617', }, - multicall: '0x275617327c958bD06b5D6b871E7f491D76113dd8', + multicall: '0x275617327c958bd06b5d6b871e7f491d76113dd8', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', avgBlockSpeed: 1, sor: { @@ -103,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', @@ -118,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), @@ -197,15 +327,11 @@ export const polygonNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', diff --git a/modules/network/zkevm.ts b/modules/network/zkevm.ts index d04bb6967..08efcc313 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,13 +16,14 @@ 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: { slug: 'zkevm', id: 1101, nativeAssetAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - wrappedNativeAssetAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', + wrappedNativeAssetAddress: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', prismaId: 'ZKEVM', gqlId: 'ZKEVM', }, @@ -67,15 +67,14 @@ const zkevmNetworkData: NetworkData = { delegationProxy: '0xc7e5ed1054a24ef31d827e6f86caa58b3bc168d7', }, balancer: { - vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', + vault: '0xba12222222228d8ba445958a75a0704d566bf2c8', composableStablePoolFactories: [ - '0x8eA89804145c007e7D226001A96955ad53836087', - '0x956CCab09898C0AF2aCa5e6C229c3aD4E93d9288', + '0x8ea89804145c007e7d226001a96955ad53836087', + '0x956ccab09898c0af2aca5e6c229c3ad4e93d9288', ], - weightedPoolV2Factories: ['0x03F3Fb107e74F2EAC9358862E91ad3c692712054'], + weightedPoolV2Factories: ['0x03f3fb107e74f2eac9358862e91ad3c692712054'], swapProtocolFeePercentage: 0.5, yieldProtocolFeePercentage: 0.5, - poolDataQueryContract: '0xF24917fB88261a37Cc57F686eBC831a5c0B9fD39', }, multicall: '0xca11bde05977b3631167028862be2a173976ca11', multicall3: '0xca11bde05977b3631167028862be2a173976ca11', @@ -100,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', @@ -115,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), @@ -194,15 +235,11 @@ export const zkevmNetworkConfig: NetworkConfig = { }, { name: 'user-sync-wallet-balances-for-all-pools', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'user-sync-staked-balances', - interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(30, 'seconds') : every(15, 'seconds'), - alarmEvaluationPeriod: 1, - alarmDatapointsToAlarm: 1, + interval: (env.DEPLOYMENT_ENV as DeploymentEnv) === 'canary' ? every(5, 'minutes') : every(2, 'minutes'), }, { name: 'sync-user-snapshots', 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..a6c64e39c 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,42 @@ 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, + token: true, + }, + }, + }, + }); + + for (const pool of filteredBoostedPoolsExpanded) { const protocolYieldFeePercentage = pool.dynamicData?.protocolYieldFee ? parseFloat(pool.dynamicData.protocolYieldFee) : networkContext.data.balancer.yieldProtocolFeePercentage; @@ -39,7 +67,7 @@ export class BoostedPoolAprService implements PoolAprService { const aprItems = await prisma.prismaPoolAprItem.findMany({ where: { poolId: { in: poolIds }, - type: { in: ['LINEAR_BOOSTED', 'PHANTOM_STABLE_BOOSTED', 'IB_YIELD'] }, + type: { in: ['LINEAR_BOOSTED', 'PHANTOM_STABLE_BOOSTED', 'IB_YIELD', 'SWAP_FEE'] }, chain: networkContext.chain, }, }); @@ -51,8 +79,9 @@ export class BoostedPoolAprService implements PoolAprService { !pool.dynamicData || !token.dynamicData || !token.nestedPool || - !token.nestedPool.dynamicData || - token.dynamicData.balanceUSD === 0 + !token.nestedPool.type || + token.dynamicData.balanceUSD === 0 || + pool.dynamicData.totalLiquidity === 0 ) { continue; } @@ -67,11 +96,15 @@ export class BoostedPoolAprService implements PoolAprService { if ( collectsYieldFee(pool) && //nested phantom stables already have the yield fee removed - token.nestedPool.type !== 'PHANTOM_STABLE' + token.nestedPool.type !== 'PHANTOM_STABLE' && + // nested tokens/bpts that dont have a rate provider, we don't take any fees + token.dynamicData.priceRate !== '1.0' ) { userApr = apr * (1 - protocolYieldFeePercentage); } + const title = aprItem.type === 'SWAP_FEE' ? `${token.token.symbol} APR` : aprItem.title; + await prisma.prismaPoolAprItem.upsert({ where: { id_chain: { id: itemId, chain: networkContext.chain } }, create: { @@ -79,10 +112,10 @@ export class BoostedPoolAprService implements PoolAprService { chain: networkContext.chain, poolId: pool.id, apr: userApr, - title: aprItem.title, + title: title, group: aprItem.group, }, - update: { apr: userApr, title: aprItem.title }, + update: { apr: userApr, title: title }, }); } } 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/masterchef-farm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts index de3609d5c..b06f1a8cf 100644 --- a/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/masterchef-farm-apr.service.ts @@ -1,6 +1,6 @@ import { PrismaPoolAprItem, PrismaTokenCurrentPrice } from '@prisma/client'; import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; +import { PrismaPoolWithExpandedNesting, PrismaPoolWithTokens } from '../../../../../prisma/prisma-types'; import { prismaBulkExecuteOperations } from '../../../../../prisma/prisma-util'; import { secondsPerYear } from '../../../../common/time'; import { blocksSubgraphService } from '../../../../subgraphs/blocks-subgraph/blocks-subgraph.service'; @@ -19,7 +19,7 @@ export class MasterchefFarmAprService implements PoolAprService { return 'MasterchefFarmAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { const farms = await masterchefService.getAllFarms({}); const blocksPerDay = await blocksSubgraphService.getBlocksPerDay(); @@ -27,7 +27,14 @@ export class MasterchefFarmAprService implements PoolAprService { const tokenPrices = await tokenService.getTokenPrices(); const operations: any[] = []; - for (const pool of pools) { + const expandedMasterchefPools = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: pools.map((pool) => pool.id) } }, + include: { + dynamicData: true, + }, + }); + + for (const pool of expandedMasterchefPools) { const farm = farms.find((farm) => { if (pool.id === networkContext.data.fbeets!.poolId) { return farm.id === networkContext.data.fbeets!.farmId; diff --git a/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts index fc3b3c2a0..6cea39e57 100644 --- a/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/reliquary-farm-apr.service.ts @@ -1,7 +1,7 @@ import { isSameAddress } from '@balancer-labs/sdk'; import { PrismaPoolAprType } from '@prisma/client'; import { prisma } from '../../../../../prisma/prisma-client'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; +import { PrismaPoolWithExpandedNesting, PrismaPoolWithTokens } from '../../../../../prisma/prisma-types'; import { prismaBulkExecuteOperations } from '../../../../../prisma/prisma-util'; import { secondsPerYear } from '../../../../common/time'; import { reliquarySubgraphService } from '../../../../subgraphs/reliquary-subgraph/reliquary.service'; @@ -16,16 +16,34 @@ export class ReliquaryFarmAprService implements PoolAprService { return 'ReliquaryFarmAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { const allSubgraphFarms = await reliquarySubgraphService.getAllFarms({}); const filteredFarms = allSubgraphFarms.filter( (farm) => !networkContext.data.reliquary!.excludedFarmIds.includes(farm.pid.toString()), ); + const expandedReliquaryPools = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: pools.map((pool) => pool.id) } }, + include: { + dynamicData: true, + staking: { + include: { + reliquary: { + include: { + levels: { + orderBy: { level: 'asc' }, + }, + }, + }, + }, + }, + }, + }); + const tokenPrices = await tokenService.getTokenPrices(); const operations: any[] = []; - for (const pool of pools) { + for (const pool of expandedReliquaryPools) { const subgraphFarm = filteredFarms.find((farm) => isSameAddress(pool.address, farm.poolTokenAddress)); let farm; for (const stake of pool.staking) { diff --git a/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts b/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts index 2cd9a9635..84bb17165 100644 --- a/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts +++ b/modules/pool/lib/apr-data-sources/fantom/spooky-swap-apr.service.ts @@ -1,5 +1,5 @@ import { PoolAprService } from '../../../pool-types'; -import { PrismaPoolWithExpandedNesting } from '../../../../../prisma/prisma-types'; +import { PrismaPoolWithExpandedNesting, PrismaPoolWithTokens } from '../../../../../prisma/prisma-types'; import axios from 'axios'; import { prisma } from '../../../../../prisma/prisma-client'; import { TokenService } from '../../../../token/token.service'; @@ -15,13 +15,28 @@ export class SpookySwapAprService implements PoolAprService { return 'SpookySwapAprService'; } - public async updateAprForPools(pools: PrismaPoolWithExpandedNesting[]): Promise { + public async updateAprForPools(pools: PrismaPoolWithTokens[]): Promise { const tokenPrices = await this.tokenService.getTokenPrices(); const xBooBaseApr = await liquidStakedBaseAprService.getXBooBaseApr(); + const expandedSpookyPools = await prisma.prismaPool.findMany({ + where: { chain: networkContext.chain, id: { in: pools.map((pool) => pool.id) } }, + include: { + dynamicData: true, + linearData: true, + tokens: { + orderBy: { index: 'asc' }, + include: { + dynamicData: true, + token: true, + }, + }, + }, + }); + let operations: any[] = []; - for (const pool of pools) { + for (const pool of expandedSpookyPools) { if ( !pool.linearData || !pool.dynamicData || 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..ccdce1f15 --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-linear-apr-handlers/sources/tessera-apr-handler.ts @@ -0,0 +1,43 @@ +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() { + 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); + Sentry.captureException(`Tessera IB APR handler failed: ${error}`); + aprEntries.push([tokenAddress, { apr: 0, isIbYield: isIbYield ?? false }]); + } + } + return Object.fromEntries(aprEntries); + } +} 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..dc3dda53c --- /dev/null +++ b/modules/pool/lib/apr-data-sources/ib-tokens-apr.service.ts @@ -0,0 +1,123 @@ +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; + } + + 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) && token.dynamicData && token.dynamicData.priceRate !== '1.0') { + 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..6b2695208 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) { @@ -67,34 +84,44 @@ export class GaugeAprService implements PoolAprService { thirdPartyApr += rewardApr; } - // apply vebal boost for BAL rewards on v2 gauges + // apply vebal boost for BAL rewards on v2 gauges on child changes or on mainnet if ( rewardToken.tokenAddress.toLowerCase() === networkContext.data.bal!.address.toLowerCase() && - preferredStaking.gauge.version === 2 + (preferredStaking.gauge.version === 2 || networkContext.chain === 'MAINNET') ) { const aprItemId = `${pool.id}-${rewardTokenDefinition.symbol}-apr`; const aprRangeId = `${pool.id}-bal-apr-range`; - // we need to create/update the range item first, as APRs can change from total to range types - // if we try to update apritem and nested range item in the same upsert, range item does not yet exist. - operations.push( - prisma.prismaPoolAprRange.upsert({ - where: { - id_chain: { id: aprRangeId, chain: networkContext.chain }, - }, - update: { - min: rewardApr, - max: rewardApr * this.MAX_VEBAL_BOOST, - }, - create: { - id: aprRangeId, - chain: networkContext.chain, - aprItemId: aprItemId, - min: rewardApr, - max: rewardApr, - }, - }), - ); + // We need gauge's workingSupply and the pool BPT price + const gaugeEmissionsUsd = rewardTokenValuePerYear; + + // Only 40% of LP token staked accrue emissions, totalSupply = workingSupply * 2.5 + const workingSupply = (parseFloat(preferredStaking.gauge.workingSupply) + 0.4) / 0.4; + const bptPrice = this.tokenService.getPriceForToken(tokenPrices, pool.address); + const workingSupplyUsd = workingSupply * bptPrice; + + let balApr = 0; + if (workingSupply > 0) { + balApr = gaugeEmissionsUsd / workingSupplyUsd; + } + + const itemData = { + id: aprItemId, + chain: networkContext.chain, + poolId: pool.id, + title: `${rewardTokenDefinition.symbol} reward APR`, + apr: balApr, + type: PrismaPoolAprType.NATIVE_REWARD, + group: null, + }; + + const rangeData = { + id: aprRangeId, + chain: networkContext.chain, + aprItemId: aprItemId, + min: balApr, + max: balApr * this.MAX_VEBAL_BOOST, + }; operations.push( prisma.prismaPoolAprItem.upsert({ @@ -104,18 +131,18 @@ export class GaugeAprService implements PoolAprService { chain: networkContext.chain, }, }, - update: { - apr: 0, - }, - create: { - id: aprItemId, - chain: networkContext.chain, - poolId: pool.id, - title: `${rewardTokenDefinition.symbol} reward APR`, - apr: 0, - type: PrismaPoolAprType.NATIVE_REWARD, - group: null, + update: itemData, + create: itemData, + }), + ); + + operations.push( + prisma.prismaPoolAprRange.upsert({ + where: { + id_chain: { id: aprRangeId, chain: networkContext.chain }, }, + update: rangeData, + create: rangeData, }), ); } else { @@ -138,6 +165,6 @@ export class GaugeAprService implements PoolAprService { } } } - await prismaBulkExecuteOperations(operations); + await prismaBulkExecuteOperations(operations, true); } } diff --git a/modules/pool/lib/pool-apr-updater.service.ts b/modules/pool/lib/pool-apr-updater.service.ts index be99509d9..4f7fd330b 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 }, }); @@ -54,7 +54,7 @@ export class PoolAprUpdaterService { } } - public async realodAllPoolAprs() { + public async reloadAllPoolAprs() { await prisma.prismaPoolAprRange.deleteMany({ where: { chain: networkContext.chain } }); await prisma.prismaPoolAprItem.deleteMany({ where: { chain: networkContext.chain } }); await this.updatePoolAprs(); diff --git a/modules/pool/lib/staking/gauge-staking.service.ts b/modules/pool/lib/staking/gauge-staking.service.ts index f0fb2abb2..d2ea55107 100644 --- a/modules/pool/lib/staking/gauge-staking.service.ts +++ b/modules/pool/lib/staking/gauge-staking.service.ts @@ -1,7 +1,7 @@ import { PoolStakingService } from '../../pool-types'; import { prisma } from '../../../../prisma/prisma-client'; import { prismaBulkExecuteOperations } from '../../../../prisma/prisma-util'; -import { PrismaPoolStakingType } from '@prisma/client'; +import { Chain, PrismaPoolStakingType } from '@prisma/client'; import { networkContext } from '../../../network/network-context.service'; import { GaugeSubgraphService, LiquidityGaugeStatus } from '../../../subgraphs/gauge-subgraph/gauge-subgraph.service'; import { formatUnits } from 'ethers/lib/utils'; @@ -9,23 +9,27 @@ import { getContractAt } from '../../../web3/contract'; import childChainGaugeV2Abi from './abi/ChildChainGaugeV2.json'; import childChainGaugeV1Abi from './abi/ChildChainGaugeV1.json'; import moment from 'moment'; -import { formatFixed } from '@ethersproject/bignumber'; +import { BigNumber, formatFixed } from '@ethersproject/bignumber'; import { Multicaller3 } from '../../../web3/multicaller3'; import _ from 'lodash'; -interface ChildChainInfo { +interface GaugeRate { /** 1 for old gauges, 2 for gauges receiving cross chain BAL rewards */ version: number; /** BAL per second received by the gauge */ rate: string; + // Amount of tokens staked in the gauge + workingSupply: string; } export class GaugeStakingService implements PoolStakingService { private balAddress: string; + constructor(private readonly gaugeSubgraphService: GaugeSubgraphService, balAddress: string) { this.balAddress = balAddress.toLowerCase(); } - public async syncStakingForPools(): Promise { + + async syncStakingForPools(): Promise { const pools = await prisma.prismaPool.findMany({ where: { chain: networkContext.chain }, }); @@ -37,7 +41,11 @@ export class GaugeStakingService implements PoolStakingService { const allGaugeAddresses = subgraphPoolsWithGauges.map((pool) => pool.gaugesList).flat(); - const childChainGaugeInfo = await this.getChildChainGaugeInfo(allGaugeAddresses); + const childChainGaugeInfo = await ( + networkContext.chain === Chain.MAINNET + ? this.getMainnetGaugeRates(allGaugeAddresses) + : this.getChildChainGaugeInfo(allGaugeAddresses) + ); for (const gaugePool of subgraphPoolsWithGauges) { const pool = pools.find((pool) => pool.id === gaugePool.poolId); @@ -83,6 +91,8 @@ export class GaugeStakingService implements PoolStakingService { }, update: { status: gaugeStatus, + version: gaugeVersion, + workingSupply: childChainGaugeInfo[gauge.id].workingSupply, }, }), ); @@ -109,8 +119,22 @@ export class GaugeStakingService implements PoolStakingService { } } if (gauge.tokens) { + const rewardTokens = await prisma.prismaToken.findMany({ + where: { + address: { in: gauge.tokens.map((token) => token.id.split('-')[0].toLowerCase()) }, + chain: networkContext.chain, + }, + }); for (let rewardToken of gauge.tokens) { const tokenAddress = rewardToken.id.split('-')[0].toLowerCase(); + const token = rewardTokens.find((token) => token.address === tokenAddress); + if (!token) { + console.error( + `Could not find reward token (${tokenAddress}) in DB for gauge ${gauge.id} of pool ${pool.id}`, + ); + continue; + } + const id = `${gauge.id}-${tokenAddress}`; let rewardRate = '0.0'; @@ -123,7 +147,7 @@ export class GaugeStakingService implements PoolStakingService { periodFinish = rewardData[2]; if (periodFinish > moment().unix()) { // period still running - rewardRate = formatFixed(rewardData[3], 18); + rewardRate = formatFixed(rewardData[3], token.decimals); } } else { // we can't get BAL rate from the reward data but got it from the inflation_rate call which set the rewardToken.rate @@ -136,7 +160,7 @@ export class GaugeStakingService implements PoolStakingService { periodFinish = parseFloat(formatUnits(rewardData[1], 0)); if (periodFinish > moment().unix()) { // period still running - rewardRate = formatFixed(rewardData[2], 18); + rewardRate = formatFixed(rewardData[2], token.decimals); } } } @@ -165,39 +189,88 @@ export class GaugeStakingService implements PoolStakingService { await prismaBulkExecuteOperations(operations, true, undefined); } - async getChildChainGaugeInfo(gaugeAddresses: string[]): Promise<{ [gaugeAddress: string]: ChildChainInfo }> { + /** + * Get the inflation rate for all the gauges on child chains. + * + * @param gaugeAddresses + * @returns + */ + private async getChildChainGaugeInfo(gaugeAddresses: string[]): Promise<{ [gaugeAddress: string]: GaugeRate }> { const currentWeek = Math.floor(Date.now() / 1000 / 604800); - const childChainAbi = networkContext.chain === 'MAINNET' - ? 'function inflation_rate() view returns (uint256)' - : 'function inflation_rate(uint256 week) view returns (uint256)'; - const multicall = new Multicaller3([childChainAbi]); - - let response: { [gaugeAddress: string]: ChildChainInfo } = {}; + const childChainAbi = [ + 'function inflation_rate(uint256 week) view returns (uint256)', + 'function working_supply() view returns (uint256)' + ]; + const multicall = new Multicaller3(childChainAbi); gaugeAddresses.forEach((address) => { - // Only L2 gauges have the inflation_rate with a week parameter - if (networkContext.chain === 'MAINNET') { - multicall.call(address, address, 'inflation_rate', [], true); - } else { - multicall.call(address, address, 'inflation_rate', [currentWeek], true); - } + multicall.call(`${address}.inflationRate`, address, 'inflation_rate', [currentWeek], true); + multicall.call(`${address}.workingSupply`, address, 'working_supply', [], true); }); - const childChainData = (await multicall.execute()) as Record; - - for (const childChainGauge in childChainData) { - if (childChainData[childChainGauge]) { - response[childChainGauge] = { - version: 2, - rate: formatUnits(childChainData[childChainGauge]!, 18), - }; - } else { - response[childChainGauge] = { - version: 1, - rate: '0.0', - }; + const results = (await multicall.execute()) as { + [address: string]: { + inflationRate: BigNumber | undefined, + workingSupply: BigNumber } - } + }; + + const response = Object.keys(results).reduce((acc, address) => { + const rate = results[address].inflationRate ? formatUnits(results[address].inflationRate!) : '0.0'; + const workingSupply = formatUnits(results[address].workingSupply); + const version = results[address].inflationRate ? 2 : 1; + acc[address] = { version, rate, workingSupply }; + return acc; + }, {} as { [gaugeAddress: string]: GaugeRate }); + + return response; + } + + /** + * Get the inflation rate for all the gauges on mainnet. + * Gauges on mainnet use the inflation rate from token admin contract + * and relative weight from the gauge contract. + * + * @param gaugeAddresses + * @returns + */ + private async getMainnetGaugeRates(gaugeAddresses: string[]): Promise<{ [gaugeAddress: string]: GaugeRate }> { + const version = 2; // On Mainnet BAL is always distributed directly to gauges + const { gaugeControllerAddress } = networkContext.data; + const abi = [ + 'function inflation_rate() view returns (uint256)', + 'function working_supply() view returns (uint256)', + 'function gauge_relative_weight(address) view returns (uint256)' + ]; + const multicall = new Multicaller3(abi); + + // On mainnet inflation rate is the same for all the gauges + multicall.call('inflation_rate', gaugeAddresses[0], 'inflation_rate', [], true); + + gaugeAddresses.forEach((address) => { + multicall.call(`${address}.weight`, gaugeControllerAddress!, 'gauge_relative_weight', [address], true); + multicall.call(`${address}.workingSupply`, address, 'working_supply', [], true); + }); + + const multicallResult = await multicall.execute() as { inflation_rate: BigNumber } & { [address: string]: { weight: BigNumber, workingSupply: BigNumber } }; + const { inflation_rate, ...gaugeData } = multicallResult; + const inflationRate = Number(formatUnits(inflation_rate!)); + + const weightedRates = _.mapValues(gaugeData, ({ weight }) => { + if (weight.eq(0)) return 0; + return inflationRate * Number(formatUnits(weight)); + }); + + const response = Object.keys(gaugeData).reduce((acc, address) => { + acc[address] = { + version, + rate: weightedRates[address] > 0 + ? weightedRates[address].toFixed(18) + : '0.0', + workingSupply: formatUnits(gaugeData[address].workingSupply) + }; + return acc; + }, {} as { [gaugeAddress: string]: GaugeRate }); return response; } @@ -207,6 +280,9 @@ export class GaugeStakingService implements PoolStakingService { await prisma.prismaUserStakedBalance.deleteMany({ where: { staking: { type: 'GAUGE', chain: networkContext.chain } }, }); + await prisma.prismaVotingGauge.deleteMany({ + where: { chain: networkContext.chain }, + }); await prisma.prismaPoolStakingGaugeReward.deleteMany({ where: { chain: networkContext.chain } }); await prisma.prismaPoolStakingGauge.deleteMany({ where: { chain: networkContext.chain } }); await prisma.prismaPoolStaking.deleteMany({ 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.gql b/modules/pool/pool.gql index c168048ae..e1bcfe7dc 100644 --- a/modules/pool/pool.gql +++ b/modules/pool/pool.gql @@ -798,6 +798,7 @@ type GqlPoolStakingGauge { rewards: [GqlPoolStakingGaugeReward!]! status: GqlPoolStakingGaugeStatus! version: Int! + workingSupply: String! # There can be more than one gauge per pool, but only one preferred. For simplicity of handling, we focus on # the primary gauge. otherGauges: [GqlPoolStakingOtherGauge!] 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/modules/pool/pool.resolvers.ts b/modules/pool/pool.resolvers.ts index 19878217c..d958ca087 100644 --- a/modules/pool/pool.resolvers.ts +++ b/modules/pool/pool.resolvers.ts @@ -137,7 +137,7 @@ const balancerResolvers: Resolvers = { poolReloadAllPoolAprs: async (parent, {}, context) => { isAdminRoute(context); - await poolService.realodAllPoolAprs(); + await poolService.reloadAllPoolAprs(); return 'success'; }, diff --git a/modules/pool/pool.service.ts b/modules/pool/pool.service.ts index f1e42fcd9..852a03bc0 100644 --- a/modules/pool/pool.service.ts +++ b/modules/pool/pool.service.ts @@ -264,8 +264,8 @@ export class PoolService { await this.poolSyncService.syncChangedPools(); } - public async realodAllPoolAprs() { - await this.poolAprUpdaterService.realodAllPoolAprs(); + public async reloadAllPoolAprs() { + await this.poolAprUpdaterService.reloadAllPoolAprs(); } public async updateLiquidity24hAgoForAllPools() { @@ -438,6 +438,10 @@ export class PoolService { await prisma.prismaPoolLinearData.deleteMany({ where: { chain: networkContext.chain, poolId: poolId }, }); + + await prisma.prismaPoolGyroData.deleteMany({ + where: { chain: networkContext.chain, poolId: poolId }, + }); await prisma.prismaPoolExpandedTokens.deleteMany({ where: { chain: networkContext.chain, poolId: poolId }, @@ -466,6 +470,22 @@ export class PoolService { where: { chain: networkContext.chain, gaugeId: staking.id }, }); + // delete votingGauge entry before deleting the staking gauge + let gauge = await prisma.prismaPoolStakingGauge.findFirst({ + where: { + chain: networkContext.chain, + stakingId: staking.id, + }, + select: { + votingGauge: true, + }, + }); + + if(gauge && gauge.votingGauge) + await prisma.prismaVotingGauge.deleteMany({ + where: { chain: networkContext.chain, id: gauge.votingGauge.id } + }); + await prisma.prismaPoolStakingGauge.deleteMany({ where: { chain: networkContext.chain, stakingId: staking.id }, }); 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/migrations/20230911145905_add_working_supplies/migration.sql b/prisma/migrations/20230911145905_add_working_supplies/migration.sql new file mode 100644 index 000000000..c922dca4b --- /dev/null +++ b/prisma/migrations/20230911145905_add_working_supplies/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "PrismaPoolStakingGauge" ADD COLUMN "workingSupply" TEXT NOT NULL DEFAULT '0.0'; 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..bace27203 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 { @@ -460,6 +470,7 @@ model PrismaPoolStakingGauge { rewards PrismaPoolStakingGaugeReward[] status PrismaPoolStakingGaugeStatus @default(ACTIVE) version Int @default(1) + workingSupply String @default("0.0") } enum PrismaPoolStakingGaugeStatus { diff --git a/worker/job-handlers.ts b/worker/job-handlers.ts index 82a56ea7a..5046e65c3 100644 --- a/worker/job-handlers.ts +++ b/worker/job-handlers.ts @@ -34,6 +34,11 @@ async function runIfNotAlreadyRunning( try { runningJobs.add(jobId); + // TODO, this does not seem to work properly as it is a "global" scope + Sentry.configureScope((scope) => { + scope.setTransactionName(`POST /${jobId}`); + }); + console.time(jobId); console.log(`Start job ${jobId}-start`); diff --git a/worker/worker.ts b/worker/worker.ts index 311703b5c..325aefb2d 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -14,7 +14,6 @@ export async function startWorker() { // new Tracing.Integrations.Express({ app }), new Sentry.Integrations.Http({ tracing: true }), ], - sampleRate: 0, }); app.use(Sentry.Handlers.requestHandler());