Skip to content

Commit 48c764b

Browse files
authored
Merge pull request #159 from balancer-labs/develop
v0.1.26
2 parents 162cea1 + 02b107b commit 48c764b

File tree

9 files changed

+217
-41
lines changed

9 files changed

+217
-41
lines changed

balancer-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@balancer-labs/sdk",
3-
"version": "0.1.25",
3+
"version": "0.1.26",
44
"description": "JavaScript SDK for interacting with the Balancer Protocol V2",
55
"license": "GPL-3.0-only",
66
"homepage": "https://github.com/balancer-labs/balancer-sdk/balancer-js#readme",

balancer-js/src/lib/constants/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const BALANCER_NETWORK_CONFIG: Record<Network, BalancerNetworkConfig> = {
1313
lidoRelayer: '0xdcdbf71A870cc60C6F9B621E28a7D3Ffd6Dd4965',
1414
gaugeController: '0xc128468b7ce63ea702c1f104d55a2566b13d3abd',
1515
feeDistributor: '0xD3cf852898b21fc233251427c2DC93d3d604F3BB',
16+
protocolFeePercentagesProvider:
17+
'0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A',
1618
},
1719
tokens: {
1820
wrappedNativeAsset: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',

balancer-js/src/modules/data/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './token';
66
export * from './token-prices';
77
export * from './fee-distributor/repository';
88
export * from './fee-collector/repository';
9+
export * from './protocol-fees/provider';
910
export * from './token-yields/repository';
1011
export * from './block-number';
1112

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

2325
// initialCoingeckoList are used to get the initial token list for coingecko
@@ -32,6 +34,7 @@ export class Data implements BalancerDataRepositories {
3234
liquidityGauges;
3335
feeDistributor;
3436
feeCollector;
37+
protocolFees;
3538
tokenYields;
3639
blockNumbers;
3740

@@ -103,6 +106,14 @@ export class Data implements BalancerDataRepositories {
103106
provider
104107
);
105108

109+
if (networkConfig.addresses.contracts.protocolFeePercentagesProvider) {
110+
this.protocolFees = new ProtocolFeesProvider(
111+
networkConfig.addresses.contracts.multicall,
112+
networkConfig.addresses.contracts.protocolFeePercentagesProvider,
113+
provider
114+
);
115+
}
116+
106117
this.tokenYields = new TokenYieldsRepository();
107118
}
108119
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// 0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A
2+
3+
import { Interface } from '@ethersproject/abi';
4+
import { Provider } from '@ethersproject/providers';
5+
import { Contract } from '@ethersproject/contracts';
6+
import { formatUnits } from '@ethersproject/units';
7+
import { Multicall } from '@/modules/contracts/multicall';
8+
9+
const iProtocolFeePercentagesProvider = new Interface([
10+
'function getSwapFeePercentage() view returns (uint)',
11+
]);
12+
13+
export interface ProtocolFees {
14+
swapFee: number;
15+
yieldFee: number;
16+
}
17+
18+
// Using singleton here, so subsequent calls will return the same promise
19+
let feesPromise: Promise<ProtocolFees>;
20+
21+
export class ProtocolFeesProvider {
22+
multicall: Contract;
23+
protocolFees?: ProtocolFees;
24+
25+
constructor(
26+
multicallAddress: string,
27+
private protocolFeePercentagesProviderAddress: string,
28+
provider: Provider
29+
) {
30+
this.multicall = Multicall(multicallAddress, provider);
31+
}
32+
33+
private async fetch(): Promise<ProtocolFees> {
34+
const payload = [
35+
[
36+
this.protocolFeePercentagesProviderAddress,
37+
iProtocolFeePercentagesProvider.encodeFunctionData(
38+
'getFeeTypePercentage',
39+
[0]
40+
),
41+
],
42+
[
43+
this.protocolFeePercentagesProviderAddress,
44+
iProtocolFeePercentagesProvider.encodeFunctionData(
45+
'getFeeTypePercentage',
46+
[2]
47+
),
48+
],
49+
];
50+
const [, res] = await this.multicall.aggregate(payload);
51+
52+
const fees = {
53+
swapFee: parseFloat(formatUnits(res[0], 18)),
54+
yieldFee: parseFloat(formatUnits(res[2], 18)),
55+
};
56+
57+
return fees;
58+
}
59+
60+
async getFees(): Promise<ProtocolFees> {
61+
if (!feesPromise) {
62+
feesPromise = this.fetch();
63+
}
64+
this.protocolFees = await feesPromise;
65+
66+
return this.protocolFees;
67+
}
68+
}

balancer-js/src/modules/data/token-prices/coingecko.ts

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,126 @@
1+
/* eslint-disable @typescript-eslint/no-empty-function */
12
import { Price, Findable, TokenPrices } from '@/types';
23
import { wrappedTokensMap as aaveWrappedMap } from '../token-yields/tokens/aave';
34
import axios from 'axios';
45

6+
// Conscious choice for a deferred promise since we have setTimeout that returns a promise
7+
// Some reference for history buffs: https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns
8+
interface PromisedTokenPrices {
9+
promise: Promise<TokenPrices>;
10+
resolve: (value: TokenPrices) => void;
11+
reject: (reason: unknown) => void;
12+
}
13+
14+
const makePromise = (): PromisedTokenPrices => {
15+
let resolve: (value: TokenPrices) => void = () => {};
16+
let reject: (reason: unknown) => void = () => {};
17+
const promise = new Promise<TokenPrices>((res, rej) => {
18+
[resolve, reject] = [res, rej];
19+
});
20+
return { promise, reject, resolve };
21+
};
22+
523
/**
624
* Simple coingecko price source implementation. Configurable by network and token addresses.
725
*/
826
export class CoingeckoPriceRepository implements Findable<Price> {
927
prices: TokenPrices = {};
10-
fetching: { [address: string]: Promise<TokenPrices> } = {};
1128
urlBase: string;
1229
baseTokenAddresses: string[];
1330

31+
// Properties used for deferring API calls
32+
// TODO: move this logic to hooks
33+
requestedAddresses = new Set<string>(); // Accumulates requested addresses
34+
debounceWait = 200; // Debouncing waiting time [ms]
35+
promisedCalls: PromisedTokenPrices[] = []; // When requesting a price we return a deferred promise
36+
promisedCount = 0; // New request coming when setTimeout is executing will make a new promise
37+
timeout?: ReturnType<typeof setTimeout>;
38+
debounceCancel = (): void => {}; // Allow to cancel mid-flight requests
39+
1440
constructor(tokenAddresses: string[], chainId = 1) {
15-
this.baseTokenAddresses = tokenAddresses.map((a) => a.toLowerCase());
41+
this.baseTokenAddresses = tokenAddresses
42+
.map((a) => a.toLowerCase())
43+
.map((a) => unwrapToken(a));
1644
this.urlBase = `https://api.coingecko.com/api/v3/simple/token_price/${this.platform(
1745
chainId
1846
)}?vs_currencies=usd,eth`;
1947
}
2048

21-
fetch(address: string): { [address: string]: Promise<TokenPrices> } {
22-
console.time(`fetching coingecko ${address}`);
23-
const addresses = this.addresses(address);
24-
const prices = axios
25-
.get(this.url(addresses))
49+
private fetch(
50+
addresses: string[],
51+
{ signal }: { signal?: AbortSignal } = {}
52+
): Promise<TokenPrices> {
53+
console.time(`fetching coingecko for ${addresses.length} tokens`);
54+
return axios
55+
.get<TokenPrices>(this.url(addresses), { signal })
2656
.then(({ data }) => {
27-
addresses.forEach((address) => {
28-
delete this.fetching[address];
29-
});
30-
this.prices = {
31-
...this.prices,
32-
...(Object.keys(data).length == 0 ? { [address]: {} } : data),
33-
};
34-
return this.prices;
57+
return data;
3558
})
36-
.catch((error) => {
37-
console.error(error);
38-
return this.prices;
59+
.finally(() => {
60+
console.timeEnd(`fetching coingecko for ${addresses.length} tokens`);
3961
});
40-
console.timeEnd(`fetching coingecko ${address}`);
41-
return Object.fromEntries(addresses.map((a) => [a, prices]));
62+
}
63+
64+
private debouncedFetch(): Promise<TokenPrices> {
65+
if (!this.promisedCalls[this.promisedCount]) {
66+
this.promisedCalls[this.promisedCount] = makePromise();
67+
}
68+
69+
const { promise, resolve, reject } = this.promisedCalls[this.promisedCount];
70+
71+
if (this.timeout) {
72+
clearTimeout(this.timeout);
73+
}
74+
75+
this.timeout = setTimeout(() => {
76+
this.promisedCount++; // any new call will get a new promise
77+
this.fetch([...this.requestedAddresses])
78+
.then((results) => {
79+
resolve(results);
80+
this.debounceCancel = () => {};
81+
})
82+
.catch((reason) => {
83+
console.error(reason);
84+
});
85+
}, this.debounceWait);
86+
87+
this.debounceCancel = () => {
88+
if (this.timeout) {
89+
clearTimeout(this.timeout);
90+
}
91+
reject('Cancelled');
92+
delete this.promisedCalls[this.promisedCount];
93+
};
94+
95+
return promise;
4296
}
4397

4498
async find(address: string): Promise<Price | undefined> {
4599
const lowercaseAddress = address.toLowerCase();
46100
const unwrapped = unwrapToken(lowercaseAddress);
47-
if (Object.keys(this.fetching).includes(unwrapped)) {
48-
await this.fetching[unwrapped];
49-
} else if (!Object.keys(this.prices).includes(unwrapped)) {
50-
this.fetching = {
51-
...this.fetching,
52-
...this.fetch(unwrapped),
53-
};
54-
await this.fetching[unwrapped];
101+
if (!this.prices[unwrapped]) {
102+
try {
103+
let init = false;
104+
if (Object.keys(this.prices).length === 0) {
105+
// Make initial call with all the tokens we want to preload
106+
this.baseTokenAddresses.forEach(
107+
this.requestedAddresses.add.bind(this.requestedAddresses)
108+
);
109+
init = true;
110+
}
111+
this.requestedAddresses.add(unwrapped);
112+
const promised = await this.debouncedFetch();
113+
this.prices[unwrapped] = promised[unwrapped];
114+
this.requestedAddresses.delete(unwrapped);
115+
if (init) {
116+
this.baseTokenAddresses.forEach((a) => {
117+
this.prices[a] = promised[a];
118+
this.requestedAddresses.delete(a);
119+
});
120+
}
121+
} catch (error) {
122+
console.error(error);
123+
}
55124
}
56125

57126
return this.prices[unwrapped];
@@ -83,14 +152,6 @@ export class CoingeckoPriceRepository implements Findable<Price> {
83152
private url(addresses: string[]): string {
84153
return `${this.urlBase}&contract_addresses=${addresses.join(',')}`;
85154
}
86-
87-
private addresses(address: string): string[] {
88-
if (this.baseTokenAddresses.includes(address)) {
89-
return this.baseTokenAddresses;
90-
} else {
91-
return [address];
92-
}
93-
}
94155
}
95156

96157
const unwrapToken = (wrappedAddress: string) => {

balancer-js/src/modules/data/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { LiquidityGauge } from './liquidity-gauges/provider';
22
export { PoolAttribute } from './pool/types';
33
export { TokenAttribute } from './token/types';
4+
export { ProtocolFees } from './protocol-fees/provider';
45

56
export interface Findable<T, P = string> {
67
find: (id: string) => Promise<T | undefined>;

balancer-js/src/modules/pools/apr/apr.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
TokenAttribute,
1010
LiquidityGauge,
1111
} from '@/types';
12-
import { BaseFeeDistributor, RewardData } from '@/modules/data';
12+
import {
13+
BaseFeeDistributor,
14+
ProtocolFeesProvider,
15+
RewardData,
16+
} from '@/modules/data';
1317
import { ProtocolRevenue } from './protocol-revenue';
1418
import { Liquidity } from '@/modules/liquidity/liquidity.module';
1519
import { identity, zipObject, pickBy } from 'lodash';
@@ -54,7 +58,8 @@ export class PoolApr {
5458
private feeCollector: Findable<number>,
5559
private yesterdaysPools?: Findable<Pool, PoolAttribute>,
5660
private liquidityGauges?: Findable<LiquidityGauge>,
57-
private feeDistributor?: BaseFeeDistributor
61+
private feeDistributor?: BaseFeeDistributor,
62+
private protocolFees?: ProtocolFeesProvider
5863
) {}
5964

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

108113
if (tokenYield) {
109-
apr = tokenYield;
114+
if (pool.poolType === 'MetaStable') {
115+
apr = tokenYield * (1 - (await this.protocolSwapFeePercentage()));
116+
} else if (pool.poolType === 'ComposableStable') {
117+
// TODO: add if(token.isTokenExemptFromYieldProtocolFee) once supported by subgraph
118+
// apr = tokenYield;
119+
120+
const fees = await this.protocolFeesPercentage();
121+
apr = tokenYield * (1 - fees.yieldFee);
122+
} else {
123+
apr = tokenYield;
124+
}
110125
} else {
111126
// Handle subpool APRs with recursive call to get the subPool APR
112127
const subPool = await this.pools.findBy('address', token.address);
@@ -383,6 +398,17 @@ export class PoolApr {
383398
return fee ? fee : 0;
384399
}
385400

401+
private async protocolFeesPercentage() {
402+
if (this.protocolFees) {
403+
return await this.protocolFees.getFees();
404+
}
405+
406+
return {
407+
swapFee: 0,
408+
yieldFee: 0,
409+
};
410+
}
411+
386412
private async rewardTokenApr(tokenAddress: string, rewardData: RewardData) {
387413
if (rewardData.period_finish.toNumber() < Date.now() / 1000) {
388414
return {

balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ fragment SubgraphPoolToken on PoolToken {
163163
managedBalance
164164
weight
165165
priceRate
166+
token {
167+
pool {
168+
poolType
169+
}
170+
}
166171
}
167172

168173
query PoolHistoricalLiquidities(

balancer-js/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
PoolAttribute,
1414
TokenAttribute,
1515
} from '@/modules/data/types';
16-
import type { BaseFeeDistributor } from './modules/data';
16+
import type { BaseFeeDistributor, ProtocolFeesProvider } from './modules/data';
1717
import type { GraphQLArgs } from './lib/graphql';
1818

1919
import type { AprBreakdown } from '@/modules/pools/apr/apr';
@@ -50,6 +50,7 @@ export interface ContractAddresses {
5050
lidoRelayer?: string;
5151
gaugeController?: string;
5252
feeDistributor?: string;
53+
protocolFeePercentagesProvider?: string;
5354
}
5455

5556
export interface BalancerNetworkConfig {
@@ -84,6 +85,7 @@ export interface BalancerDataRepositories {
8485
liquidityGauges?: Findable<LiquidityGauge>;
8586
feeDistributor?: BaseFeeDistributor;
8687
feeCollector: Findable<number>;
88+
protocolFees?: ProtocolFeesProvider;
8789
tokenYields: Findable<number>;
8890
}
8991

0 commit comments

Comments
 (0)