Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replacing CoingeckoTokenPriceService by ApiTokenPriceService to fetch… #564

25 changes: 25 additions & 0 deletions balancer-js/examples/data/api-token-price-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Display APRs for pool ids hardcoded under `const ids`
* Run command: yarn example ./examples/data/token-prices.ts
*/
import { ApiTokenPriceService } from '@/modules/sor/token-price/apiTokenPriceService';

const dai = '0x6b175474e89094c44da98b954eedeac495271d0f';
const weth = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const ohm = '0X64AA3364F17A4D01C6F1751FD97C2BD3D7E7F1D5';

(async () => {
const apiTokenPriceService = new ApiTokenPriceService(1);
const daiPriceInEth = await apiTokenPriceService.getNativeAssetPriceInToken(
dai
);
console.log('Dai Price In ETH: ' + daiPriceInEth);
const wethPriceInEth = await apiTokenPriceService.getNativeAssetPriceInToken(
weth
);
console.log('WETH Price In ETH: ' + wethPriceInEth);
const ohmPriceInEth = await apiTokenPriceService.getNativeAssetPriceInToken(
ohm
);
console.log('OHM Price In ETH: ' + ohmPriceInEth);
})();
13 changes: 13 additions & 0 deletions balancer-js/src/lib/utils/coingecko-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function getCoingeckoApiBaseUrl(isDemoApi = true): string {
if (isDemoApi) {
return 'https://api.coingecko.com/api/v3/';
}
return 'https://pro-api.coingecko.com/api/v3/';
}

export function getCoingeckoApiKeyHeaderName(isDemoApi = true): string {
if (isDemoApi) {
return 'x-cg-demo-api-key';
}
return 'x-cg-pro-api-key';
}
21 changes: 14 additions & 7 deletions balancer-js/src/modules/data/token-prices/coingecko-historical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
} from '@/types';
import axios, { AxiosError } from 'axios';
import { tokenAddressForPricing } from '@/lib/utils';
import {
getCoingeckoApiBaseUrl,
getCoingeckoApiKeyHeaderName,
} from '@/lib/utils/coingecko-api';

const HOUR = 60 * 60;

Expand All @@ -18,16 +22,19 @@ const HOUR = 60 * 60;
export class CoingeckoHistoricalPriceRepository implements Findable<Price> {
prices: TokenPrices = {};
nativePrice?: Promise<Price>;
urlBase: string;
apiKey?: string;

private readonly urlBase: string;
private readonly apiKey?: string;
private readonly coingeckoApiKeyHeaderName: string;
constructor(private chainId: Network = 1, coingecko?: CoingeckoConfig) {
this.urlBase = `https://${
coingecko?.coingeckoApiKey && !coingecko.isDemoApiKey ? 'pro-' : ''
}api.coingecko.com/api/v3/coins/${this.platform(
this.urlBase = `${getCoingeckoApiBaseUrl(
coingecko?.isDemoApiKey
)}coins/${this.platform(
chainId
)}/contract/%TOKEN_ADDRESS%/market_chart/range?vs_currency=usd`;
this.apiKey = coingecko?.coingeckoApiKey;
this.coingeckoApiKeyHeaderName = getCoingeckoApiKeyHeaderName(
coingecko?.isDemoApiKey
);
}

private async fetch(
Expand All @@ -40,7 +47,7 @@ export class CoingeckoHistoricalPriceRepository implements Findable<Price> {
try {
const { data } = await axios.get<HistoricalPrices>(url, {
signal,
headers: { 'x-cg-pro-api-key': this.apiKey ?? '' },
headers: { [this.coingeckoApiKeyHeaderName]: this.apiKey ?? '' },
});
console.timeEnd(`fetching coingecko historical for ${address}`);
console.log(data);
Expand Down
47 changes: 31 additions & 16 deletions balancer-js/src/modules/data/token-prices/coingecko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ import {
import axios, { AxiosError } from 'axios';
import { TOKENS } from '@/lib/constants/tokens';
import { Debouncer, tokenAddressForPricing } from '@/lib/utils';
import {
getCoingeckoApiBaseUrl,
getCoingeckoApiKeyHeaderName,
} from '@/lib/utils/coingecko-api';

/**
* Simple coingecko price source implementation. Configurable by network and token addresses.
*/
export class CoingeckoPriceRepository implements Findable<Price> {
prices: { [key: string]: Promise<Price> } = {};
nativePrice?: Promise<Price>;
urlBase: string;
private readonly url: string;
private readonly urlNative: string;
private readonly coingeckoApiKeyHeaderName: string;
baseTokenAddresses: string[];
debouncer: Debouncer<TokenPrices, string>;
apiKey?: string;
Expand All @@ -27,9 +33,15 @@ export class CoingeckoPriceRepository implements Findable<Price> {
coingecko?: CoingeckoConfig
) {
this.baseTokenAddresses = tokenAddresses.map(tokenAddressForPricing);
this.urlBase = `https://api.coingecko.com/api/v3/simple/token_price/${this.platform(
chainId
)}?vs_currencies=usd,eth`;
this.url = `${getCoingeckoApiBaseUrl(
coingecko?.isDemoApiKey
)}simple/token_price/${this.platform(chainId)}?vs_currencies=usd,eth`;
this.urlNative = `${getCoingeckoApiBaseUrl(
coingecko?.isDemoApiKey
)}simple/price/?vs_currencies=eth,usd&ids=`;
this.coingeckoApiKeyHeaderName = getCoingeckoApiKeyHeaderName(
coingecko?.isDemoApiKey
);
this.apiKey = coingecko?.coingeckoApiKey;
this.debouncer = new Debouncer<TokenPrices, string>(
this.fetch.bind(this),
Expand All @@ -43,10 +55,15 @@ export class CoingeckoPriceRepository implements Findable<Price> {
{ signal }: { signal?: AbortSignal } = {}
): Promise<TokenPrices> {
try {
const { data } = await axios.get<TokenPrices>(this.url(addresses), {
signal,
headers: { ApiKey: this.apiKey ?? '' },
});
const { data } = await axios.get<TokenPrices>(
`${this.url}&contract_addresses=${addresses.join(',')}`,
{
signal,
headers: {
[this.coingeckoApiKeyHeaderName]: this.apiKey ?? '',
},
}
);
return data;
} catch (error) {
const message = ['Error fetching token prices from coingecko'];
Expand Down Expand Up @@ -74,10 +91,12 @@ export class CoingeckoPriceRepository implements Findable<Price> {
if (this.chainId === 137) assetId = Assets.MATIC;
if (this.chainId === 100) assetId = Assets.XDAI;
return axios
.get<{ [key in Assets]: Price }>(
`https://api.coingecko.com/api/v3/simple/price/?vs_currencies=eth,usd&ids=${assetId}`,
{ signal }
)
.get<{ [key in Assets]: Price }>(`${this.urlNative}${assetId}`, {
signal,
headers: {
[this.coingeckoApiKeyHeaderName]: this.apiKey ?? '',
},
})
.then(({ data }) => {
return data[assetId];
})
Expand Down Expand Up @@ -161,8 +180,4 @@ export class CoingeckoPriceRepository implements Findable<Price> {

return '2';
}

private url(addresses: string[]): string {
return `${this.urlBase}&contract_addresses=${addresses.join(',')}`;
}
}
4 changes: 2 additions & 2 deletions balancer-js/src/modules/pools/apr/apr.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('APR tests', () => {
const pool = await pools.find(veBalId);
if (pool) {
const apr = await pools.apr(pool);
expect(apr.protocolApr).to.be.greaterThan(1);
expect(apr.protocolApr).to.be.greaterThanOrEqual(1);
lgahdl marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw 'no pool found';
}
Expand All @@ -65,7 +65,7 @@ describe('APR tests', () => {
const pool = await pools.find(auraBALveBAL);
if (pool) {
const apr = await pools.apr(pool);
expect(apr.tokenAprs.total).to.be.greaterThan(1);
expect(apr.tokenAprs.total).to.be.greaterThanOrEqual(1);
lgahdl marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw 'no pool found';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ const balPool = {
};

const composableStablePool = {
id: '0x4edcb2b46377530bc18bb4d2c7fe46a992c73e100000000000000000000003ec',
id: '0x05ff47afada98a98982113758878f9a8b9fdda0a000000000000000000000645',
poolType: PoolType.ComposableStable,
tokensList: [
'0x4edcb2b46377530bc18bb4d2c7fe46a992c73e10',
'0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
'0xbe9895146f7af43049ca1c1ae358b0541ea49704',
'0x05ff47afada98a98982113758878f9a8b9fdda0a',
'0xae78736cd615f374d3085123a210448e74fc6393',
'0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee',
],
};

Expand Down
20 changes: 13 additions & 7 deletions balancer-js/src/modules/sor/sor.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SOR, SorConfig, TokenPriceService } from '@balancer-labs/sor';
import { Provider, JsonRpcProvider } from '@ethersproject/providers';
import { SubgraphPoolDataService } from './pool-data/subgraphPoolDataService';
import { CoingeckoTokenPriceService } from './token-price/coingeckoTokenPriceService';
import {
SubgraphClient,
createSubgraphClient,
Expand All @@ -10,10 +9,13 @@ import {
BalancerNetworkConfig,
BalancerSdkConfig,
BalancerSdkSorConfig,
CoingeckoConfig,
} from '@/types';
import { SubgraphTokenPriceService } from './token-price/subgraphTokenPriceService';
import { getNetworkConfig } from '@/modules/sdk.helpers';
import { POOLS_TO_IGNORE } from '@/lib/constants/poolsToIgnore';
import { ApiTokenPriceService } from '@/modules/sor/token-price/apiTokenPriceService';
import { CoingeckoTokenPriceService } from '@/modules/sor/token-price/coingeckoTokenPriceService';

export class Sor extends SOR {
constructor(sdkConfig: BalancerSdkConfig) {
Expand All @@ -36,15 +38,16 @@ export class Sor extends SOR {
const tokenPriceService = Sor.getTokenPriceService(
network,
sorConfig,
subgraphClient
subgraphClient,
sdkConfig.coingecko
);

super(provider, sorNetworkConfig, poolDataService, tokenPriceService);
}

private static getSorConfig(config: BalancerSdkConfig): BalancerSdkSorConfig {
return {
tokenPriceService: 'coingecko',
tokenPriceService: 'api',
poolDataService: 'subgraph',
fetchOnChainBalances: true,
...config.sor,
Expand Down Expand Up @@ -89,17 +92,20 @@ export class Sor extends SOR {
private static getTokenPriceService(
network: BalancerNetworkConfig,
sorConfig: BalancerSdkSorConfig,
subgraphClient: SubgraphClient
subgraphClient: SubgraphClient,
coingeckoConfig?: CoingeckoConfig
): TokenPriceService {
if (sorConfig.tokenPriceService === 'coingecko' && coingeckoConfig) {
return new CoingeckoTokenPriceService(network.chainId, coingeckoConfig);
}
if (typeof sorConfig.tokenPriceService === 'object') {
return sorConfig.tokenPriceService;
} else if (sorConfig.tokenPriceService === 'subgraph') {
new SubgraphTokenPriceService(
return new SubgraphTokenPriceService(
subgraphClient,
network.addresses.tokens.wrappedNativeAsset
);
}

return new CoingeckoTokenPriceService(network.chainId);
return new ApiTokenPriceService(network.chainId);
brunoguerios marked this conversation as resolved.
Show resolved Hide resolved
}
}
64 changes: 64 additions & 0 deletions balancer-js/src/modules/sor/token-price/apiTokenPriceService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { TokenPriceService } from '@balancer-labs/sor';
import { gql, request } from 'graphql-request';
import { Network } from '@/types';

export class ApiTokenPriceService implements TokenPriceService {
private chainKey: string;

private balancerApiUrl = 'https://api-v3.balancer.fi/';

private tokenPriceQuery = gql`
query queryTokenPrices($chainKey: GqlChain!) {
tokenGetCurrentPrices(chains: [$chainKey]) {
address
price
}
}
`;

constructor(private readonly chainId: number) {
this.chainKey = Network[chainId];
}
async getNativeAssetPriceInToken(tokenAddress: string): Promise<string> {
const { tokenGetCurrentPrices: tokenPrices } = await request(
this.balancerApiUrl,
this.tokenPriceQuery,
{
chainKey: this.chainKey,
}
);
const tokenPriceUsd = (
tokenPrices as { address: string; price: number }[]
).find(
({ address }) => address.toLowerCase() === tokenAddress.toLowerCase()
);
if (!tokenPriceUsd) {
throw new Error('Token Price not found in the API');
}
const nativeAssetPriceUsd = (
tokenPrices as { address: string; price: number }[]
).find(
({ address }) =>
address.toLowerCase() ===
NativeAssetAddress[this.chainKey as keyof typeof NativeAssetAddress]
);
if (!nativeAssetPriceUsd) {
throw new Error('Native Token Price not found in the API');
}
const tokenPriceInNativeAsset =
tokenPriceUsd.price / nativeAssetPriceUsd.price;
return String(tokenPriceInNativeAsset);
}
}

enum NativeAssetAddress {
MAINNET = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
POLYGON = '0x0000000000000000000000000000000000001010',
ARBITRUM = '0x912ce59144191c1204e64559fe8253a0e49e6548',
AVALANCHE = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
BASE = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
FANTOM = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
GNOSIS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
OPTIMISM = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
ZKEVM = '0xa2036f0538221a77a3937f1379699f44945018d0',
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { TokenPriceService } from '@balancer-labs/sor';
import axios from 'axios';
import { BALANCER_NETWORK_CONFIG } from '@/lib/constants/config';
import { Network, BalancerNetworkConfig } from '@/types';
import { Network, BalancerNetworkConfig, CoingeckoConfig } from '@/types';
import {
getCoingeckoApiBaseUrl,
getCoingeckoApiKeyHeaderName,
} from '@/lib/utils/coingecko-api';

export class CoingeckoTokenPriceService implements TokenPriceService {
constructor(private readonly chainId: number) {}
private readonly urlBase: string;
private readonly apiKey: string;
private readonly coingeckoApiKeyHeaderName: string;
constructor(private readonly chainId: number, coingecko: CoingeckoConfig) {
this.urlBase = `${getCoingeckoApiBaseUrl(
coingecko?.isDemoApiKey
)}simple/token_price/${this.platformId}?vs_currencies=${
this.nativeAssetId
}`;
this.coingeckoApiKeyHeaderName = getCoingeckoApiKeyHeaderName(
coingecko?.isDemoApiKey
);
this.apiKey = coingecko.coingeckoApiKey;
}

public async getNativeAssetPriceInToken(
tokenAddress: string
Expand All @@ -22,12 +39,13 @@ export class CoingeckoTokenPriceService implements TokenPriceService {
* @returns the price of 1 ETH in terms of the token base units
*/
async getTokenPriceInNativeAsset(tokenAddress: string): Promise<string> {
const endpoint = `https://api.coingecko.com/api/v3/simple/token_price/${this.platformId}?contract_addresses=${tokenAddress}&vs_currencies=${this.nativeAssetId}`;
const endpoint = `${this.urlBase}&contract_addresses=${tokenAddress}`;

const { data } = await axios.get(endpoint, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
[this.coingeckoApiKeyHeaderName]: this.apiKey ?? '',
},
});

Expand Down
2 changes: 1 addition & 1 deletion balancer-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface BalancerTenderlyConfig {
export interface BalancerSdkSorConfig {
//use a built-in service or provide a custom implementation of a TokenPriceService
//defaults to coingecko
tokenPriceService: 'coingecko' | 'subgraph' | TokenPriceService;
tokenPriceService: 'api' | 'coingecko' | 'subgraph' | TokenPriceService;
//use a built-in service or provide a custom implementation of a PoolDataService
//defaults to subgraph
poolDataService: 'subgraph' | PoolDataService;
Expand Down
Loading