Skip to content

Commit

Permalink
fix(velodrome): Fix claimables and extract external api calls into a …
Browse files Browse the repository at this point in the history
…resolver (Zapper-fi#2229)
  • Loading branch information
wpoulin authored and 0xdapper committed Feb 28, 2023
1 parent dea7f66 commit d2f1900
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 71 deletions.
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() {}

0 comments on commit d2f1900

Please sign in to comment.