feat(warpMonitor): Use coingecko api key for value monitoring (#4787)

### Description

- Fetching coingecko api from GCP secrets and suing with CoinGecko
Client
- Using coingecko-api-v3 package to support passing API keys 

### Drive-By Changes

- Rename eclipse warp route config files and add `tokenCoinGeckoId`

### Testing

Manual
pull/4825/head
Mohammed Hussan 2 weeks ago committed by GitHub
parent 0772863025
commit 7b3b07900c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/honest-experts-promise.md
  2. 1
      typescript/infra/config/environments/mainnet3/warp/TIA-eclipse-stride-deployments.yaml
  3. 1
      typescript/infra/config/environments/mainnet3/warp/stTIA-eclipse-stride-deployments.yaml
  4. 5
      typescript/infra/helm/warp-routes/templates/env-var-external-secret.yaml
  5. 26
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
  6. 3
      typescript/sdk/package.json
  7. 34
      typescript/sdk/src/gas/token-prices.ts
  8. 21
      typescript/sdk/src/test/MockCoinGecko.ts
  9. 27
      yarn.lock

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Support using apiKey for CoinGeckoTokenPriceGetter

@ -9,6 +9,7 @@ data:
type: collateral type: collateral
hypAddress: stride1pvtesu3ve7qn7ctll2x495mrqf2ysp6fws68grvcu6f7n2ajghgsh2jdj6 hypAddress: stride1pvtesu3ve7qn7ctll2x495mrqf2ysp6fws68grvcu6f7n2ajghgsh2jdj6
tokenAddress: ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801 tokenAddress: ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801
tokenCoinGeckoId: celestia
name: Celestia name: Celestia
symbol: TIA symbol: TIA
decimals: 6 decimals: 6

@ -9,6 +9,7 @@ data:
type: collateral type: collateral
hypAddress: stride134axwdlam929m3mar3wv95nvkyep7mr87ravkqcpf8dfe3v0pjlqwrw6ee hypAddress: stride134axwdlam929m3mar3wv95nvkyep7mr87ravkqcpf8dfe3v0pjlqwrw6ee
tokenAddress: 'stutia' tokenAddress: 'stutia'
tokenCoinGeckoId: stride-staked-tia
name: Stride Staked TIA name: Stride Staked TIA
symbol: stTIA symbol: stTIA
decimals: 6 decimals: 6

@ -21,6 +21,7 @@ spec:
update-on-redeploy: "{{ now }}" update-on-redeploy: "{{ now }}"
data: data:
GCP_SECRET_OVERRIDES_ENABLED: "true" GCP_SECRET_OVERRIDES_ENABLED: "true"
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_COINGECKO_API_KEY: {{ printf "'{{ .%s_coingecko_api_key | toString }}'" .Values.hyperlane.runEnv }}
{{/* {{/*
* For each network, create an environment variable with the RPC endpoint. * For each network, create an environment variable with the RPC endpoint.
* The templating of external-secrets will use the data section below to know how * The templating of external-secrets will use the data section below to know how
@ -30,9 +31,9 @@ spec:
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }}
{{- end }} {{- end }}
data: data:
- secretKey: deployer_key - secretKey: {{ printf "%s_coingecko_api_key" .Values.hyperlane.runEnv }}
remoteRef: remoteRef:
key: {{ printf "hyperlane-%s-key-deployer" .Values.hyperlane.runEnv }} key: {{ printf "%s-coingecko-api-key" .Values.hyperlane.runEnv }}
{{/* {{/*
* For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network, * For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network,
* and associate it with the secret key networkname_rpc. * and associate it with the secret key networkname_rpc.

@ -32,6 +32,8 @@ import {
rootLogger, rootLogger,
} from '@hyperlane-xyz/utils'; } from '@hyperlane-xyz/utils';
import { DeployEnvironment } from '../../src/config/environment.js';
import { fetchGCPSecret } from '../../src/utils/gcloud.js';
import { startMetricsServer } from '../../src/utils/metrics.js'; import { startMetricsServer } from '../../src/utils/metrics.js';
import { readYaml } from '../../src/utils/utils.js'; import { readYaml } from '../../src/utils/utils.js';
import { getArgs } from '../agent-utils.js'; import { getArgs } from '../agent-utils.js';
@ -636,8 +638,10 @@ async function checkWarpRouteMetrics(
tokenConfig: WarpRouteConfig, tokenConfig: WarpRouteConfig,
chainMetadata: ChainMap<ChainMetadata>, chainMetadata: ChainMap<ChainMetadata>,
) { ) {
const tokenPriceGetter = const tokenPriceGetter = CoinGeckoTokenPriceGetter.withDefaultCoinGecko(
CoinGeckoTokenPriceGetter.withDefaultCoinGecko(chainMetadata); chainMetadata,
await getCoinGeckoApiKey(),
);
setInterval(async () => { setInterval(async () => {
try { try {
@ -672,4 +676,22 @@ async function checkWarpRouteMetrics(
}, checkFrequency); }, checkFrequency);
} }
async function getCoinGeckoApiKey(): Promise<string | undefined> {
const environment: DeployEnvironment = 'mainnet3';
let apiKey: string | undefined;
try {
apiKey = (await fetchGCPSecret(
`${environment}-coingecko-api-key`,
false,
)) as string;
} catch (e) {
logger.error(
'Error fetching CoinGecko API key, proceeding with public tier',
e,
);
}
return apiKey;
}
main().then(logger.info).catch(logger.error); main().then(logger.info).catch(logger.error);

@ -14,10 +14,9 @@
"@safe-global/safe-deployments": "1.37.8", "@safe-global/safe-deployments": "1.37.8",
"@solana/spl-token": "^0.3.8", "@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.0", "@solana/web3.js": "^1.78.0",
"@types/coingecko-api": "^1.0.10",
"@wagmi/chains": "^1.8.0", "@wagmi/chains": "^1.8.0",
"bignumber.js": "^9.1.1", "bignumber.js": "^9.1.1",
"coingecko-api": "^1.0.10", "coingecko-api-v3": "^0.0.29",
"cosmjs-types": "^0.9.0", "cosmjs-types": "^0.9.0",
"cross-fetch": "^3.1.5", "cross-fetch": "^3.1.5",
"ethers": "^5.7.2", "ethers": "^5.7.2",

@ -1,4 +1,4 @@
import CoinGecko from 'coingecko-api'; import { CoinGeckoClient, SimplePriceResponse } from 'coingecko-api-v3';
import { rootLogger, sleep } from '@hyperlane-xyz/utils'; import { rootLogger, sleep } from '@hyperlane-xyz/utils';
@ -10,12 +10,11 @@ export interface TokenPriceGetter {
getTokenExchangeRate(base: ChainName, quote: ChainName): Promise<number>; getTokenExchangeRate(base: ChainName, quote: ChainName): Promise<number>;
} }
export type CoinGeckoInterface = Pick<CoinGecko, 'simple'>; export type CoinGeckoInterface = Pick<CoinGeckoClient, 'simplePrice'>;
export type CoinGeckoSimpleInterface = CoinGecko['simple']; export type CoinGeckoSimplePriceInterface = CoinGeckoClient['simplePrice'];
export type CoinGeckoSimplePriceParams = Parameters< export type CoinGeckoSimplePriceParams =
CoinGeckoSimpleInterface['price'] Parameters<CoinGeckoSimplePriceInterface>[0];
>[0]; export type CoinGeckoResponse = ReturnType<CoinGeckoSimplePriceInterface>;
export type CoinGeckoResponse = ReturnType<CoinGeckoSimpleInterface['price']>;
type TokenPriceCacheEntry = { type TokenPriceCacheEntry = {
price: number; price: number;
@ -85,10 +84,11 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
static withDefaultCoinGecko( static withDefaultCoinGecko(
chainMetadata: ChainMap<ChainMetadata>, chainMetadata: ChainMap<ChainMetadata>,
apiKey?: string,
expirySeconds?: number, expirySeconds?: number,
sleepMsBetweenRequests = 5000, sleepMsBetweenRequests = 5000,
): CoinGeckoTokenPriceGetter { ): CoinGeckoTokenPriceGetter {
const coinGecko = new CoinGecko(); const coinGecko = new CoinGeckoClient(undefined, apiKey);
return new CoinGeckoTokenPriceGetter( return new CoinGeckoTokenPriceGetter(
coinGecko, coinGecko,
chainMetadata, chainMetadata,
@ -153,20 +153,14 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
await sleep(this.sleepMsBetweenRequests); await sleep(this.sleepMsBetweenRequests);
if (toQuery.length > 0) { if (toQuery.length > 0) {
let response: any; let response: SimplePriceResponse;
try { try {
response = await this.coinGecko.simple.price({ response = await this.coinGecko.simplePrice({
ids: toQuery, ids: toQuery.join(','),
vs_currencies: [currency], vs_currencies: currency,
}); });
const prices = toQuery.map((id) => response[id][currency]);
if (response.success === true) { toQuery.map((id, i) => this.cache.put(id, prices[i]));
const prices = toQuery.map((id) => response.data[id][currency]);
toQuery.map((id, i) => this.cache.put(id, prices[i]));
} else {
rootLogger.warn('Failed to query token prices', response.message);
return undefined;
}
} catch (e) { } catch (e) {
rootLogger.warn('Error when querying token prices', e); rootLogger.warn('Error when querying token prices', e);
return undefined; return undefined;

@ -1,7 +1,9 @@
import { SimplePriceResponse } from 'coingecko-api-v3';
import type { import type {
CoinGeckoInterface, CoinGeckoInterface,
CoinGeckoResponse, CoinGeckoResponse,
CoinGeckoSimpleInterface, CoinGeckoSimplePriceInterface,
CoinGeckoSimplePriceParams, CoinGeckoSimplePriceParams,
} from '../gas/token-prices.js'; } from '../gas/token-prices.js';
import type { ChainName } from '../types.js'; import type { ChainName } from '../types.js';
@ -18,9 +20,9 @@ export class MockCoinGecko implements CoinGeckoInterface {
this.fail = {}; this.fail = {};
} }
price(params: CoinGeckoSimplePriceParams): CoinGeckoResponse { price(input: CoinGeckoSimplePriceParams): CoinGeckoResponse {
const data: any = {}; const data: SimplePriceResponse = {};
for (const id of params.ids) { for (const id of input.ids) {
if (this.fail[id]) { if (this.fail[id]) {
return Promise.reject(`Failed to fetch price for ${id}`); return Promise.reject(`Failed to fetch price for ${id}`);
} }
@ -28,16 +30,11 @@ export class MockCoinGecko implements CoinGeckoInterface {
usd: this.tokenPrices[id], usd: this.tokenPrices[id],
}; };
} }
return Promise.resolve({ return Promise.resolve(data);
success: true,
message: '',
code: 200,
data,
});
} }
get simple(): CoinGeckoSimpleInterface { get simplePrice(): CoinGeckoSimplePriceInterface {
return this; return this.price;
} }
setTokenPrice(chain: ChainName, price: number): void { setTokenPrice(chain: ChainName, price: number): void {

@ -8066,7 +8066,6 @@ __metadata:
"@safe-global/safe-deployments": "npm:1.37.8" "@safe-global/safe-deployments": "npm:1.37.8"
"@solana/spl-token": "npm:^0.3.8" "@solana/spl-token": "npm:^0.3.8"
"@solana/web3.js": "npm:^1.78.0" "@solana/web3.js": "npm:^1.78.0"
"@types/coingecko-api": "npm:^1.0.10"
"@types/mocha": "npm:^10.0.1" "@types/mocha": "npm:^10.0.1"
"@types/node": "npm:^16.9.1" "@types/node": "npm:^16.9.1"
"@types/sinon": "npm:^17.0.1" "@types/sinon": "npm:^17.0.1"
@ -8075,7 +8074,7 @@ __metadata:
"@wagmi/chains": "npm:^1.8.0" "@wagmi/chains": "npm:^1.8.0"
bignumber.js: "npm:^9.1.1" bignumber.js: "npm:^9.1.1"
chai: "npm:4.5.0" chai: "npm:4.5.0"
coingecko-api: "npm:^1.0.10" coingecko-api-v3: "npm:^0.0.29"
cosmjs-types: "npm:^0.9.0" cosmjs-types: "npm:^0.9.0"
cross-fetch: "npm:^3.1.5" cross-fetch: "npm:^3.1.5"
dotenv: "npm:^10.0.0" dotenv: "npm:^10.0.0"
@ -13160,13 +13159,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/coingecko-api@npm:^1.0.10":
version: 1.0.10
resolution: "@types/coingecko-api@npm:1.0.10"
checksum: 2523f946e6d293c2ee94a0abee624f53c34b4643f8df685d0164509aba66e8234276e5d8c202c514551024757f0987f7062daa7428ccaf6673bad9a5c55779a2
languageName: node
linkType: hard
"@types/concat-stream@npm:^1.6.0": "@types/concat-stream@npm:^1.6.0":
version: 1.6.1 version: 1.6.1
resolution: "@types/concat-stream@npm:1.6.1" resolution: "@types/concat-stream@npm:1.6.1"
@ -16754,10 +16746,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"coingecko-api@npm:^1.0.10": "coingecko-api-v3@npm:^0.0.29":
version: 1.0.10 version: 0.0.29
resolution: "coingecko-api@npm:1.0.10" resolution: "coingecko-api-v3@npm:0.0.29"
checksum: e0000df5aebbeee508f25824485fe8e4be57cd07825b3cfbf2dc3c51b646200eefd336c833e81747d4a209bf10c32019baef1070fb2bfbcdbae099420954d1fa dependencies:
https: "npm:^1.0.0"
checksum: e60a0996472419232a144ec77028c060bd9c289f799dd40d46dbb7229cff3d868a3e35bf88724059dc25767b8136d794789e4dd31711592fa73a7be1ca2fcbc7
languageName: node languageName: node
linkType: hard linkType: hard
@ -21651,6 +21645,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"https@npm:^1.0.0":
version: 1.0.0
resolution: "https@npm:1.0.0"
checksum: ccea8a8363a018d4b241db7748cff3a85c9f5b71bf80639e9c37dc6823f590f35dda098b80b726930e9f945387c8bfd6b1461df25cab5bf65a31903d81875b5d
languageName: node
linkType: hard
"human-id@npm:^1.0.2": "human-id@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "human-id@npm:1.0.2" resolution: "human-id@npm:1.0.2"

Loading…
Cancel
Save