Skip to content

Commit

Permalink
Merge pull request #564 from balancer/563-adapt-the-balancer-sdk-sor-…
Browse files Browse the repository at this point in the history
…to-not-use-coingecko-requests

Replacing CoingeckoTokenPriceService by ApiTokenPriceService to fetch…
  • Loading branch information
brunoguerios authored Jan 31, 2024
2 parents 59900fb + bc0b449 commit 5485c10
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 38 deletions.
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(',')}`;
}
}
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);
}
}
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

0 comments on commit 5485c10

Please sign in to comment.