Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

fix(velodrome): Fix claimables and extract external api calls into a resolver #2229

Merged
merged 1 commit into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/apps/velodrome/common/velodrome.definitions-resolver.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,75 +10,46 @@ 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 = {
address: string;
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<VelodromeBribe> {
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<VelodromeBribeDefinition[]> {
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<VelodromeBribe>) {
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,
};
});
Expand All @@ -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 => {
Expand All @@ -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);
Expand All @@ -148,6 +121,7 @@ export class OptimismVelodromeBribeContractPositionFetcher extends ContractPosit
return balance;
}),
);

return _.compact(balances);
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,17 +14,17 @@ 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<VelodromeGauge> {
groupLabel = 'Staking';

constructor(
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
@Inject(VelodromeContractFactory) protected readonly contractFactory: VelodromeContractFactory,
@Inject(VelodromeDefinitionsResolver) protected readonly definitionsResolver: VelodromeDefinitionsResolver,
) {
super(appToolkit);
}
Expand All @@ -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<VelodromeGauge>) {
Expand Down
44 changes: 31 additions & 13 deletions src/apps/velodrome/optimism/velodrome.pool.token-fetcher.ts
Original file line number Diff line number Diff line change
@@ -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<VelodromePool> {
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);
}
Expand All @@ -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<VelodromePoolTokenDefinition[]> {
return this.definitionsResolver.getPoolTokenDefinitions();
}

async getAddresses({ definitions }: GetAddressesParams) {
return definitions.map(v => v.address);
}

async getUnderlyingTokenDefinitions({ contract }: GetUnderlyingTokensParams<VelodromePool>) {
Expand All @@ -62,18 +73,25 @@ export class OptimismVelodromePoolsTokenFetcher extends AppTokenTemplatePosition
return appToken.tokens.map(v => getLabelFromToken(v)).join(' / ');
}

async getApy({
definition,
}: GetDataPropsParams<VelodromePool, DefaultAppTokenDataProps, VelodromePoolTokenDefinition>): Promise<number> {
return definition.apy;
}

async getSecondaryLabel({ appToken }: GetDisplayPropsParams<VelodromePool>) {
const { liquidity, reserves } = appToken.dataProps;
const reservePercentages = appToken.tokens.map((t, i) => reserves[i] * (t.price / liquidity));
return reservePercentages.map(p => `${Math.round(p * 100)}%`).join(' / ');
}

async getStatsItems({ appToken }: GetDisplayPropsParams<VelodromePool>) {
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 },
];
}
Expand Down
9 changes: 5 additions & 4 deletions src/apps/velodrome/velodrome.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}