From 36b45137f131949a55d4eefd3f7bf41b7e654b0b Mon Sep 17 00:00:00 2001 From: William Poulin Date: Thu, 2 Feb 2023 12:54:06 -0500 Subject: [PATCH] fix(velodrome): Fix claimables and extract external api calls into a resolver (#2229) --- .../common/velodrome.definitions-resolver.ts | 62 ++++++++++++++++ ...lodrome.bribe.contract-position-fetcher.ts | 70 ++++++------------- ...elodrome.farm.contract-position-fetcher.ts | 9 +-- .../optimism/velodrome.pool.token-fetcher.ts | 44 ++++++++---- src/apps/velodrome/velodrome.module.ts | 9 +-- 5 files changed, 123 insertions(+), 71 deletions(-) create mode 100644 src/apps/velodrome/common/velodrome.definitions-resolver.ts diff --git a/src/apps/velodrome/common/velodrome.definitions-resolver.ts b/src/apps/velodrome/common/velodrome.definitions-resolver.ts new file mode 100644 index 000000000..96133e339 --- /dev/null +++ b/src/apps/velodrome/common/velodrome.definitions-resolver.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import Axios from 'axios'; +import _ from 'lodash'; + +import { Cache } from '~cache/cache.decorator'; + +export interface VelodromeApiPairData { + address: string; + symbol: string; + gauge_address: string; + token0_address: string; + token1_address: string; + apr: number; + gauge: { + wrapped_bribe_address: string; + }; +} + +@Injectable() +export class VelodromeDefinitionsResolver { + @Cache({ + key: `studio:velodrome:pool-token-data`, + ttl: 5 * 60, // 60 minutes + }) + private async getPoolDefinitionsData() { + const url = `htthttps://api.velodrome.finance/api/v1/pairs`; + const { data } = await Axios.get<{ data: VelodromeApiPairData[] }>(url); + + return data.data; + } + + async getPoolTokenDefinitions() { + const definitionsData = await this.getPoolDefinitionsData(); + + return definitionsData.map(pool => ({ + address: pool.address.toLowerCase(), + apy: pool.apr, + })); + } + + async getFarmAddresses() { + const definitionsData = await this.getPoolDefinitionsData(); + + return definitionsData.map(pool => pool.gauge_address.toLowerCase()).filter(v => !!v); + } + + async getBribeDefinitions() { + const definitionsData = await this.getPoolDefinitionsData(); + + const definitionsRaw = definitionsData + .filter(v => !!v) + .filter(v => !!v.gauge) + .map(pool => { + const wBribeAddress = pool.gauge.wrapped_bribe_address; + if (wBribeAddress == null) return null; + + return { address: wBribeAddress.toLowerCase(), name: pool.symbol }; + }); + + return _.compact(definitionsRaw); + } +} diff --git a/src/apps/velodrome/optimism/velodrome.bribe.contract-position-fetcher.ts b/src/apps/velodrome/optimism/velodrome.bribe.contract-position-fetcher.ts index be48d0ab7..52ae7b907 100644 --- a/src/apps/velodrome/optimism/velodrome.bribe.contract-position-fetcher.ts +++ b/src/apps/velodrome/optimism/velodrome.bribe.contract-position-fetcher.ts @@ -1,5 +1,4 @@ import { Inject, NotImplementedException } from '@nestjs/common'; -import axios from 'axios'; import _, { range, sumBy } from 'lodash'; import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; @@ -11,6 +10,7 @@ import { MetaType } from '~position/position.interface'; import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher'; import { GetDisplayPropsParams, GetTokenDefinitionsParams } from '~position/template/contract-position.template.types'; +import { VelodromeDefinitionsResolver } from '../common/velodrome.definitions-resolver'; import { VelodromeContractFactory, VelodromeBribe } from '../contracts'; export type VelodromeBribeDefinition = { @@ -18,68 +18,38 @@ export type VelodromeBribeDefinition = { name: string; }; -interface VelodromeApiPairData { - gauge: GaugeData; - symbol: string; - address: string; -} - -interface GaugeData { - wrapped_bribe_address: string; -} - -// Pools which contains optiDoge -const notSupportedPoolTokenAddresses = [ - '0xce9accfbb25eddce91845c3a7c3d1613d1d7081f', - '0x22bc9c46b72b2e92f0539d18d1f2273ee0e7f3fc', - '0x763cbb83cb837114fded11d562bcbf3d58a682ac', - '0x4ecafe1a9798e4e874f5df212377615ae357b566', - '0x415c6f07757ab86d902eeca5055fcb3ca974b880', - '0xb22cd502a49e90e4e6200921a41a7f065e9a1b9e', - '0x6a938eddf290ca29a0865ae613d52c160ce4554b', - '0xc23ab0245e23c22bf306a62dc9fe2958cdcf37b0', - '0x103f2556cb47eaf2161e700f15716f65711ed983', - '0xce57d093bf2ce0bd02e83c364c9bd766be2212b2', -]; - @PositionTemplate() export class OptimismVelodromeBribeContractPositionFetcher extends ContractPositionTemplatePositionFetcher { + groupLabel = 'Bribe'; + + veTokenAddress = '0x9c7305eb78a432ced5c4d14cac27e8ed569a2e26'; + constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(VelodromeContractFactory) protected readonly contractFactory: VelodromeContractFactory, + @Inject(VelodromeDefinitionsResolver) protected readonly definitionsResolver: VelodromeDefinitionsResolver, ) { super(appToolkit); } - veTokenAddress = '0x9c7305eb78a432ced5c4d14cac27e8ed569a2e26'; - veVoteAddress = '0x09236cff45047dbee6b921e00704bed6d6b8cf7e'; - groupLabel = 'Bribe'; getContract(address: string): VelodromeBribe { return this.contractFactory.velodromeBribe({ address, network: this.network }); } async getDefinitions(): Promise { - const { data } = await axios.get<{ data: VelodromeApiPairData[] }>('https://api.velodrome.finance/api/v1/pairs'); - const definitions = data.data - .filter(v => !!v) - .filter(v => !!v.gauge) - .map(pool => { - const wBribeAddress = pool.gauge.wrapped_bribe_address; - return wBribeAddress != null ? { address: wBribeAddress, name: pool.symbol } : null; - }); - - return _.compact(definitions); + return this.definitionsResolver.getBribeDefinitions(); } async getTokenDefinitions({ contract }: GetTokenDefinitionsParams) { const numRewards = Number(await contract.rewardsListLength()); const bribeTokens = await Promise.all(range(numRewards).map(async n => await contract.rewards(n))); - const tokenDefinitions = bribeTokens.map(address => { - if (notSupportedPoolTokenAddresses.includes(address)) return null; - + const baseTokens = await this.appToolkit.getBaseTokenPrices(this.network); + const tokenDefinitions = bribeTokens.map(token => { + const tokenFound = baseTokens.find(p => p.address === token.toLowerCase()); + if (!tokenFound) return null; return { metaType: MetaType.CLAIMABLE, - address, + address: token, network: this.network, }; }); @@ -103,7 +73,9 @@ export class OptimismVelodromeBribeContractPositionFetcher extends ContractPosit this.contractFactory.velodromeVe({ address: this.veTokenAddress, network: this.network }), ); const veCount = Number(await escrow.balanceOf(address)); - if (veCount === 0) return []; + if (veCount === 0) { + return []; + } const veTokenIds = await Promise.all( range(veCount).map(async i => { @@ -125,19 +97,20 @@ export class OptimismVelodromeBribeContractPositionFetcher extends ContractPosit const tokenBalancesRaw = await Promise.all( bribeTokens.map(async bribeToken => { - return Promise.all( + const tokenBalancePerBribe = await Promise.all( veTokenIds.map(async veTokenId => { - return await multicall.wrap(bribeContract).earned(bribeToken.address, veTokenId); + const balance = await multicall.wrap(bribeContract).earned(bribeToken.address, veTokenId); + return Number(balance); }), ); + return tokenBalancePerBribe; }), ); - const tokenBalances = tokenBalancesRaw.map(x => Number(x)).flat(); + const tokenBalances = tokenBalancesRaw.flat(); - const nonZeroBalancesRaw = tokenBalances.filter(balance => balance > 0); const allTokens = contractPosition.tokens.map((cp, idx) => - drillBalance(cp, nonZeroBalancesRaw[idx]?.toString() ?? '0'), + drillBalance(cp, tokenBalances[idx]?.toString() ?? '0'), ); const tokens = allTokens.filter(v => Math.abs(v.balanceUSD) > 0.01); @@ -148,6 +121,7 @@ export class OptimismVelodromeBribeContractPositionFetcher extends ContractPosit return balance; }), ); + return _.compact(balances); } } diff --git a/src/apps/velodrome/optimism/velodrome.farm.contract-position-fetcher.ts b/src/apps/velodrome/optimism/velodrome.farm.contract-position-fetcher.ts index 3b7575cb7..3dff43504 100644 --- a/src/apps/velodrome/optimism/velodrome.farm.contract-position-fetcher.ts +++ b/src/apps/velodrome/optimism/velodrome.farm.contract-position-fetcher.ts @@ -1,5 +1,4 @@ import { Inject } from '@nestjs/common'; -import axios from 'axios'; import { range } from 'lodash'; import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface'; @@ -15,10 +14,9 @@ import { SingleStakingFarmDynamicTemplateContractPositionFetcher, } from '~position/template/single-staking.dynamic.template.contract-position-fetcher'; +import { VelodromeDefinitionsResolver } from '../common/velodrome.definitions-resolver'; import { VelodromeContractFactory, VelodromeGauge } from '../contracts'; -import { VelodromeApiPairData } from './velodrome.pool.token-fetcher'; - @PositionTemplate() export class OptimismVelodromeStakingContractPositionFetcher extends SingleStakingFarmDynamicTemplateContractPositionFetcher { groupLabel = 'Staking'; @@ -26,6 +24,7 @@ export class OptimismVelodromeStakingContractPositionFetcher extends SingleStaki constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(VelodromeContractFactory) protected readonly contractFactory: VelodromeContractFactory, + @Inject(VelodromeDefinitionsResolver) protected readonly definitionsResolver: VelodromeDefinitionsResolver, ) { super(appToolkit); } @@ -35,9 +34,7 @@ export class OptimismVelodromeStakingContractPositionFetcher extends SingleStaki } async getFarmAddresses() { - const { data } = await axios.get<{ data: VelodromeApiPairData[] }>('https://api.velodrome.finance/api/v1/pairs'); - const gaugeAddresses = data.data.map(pool => pool.gauge_address).filter(v => !!v); - return gaugeAddresses; + return this.definitionsResolver.getFarmAddresses(); } async getStakedTokenAddress({ contract }: GetTokenDefinitionsParams) { diff --git a/src/apps/velodrome/optimism/velodrome.pool.token-fetcher.ts b/src/apps/velodrome/optimism/velodrome.pool.token-fetcher.ts index 56e8ff51f..dccf94ef2 100644 --- a/src/apps/velodrome/optimism/velodrome.pool.token-fetcher.ts +++ b/src/apps/velodrome/optimism/velodrome.pool.token-fetcher.ts @@ -1,35 +1,43 @@ import { Inject } from '@nestjs/common'; -import Axios from 'axios'; import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; -import { buildDollarDisplayItem } from '~app-toolkit/helpers/presentation/display-item.present'; +import { + buildDollarDisplayItem, + buildPercentageDisplayItem, +} from '~app-toolkit/helpers/presentation/display-item.present'; import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present'; import { DefaultDataProps } from '~position/display.interface'; import { AppTokenTemplatePositionFetcher } from '~position/template/app-token.template.position-fetcher'; import { + DefaultAppTokenDataProps, + GetAddressesParams, + GetDataPropsParams, GetDisplayPropsParams, GetPricePerShareParams, GetUnderlyingTokensParams, } from '~position/template/app-token.template.types'; +import { VelodromeDefinitionsResolver } from '../common/velodrome.definitions-resolver'; import { VelodromeContractFactory, VelodromePool } from '../contracts'; -export interface VelodromeApiPairData { +export type VelodromePoolTokenDefinition = { address: string; - gauge_address: string; - token0_address: string; - token1_address: string; - apr: number; -} + apy: number; +}; @PositionTemplate() -export class OptimismVelodromePoolsTokenFetcher extends AppTokenTemplatePositionFetcher { +export class OptimismVelodromePoolsTokenFetcher extends AppTokenTemplatePositionFetcher< + VelodromePool, + DefaultAppTokenDataProps, + VelodromePoolTokenDefinition +> { groupLabel = 'Pools'; constructor( @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, @Inject(VelodromeContractFactory) private readonly contractFactory: VelodromeContractFactory, + @Inject(VelodromeDefinitionsResolver) protected readonly definitionsResolver: VelodromeDefinitionsResolver, ) { super(appToolkit); } @@ -38,9 +46,12 @@ export class OptimismVelodromePoolsTokenFetcher extends AppTokenTemplatePosition return this.contractFactory.velodromePool({ address, network: this.network }); } - async getAddresses() { - const { data } = await Axios.get<{ data: VelodromeApiPairData[] }>('https://api.velodrome.finance/api/v1/pairs'); - return data.data.map(pool => pool.address); + async getDefinitions(): Promise { + return this.definitionsResolver.getPoolTokenDefinitions(); + } + + async getAddresses({ definitions }: GetAddressesParams) { + return definitions.map(v => v.address); } async getUnderlyingTokenDefinitions({ contract }: GetUnderlyingTokensParams) { @@ -62,6 +73,12 @@ export class OptimismVelodromePoolsTokenFetcher extends AppTokenTemplatePosition return appToken.tokens.map(v => getLabelFromToken(v)).join(' / '); } + async getApy({ + definition, + }: GetDataPropsParams): Promise { + return definition.apy; + } + async getSecondaryLabel({ appToken }: GetDisplayPropsParams) { const { liquidity, reserves } = appToken.dataProps; const reservePercentages = appToken.tokens.map((t, i) => reserves[i] * (t.price / liquidity)); @@ -69,11 +86,12 @@ export class OptimismVelodromePoolsTokenFetcher extends AppTokenTemplatePosition } async getStatsItems({ appToken }: GetDisplayPropsParams) { - const { reserves, liquidity } = appToken.dataProps; + const { reserves, liquidity, apy } = appToken.dataProps; const reservesDisplay = reserves.map(v => (v < 0.01 ? '<0.01' : v.toFixed(2))).join(' / '); return [ { label: 'Liquidity', value: buildDollarDisplayItem(liquidity) }, + { label: 'APY', value: buildPercentageDisplayItem(apy) }, { label: 'Reserves', value: reservesDisplay }, ]; } diff --git a/src/apps/velodrome/velodrome.module.ts b/src/apps/velodrome/velodrome.module.ts index 68a599e01..55db039f9 100644 --- a/src/apps/velodrome/velodrome.module.ts +++ b/src/apps/velodrome/velodrome.module.ts @@ -2,20 +2,21 @@ import { Module } from '@nestjs/common'; import { AbstractApp } from '~app/app.dynamic-module'; +import { VelodromeDefinitionsResolver } from './common/velodrome.definitions-resolver'; import { VelodromeContractFactory } from './contracts'; +import { OptimismVelodromeBribeContractPositionFetcher } from './optimism/velodrome.bribe.contract-position-fetcher'; import { OptimismVelodromeStakingContractPositionFetcher } from './optimism/velodrome.farm.contract-position-fetcher'; import { OptimismVelodromePoolsTokenFetcher } from './optimism/velodrome.pool.token-fetcher'; import { OptimismVelodromeVotingEscrowContractPositionFetcher } from './optimism/velodrome.voting-escrow.contract-position-fetcher'; -import { OptimismVelodromeBribeContractPositionFetcher } from './optimism/velodrome.bribe.contract-position-fetcher'; @Module({ providers: [ + VelodromeContractFactory, + VelodromeDefinitionsResolver, OptimismVelodromePoolsTokenFetcher, OptimismVelodromeStakingContractPositionFetcher, OptimismVelodromeVotingEscrowContractPositionFetcher, OptimismVelodromeBribeContractPositionFetcher, - - VelodromeContractFactory, ], }) -export class VelodromeAppModule extends AbstractApp() { } +export class VelodromeAppModule extends AbstractApp() {}