Skip to content

Commit

Permalink
Merge pull request #159 from balancer-labs/develop
Browse files Browse the repository at this point in the history
v0.1.26
  • Loading branch information
johngrantuk authored Sep 28, 2022
2 parents 162cea1 + 02b107b commit 48c764b
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 41 deletions.
2 changes: 1 addition & 1 deletion balancer-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@balancer-labs/sdk",
"version": "0.1.25",
"version": "0.1.26",
"description": "JavaScript SDK for interacting with the Balancer Protocol V2",
"license": "GPL-3.0-only",
"homepage": "https://github.com/balancer-labs/balancer-sdk/balancer-js#readme",
Expand Down
2 changes: 2 additions & 0 deletions balancer-js/src/lib/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const BALANCER_NETWORK_CONFIG: Record<Network, BalancerNetworkConfig> = {
lidoRelayer: '0xdcdbf71A870cc60C6F9B621E28a7D3Ffd6Dd4965',
gaugeController: '0xc128468b7ce63ea702c1f104d55a2566b13d3abd',
feeDistributor: '0xD3cf852898b21fc233251427c2DC93d3d604F3BB',
protocolFeePercentagesProvider:
'0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A',
},
tokens: {
wrappedNativeAsset: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
Expand Down
11 changes: 11 additions & 0 deletions balancer-js/src/modules/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './token';
export * from './token-prices';
export * from './fee-distributor/repository';
export * from './fee-collector/repository';
export * from './protocol-fees/provider';
export * from './token-yields/repository';
export * from './block-number';

Expand All @@ -18,6 +19,7 @@ import { LiquidityGaugeSubgraphRPCProvider } from './liquidity-gauges/provider';
import { FeeDistributorRepository } from './fee-distributor/repository';
import { FeeCollectorRepository } from './fee-collector/repository';
import { TokenYieldsRepository } from './token-yields/repository';
import { ProtocolFeesProvider } from './protocol-fees/provider';
import { Provider } from '@ethersproject/providers';

// initialCoingeckoList are used to get the initial token list for coingecko
Expand All @@ -32,6 +34,7 @@ export class Data implements BalancerDataRepositories {
liquidityGauges;
feeDistributor;
feeCollector;
protocolFees;
tokenYields;
blockNumbers;

Expand Down Expand Up @@ -103,6 +106,14 @@ export class Data implements BalancerDataRepositories {
provider
);

if (networkConfig.addresses.contracts.protocolFeePercentagesProvider) {
this.protocolFees = new ProtocolFeesProvider(
networkConfig.addresses.contracts.multicall,
networkConfig.addresses.contracts.protocolFeePercentagesProvider,
provider
);
}

this.tokenYields = new TokenYieldsRepository();
}
}
68 changes: 68 additions & 0 deletions balancer-js/src/modules/data/protocol-fees/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A

import { Interface } from '@ethersproject/abi';
import { Provider } from '@ethersproject/providers';
import { Contract } from '@ethersproject/contracts';
import { formatUnits } from '@ethersproject/units';
import { Multicall } from '@/modules/contracts/multicall';

const iProtocolFeePercentagesProvider = new Interface([
'function getSwapFeePercentage() view returns (uint)',
]);

export interface ProtocolFees {
swapFee: number;
yieldFee: number;
}

// Using singleton here, so subsequent calls will return the same promise
let feesPromise: Promise<ProtocolFees>;

export class ProtocolFeesProvider {
multicall: Contract;
protocolFees?: ProtocolFees;

constructor(
multicallAddress: string,
private protocolFeePercentagesProviderAddress: string,
provider: Provider
) {
this.multicall = Multicall(multicallAddress, provider);
}

private async fetch(): Promise<ProtocolFees> {
const payload = [
[
this.protocolFeePercentagesProviderAddress,
iProtocolFeePercentagesProvider.encodeFunctionData(
'getFeeTypePercentage',
[0]
),
],
[
this.protocolFeePercentagesProviderAddress,
iProtocolFeePercentagesProvider.encodeFunctionData(
'getFeeTypePercentage',
[2]
),
],
];
const [, res] = await this.multicall.aggregate(payload);

const fees = {
swapFee: parseFloat(formatUnits(res[0], 18)),
yieldFee: parseFloat(formatUnits(res[2], 18)),
};

return fees;
}

async getFees(): Promise<ProtocolFees> {
if (!feesPromise) {
feesPromise = this.fetch();
}
this.protocolFees = await feesPromise;

return this.protocolFees;
}
}
133 changes: 97 additions & 36 deletions balancer-js/src/modules/data/token-prices/coingecko.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,126 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { Price, Findable, TokenPrices } from '@/types';
import { wrappedTokensMap as aaveWrappedMap } from '../token-yields/tokens/aave';
import axios from 'axios';

// Conscious choice for a deferred promise since we have setTimeout that returns a promise
// Some reference for history buffs: https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns
interface PromisedTokenPrices {
promise: Promise<TokenPrices>;
resolve: (value: TokenPrices) => void;
reject: (reason: unknown) => void;
}

const makePromise = (): PromisedTokenPrices => {
let resolve: (value: TokenPrices) => void = () => {};
let reject: (reason: unknown) => void = () => {};
const promise = new Promise<TokenPrices>((res, rej) => {
[resolve, reject] = [res, rej];
});
return { promise, reject, resolve };
};

/**
* Simple coingecko price source implementation. Configurable by network and token addresses.
*/
export class CoingeckoPriceRepository implements Findable<Price> {
prices: TokenPrices = {};
fetching: { [address: string]: Promise<TokenPrices> } = {};
urlBase: string;
baseTokenAddresses: string[];

// Properties used for deferring API calls
// TODO: move this logic to hooks
requestedAddresses = new Set<string>(); // Accumulates requested addresses
debounceWait = 200; // Debouncing waiting time [ms]
promisedCalls: PromisedTokenPrices[] = []; // When requesting a price we return a deferred promise
promisedCount = 0; // New request coming when setTimeout is executing will make a new promise
timeout?: ReturnType<typeof setTimeout>;
debounceCancel = (): void => {}; // Allow to cancel mid-flight requests

constructor(tokenAddresses: string[], chainId = 1) {
this.baseTokenAddresses = tokenAddresses.map((a) => a.toLowerCase());
this.baseTokenAddresses = tokenAddresses
.map((a) => a.toLowerCase())
.map((a) => unwrapToken(a));
this.urlBase = `https://api.coingecko.com/api/v3/simple/token_price/${this.platform(
chainId
)}?vs_currencies=usd,eth`;
}

fetch(address: string): { [address: string]: Promise<TokenPrices> } {
console.time(`fetching coingecko ${address}`);
const addresses = this.addresses(address);
const prices = axios
.get(this.url(addresses))
private fetch(
addresses: string[],
{ signal }: { signal?: AbortSignal } = {}
): Promise<TokenPrices> {
console.time(`fetching coingecko for ${addresses.length} tokens`);
return axios
.get<TokenPrices>(this.url(addresses), { signal })
.then(({ data }) => {
addresses.forEach((address) => {
delete this.fetching[address];
});
this.prices = {
...this.prices,
...(Object.keys(data).length == 0 ? { [address]: {} } : data),
};
return this.prices;
return data;
})
.catch((error) => {
console.error(error);
return this.prices;
.finally(() => {
console.timeEnd(`fetching coingecko for ${addresses.length} tokens`);
});
console.timeEnd(`fetching coingecko ${address}`);
return Object.fromEntries(addresses.map((a) => [a, prices]));
}

private debouncedFetch(): Promise<TokenPrices> {
if (!this.promisedCalls[this.promisedCount]) {
this.promisedCalls[this.promisedCount] = makePromise();
}

const { promise, resolve, reject } = this.promisedCalls[this.promisedCount];

if (this.timeout) {
clearTimeout(this.timeout);
}

this.timeout = setTimeout(() => {
this.promisedCount++; // any new call will get a new promise
this.fetch([...this.requestedAddresses])
.then((results) => {
resolve(results);
this.debounceCancel = () => {};
})
.catch((reason) => {
console.error(reason);
});
}, this.debounceWait);

this.debounceCancel = () => {
if (this.timeout) {
clearTimeout(this.timeout);
}
reject('Cancelled');
delete this.promisedCalls[this.promisedCount];
};

return promise;
}

async find(address: string): Promise<Price | undefined> {
const lowercaseAddress = address.toLowerCase();
const unwrapped = unwrapToken(lowercaseAddress);
if (Object.keys(this.fetching).includes(unwrapped)) {
await this.fetching[unwrapped];
} else if (!Object.keys(this.prices).includes(unwrapped)) {
this.fetching = {
...this.fetching,
...this.fetch(unwrapped),
};
await this.fetching[unwrapped];
if (!this.prices[unwrapped]) {
try {
let init = false;
if (Object.keys(this.prices).length === 0) {
// Make initial call with all the tokens we want to preload
this.baseTokenAddresses.forEach(
this.requestedAddresses.add.bind(this.requestedAddresses)
);
init = true;
}
this.requestedAddresses.add(unwrapped);
const promised = await this.debouncedFetch();
this.prices[unwrapped] = promised[unwrapped];
this.requestedAddresses.delete(unwrapped);
if (init) {
this.baseTokenAddresses.forEach((a) => {
this.prices[a] = promised[a];
this.requestedAddresses.delete(a);
});
}
} catch (error) {
console.error(error);
}
}

return this.prices[unwrapped];
Expand Down Expand Up @@ -83,14 +152,6 @@ export class CoingeckoPriceRepository implements Findable<Price> {
private url(addresses: string[]): string {
return `${this.urlBase}&contract_addresses=${addresses.join(',')}`;
}

private addresses(address: string): string[] {
if (this.baseTokenAddresses.includes(address)) {
return this.baseTokenAddresses;
} else {
return [address];
}
}
}

const unwrapToken = (wrappedAddress: string) => {
Expand Down
1 change: 1 addition & 0 deletions balancer-js/src/modules/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { LiquidityGauge } from './liquidity-gauges/provider';
export { PoolAttribute } from './pool/types';
export { TokenAttribute } from './token/types';
export { ProtocolFees } from './protocol-fees/provider';

export interface Findable<T, P = string> {
find: (id: string) => Promise<T | undefined>;
Expand Down
32 changes: 29 additions & 3 deletions balancer-js/src/modules/pools/apr/apr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import type {
TokenAttribute,
LiquidityGauge,
} from '@/types';
import { BaseFeeDistributor, RewardData } from '@/modules/data';
import {
BaseFeeDistributor,
ProtocolFeesProvider,
RewardData,
} from '@/modules/data';
import { ProtocolRevenue } from './protocol-revenue';
import { Liquidity } from '@/modules/liquidity/liquidity.module';
import { identity, zipObject, pickBy } from 'lodash';
Expand Down Expand Up @@ -54,7 +58,8 @@ export class PoolApr {
private feeCollector: Findable<number>,
private yesterdaysPools?: Findable<Pool, PoolAttribute>,
private liquidityGauges?: Findable<LiquidityGauge>,
private feeDistributor?: BaseFeeDistributor
private feeDistributor?: BaseFeeDistributor,
private protocolFees?: ProtocolFeesProvider
) {}

/**
Expand Down Expand Up @@ -106,7 +111,17 @@ export class PoolApr {
const tokenYield = await this.tokenYields.find(token.address);

if (tokenYield) {
apr = tokenYield;
if (pool.poolType === 'MetaStable') {
apr = tokenYield * (1 - (await this.protocolSwapFeePercentage()));
} else if (pool.poolType === 'ComposableStable') {
// TODO: add if(token.isTokenExemptFromYieldProtocolFee) once supported by subgraph
// apr = tokenYield;

const fees = await this.protocolFeesPercentage();
apr = tokenYield * (1 - fees.yieldFee);
} else {
apr = tokenYield;
}
} else {
// Handle subpool APRs with recursive call to get the subPool APR
const subPool = await this.pools.findBy('address', token.address);
Expand Down Expand Up @@ -383,6 +398,17 @@ export class PoolApr {
return fee ? fee : 0;
}

private async protocolFeesPercentage() {
if (this.protocolFees) {
return await this.protocolFees.getFees();
}

return {
swapFee: 0,
yieldFee: 0,
};
}

private async rewardTokenApr(tokenAddress: string, rewardData: RewardData) {
if (rewardData.period_finish.toNumber() < Date.now() / 1000) {
return {
Expand Down
5 changes: 5 additions & 0 deletions balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ fragment SubgraphPoolToken on PoolToken {
managedBalance
weight
priceRate
token {
pool {
poolType
}
}
}

query PoolHistoricalLiquidities(
Expand Down
4 changes: 3 additions & 1 deletion balancer-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
PoolAttribute,
TokenAttribute,
} from '@/modules/data/types';
import type { BaseFeeDistributor } from './modules/data';
import type { BaseFeeDistributor, ProtocolFeesProvider } from './modules/data';
import type { GraphQLArgs } from './lib/graphql';

import type { AprBreakdown } from '@/modules/pools/apr/apr';
Expand Down Expand Up @@ -50,6 +50,7 @@ export interface ContractAddresses {
lidoRelayer?: string;
gaugeController?: string;
feeDistributor?: string;
protocolFeePercentagesProvider?: string;
}

export interface BalancerNetworkConfig {
Expand Down Expand Up @@ -84,6 +85,7 @@ export interface BalancerDataRepositories {
liquidityGauges?: Findable<LiquidityGauge>;
feeDistributor?: BaseFeeDistributor;
feeCollector: Findable<number>;
protocolFees?: ProtocolFeesProvider;
tokenYields: Findable<number>;
}

Expand Down

0 comments on commit 48c764b

Please sign in to comment.