From 7b3b07900c3bc1780c49c054c469a767a4183dfa Mon Sep 17 00:00:00 2001 From: Mohammed Hussan Date: Tue, 5 Nov 2024 15:38:18 +0000 Subject: [PATCH] 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 --- .changeset/honest-experts-promise.md | 5 +++ ...ml => TIA-eclipse-stride-deployments.yaml} | 1 + ... => stTIA-eclipse-stride-deployments.yaml} | 1 + .../templates/env-var-external-secret.yaml | 5 +-- .../monitor-warp-routes-balances.ts | 26 ++++++++++++-- typescript/sdk/package.json | 3 +- typescript/sdk/src/gas/token-prices.ts | 34 ++++++++----------- typescript/sdk/src/test/MockCoinGecko.ts | 21 +++++------- yarn.lock | 27 ++++++++------- 9 files changed, 72 insertions(+), 51 deletions(-) create mode 100644 .changeset/honest-experts-promise.md rename typescript/infra/config/environments/mainnet3/warp/{TIA-eclipsestride-deployments.yaml => TIA-eclipse-stride-deployments.yaml} (95%) rename typescript/infra/config/environments/mainnet3/warp/{STTIA-eclipsestride-deployments.yaml => stTIA-eclipse-stride-deployments.yaml} (94%) diff --git a/.changeset/honest-experts-promise.md b/.changeset/honest-experts-promise.md new file mode 100644 index 000000000..76ec7a6f4 --- /dev/null +++ b/.changeset/honest-experts-promise.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Support using apiKey for CoinGeckoTokenPriceGetter diff --git a/typescript/infra/config/environments/mainnet3/warp/TIA-eclipsestride-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/TIA-eclipse-stride-deployments.yaml similarity index 95% rename from typescript/infra/config/environments/mainnet3/warp/TIA-eclipsestride-deployments.yaml rename to typescript/infra/config/environments/mainnet3/warp/TIA-eclipse-stride-deployments.yaml index 999e8ea95..45c9e0efc 100644 --- a/typescript/infra/config/environments/mainnet3/warp/TIA-eclipsestride-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/TIA-eclipse-stride-deployments.yaml @@ -9,6 +9,7 @@ data: type: collateral hypAddress: stride1pvtesu3ve7qn7ctll2x495mrqf2ysp6fws68grvcu6f7n2ajghgsh2jdj6 tokenAddress: ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801 + tokenCoinGeckoId: celestia name: Celestia symbol: TIA decimals: 6 diff --git a/typescript/infra/config/environments/mainnet3/warp/STTIA-eclipsestride-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/stTIA-eclipse-stride-deployments.yaml similarity index 94% rename from typescript/infra/config/environments/mainnet3/warp/STTIA-eclipsestride-deployments.yaml rename to typescript/infra/config/environments/mainnet3/warp/stTIA-eclipse-stride-deployments.yaml index 9938ad9f9..7f18e471c 100644 --- a/typescript/infra/config/environments/mainnet3/warp/STTIA-eclipsestride-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/stTIA-eclipse-stride-deployments.yaml @@ -9,6 +9,7 @@ data: type: collateral hypAddress: stride134axwdlam929m3mar3wv95nvkyep7mr87ravkqcpf8dfe3v0pjlqwrw6ee tokenAddress: 'stutia' + tokenCoinGeckoId: stride-staked-tia name: Stride Staked TIA symbol: stTIA decimals: 6 diff --git a/typescript/infra/helm/warp-routes/templates/env-var-external-secret.yaml b/typescript/infra/helm/warp-routes/templates/env-var-external-secret.yaml index e7a74b714..d1f1eda25 100644 --- a/typescript/infra/helm/warp-routes/templates/env-var-external-secret.yaml +++ b/typescript/infra/helm/warp-routes/templates/env-var-external-secret.yaml @@ -21,6 +21,7 @@ spec: update-on-redeploy: "{{ now }}" data: 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. * 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 }}'" . }} {{- end }} data: - - secretKey: deployer_key + - secretKey: {{ printf "%s_coingecko_api_key" .Values.hyperlane.runEnv }} 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, * and associate it with the secret key networkname_rpc. diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index 2d588a89c..fd4da97f5 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -32,6 +32,8 @@ import { rootLogger, } 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 { readYaml } from '../../src/utils/utils.js'; import { getArgs } from '../agent-utils.js'; @@ -636,8 +638,10 @@ async function checkWarpRouteMetrics( tokenConfig: WarpRouteConfig, chainMetadata: ChainMap, ) { - const tokenPriceGetter = - CoinGeckoTokenPriceGetter.withDefaultCoinGecko(chainMetadata); + const tokenPriceGetter = CoinGeckoTokenPriceGetter.withDefaultCoinGecko( + chainMetadata, + await getCoinGeckoApiKey(), + ); setInterval(async () => { try { @@ -672,4 +676,22 @@ async function checkWarpRouteMetrics( }, checkFrequency); } +async function getCoinGeckoApiKey(): Promise { + 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); diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 76e84cdbf..233a0a423 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -14,10 +14,9 @@ "@safe-global/safe-deployments": "1.37.8", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.0", - "@types/coingecko-api": "^1.0.10", "@wagmi/chains": "^1.8.0", "bignumber.js": "^9.1.1", - "coingecko-api": "^1.0.10", + "coingecko-api-v3": "^0.0.29", "cosmjs-types": "^0.9.0", "cross-fetch": "^3.1.5", "ethers": "^5.7.2", diff --git a/typescript/sdk/src/gas/token-prices.ts b/typescript/sdk/src/gas/token-prices.ts index d60949630..80b1ae288 100644 --- a/typescript/sdk/src/gas/token-prices.ts +++ b/typescript/sdk/src/gas/token-prices.ts @@ -1,4 +1,4 @@ -import CoinGecko from 'coingecko-api'; +import { CoinGeckoClient, SimplePriceResponse } from 'coingecko-api-v3'; import { rootLogger, sleep } from '@hyperlane-xyz/utils'; @@ -10,12 +10,11 @@ export interface TokenPriceGetter { getTokenExchangeRate(base: ChainName, quote: ChainName): Promise; } -export type CoinGeckoInterface = Pick; -export type CoinGeckoSimpleInterface = CoinGecko['simple']; -export type CoinGeckoSimplePriceParams = Parameters< - CoinGeckoSimpleInterface['price'] ->[0]; -export type CoinGeckoResponse = ReturnType; +export type CoinGeckoInterface = Pick; +export type CoinGeckoSimplePriceInterface = CoinGeckoClient['simplePrice']; +export type CoinGeckoSimplePriceParams = + Parameters[0]; +export type CoinGeckoResponse = ReturnType; type TokenPriceCacheEntry = { price: number; @@ -85,10 +84,11 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { static withDefaultCoinGecko( chainMetadata: ChainMap, + apiKey?: string, expirySeconds?: number, sleepMsBetweenRequests = 5000, ): CoinGeckoTokenPriceGetter { - const coinGecko = new CoinGecko(); + const coinGecko = new CoinGeckoClient(undefined, apiKey); return new CoinGeckoTokenPriceGetter( coinGecko, chainMetadata, @@ -153,20 +153,14 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { await sleep(this.sleepMsBetweenRequests); if (toQuery.length > 0) { - let response: any; + let response: SimplePriceResponse; try { - response = await this.coinGecko.simple.price({ - ids: toQuery, - vs_currencies: [currency], + response = await this.coinGecko.simplePrice({ + ids: toQuery.join(','), + vs_currencies: currency, }); - - if (response.success === true) { - 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; - } + const prices = toQuery.map((id) => response[id][currency]); + toQuery.map((id, i) => this.cache.put(id, prices[i])); } catch (e) { rootLogger.warn('Error when querying token prices', e); return undefined; diff --git a/typescript/sdk/src/test/MockCoinGecko.ts b/typescript/sdk/src/test/MockCoinGecko.ts index f2b193430..8b410125a 100644 --- a/typescript/sdk/src/test/MockCoinGecko.ts +++ b/typescript/sdk/src/test/MockCoinGecko.ts @@ -1,7 +1,9 @@ +import { SimplePriceResponse } from 'coingecko-api-v3'; + import type { CoinGeckoInterface, CoinGeckoResponse, - CoinGeckoSimpleInterface, + CoinGeckoSimplePriceInterface, CoinGeckoSimplePriceParams, } from '../gas/token-prices.js'; import type { ChainName } from '../types.js'; @@ -18,9 +20,9 @@ export class MockCoinGecko implements CoinGeckoInterface { this.fail = {}; } - price(params: CoinGeckoSimplePriceParams): CoinGeckoResponse { - const data: any = {}; - for (const id of params.ids) { + price(input: CoinGeckoSimplePriceParams): CoinGeckoResponse { + const data: SimplePriceResponse = {}; + for (const id of input.ids) { if (this.fail[id]) { return Promise.reject(`Failed to fetch price for ${id}`); } @@ -28,16 +30,11 @@ export class MockCoinGecko implements CoinGeckoInterface { usd: this.tokenPrices[id], }; } - return Promise.resolve({ - success: true, - message: '', - code: 200, - data, - }); + return Promise.resolve(data); } - get simple(): CoinGeckoSimpleInterface { - return this; + get simplePrice(): CoinGeckoSimplePriceInterface { + return this.price; } setTokenPrice(chain: ChainName, price: number): void { diff --git a/yarn.lock b/yarn.lock index 0177617da..71c573b5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8066,7 +8066,6 @@ __metadata: "@safe-global/safe-deployments": "npm:1.37.8" "@solana/spl-token": "npm:^0.3.8" "@solana/web3.js": "npm:^1.78.0" - "@types/coingecko-api": "npm:^1.0.10" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^16.9.1" "@types/sinon": "npm:^17.0.1" @@ -8075,7 +8074,7 @@ __metadata: "@wagmi/chains": "npm:^1.8.0" bignumber.js: "npm:^9.1.1" chai: "npm:4.5.0" - coingecko-api: "npm:^1.0.10" + coingecko-api-v3: "npm:^0.0.29" cosmjs-types: "npm:^0.9.0" cross-fetch: "npm:^3.1.5" dotenv: "npm:^10.0.0" @@ -13160,13 +13159,6 @@ __metadata: languageName: node 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": version: 1.6.1 resolution: "@types/concat-stream@npm:1.6.1" @@ -16754,10 +16746,12 @@ __metadata: languageName: node linkType: hard -"coingecko-api@npm:^1.0.10": - version: 1.0.10 - resolution: "coingecko-api@npm:1.0.10" - checksum: e0000df5aebbeee508f25824485fe8e4be57cd07825b3cfbf2dc3c51b646200eefd336c833e81747d4a209bf10c32019baef1070fb2bfbcdbae099420954d1fa +"coingecko-api-v3@npm:^0.0.29": + version: 0.0.29 + resolution: "coingecko-api-v3@npm:0.0.29" + dependencies: + https: "npm:^1.0.0" + checksum: e60a0996472419232a144ec77028c060bd9c289f799dd40d46dbb7229cff3d868a3e35bf88724059dc25767b8136d794789e4dd31711592fa73a7be1ca2fcbc7 languageName: node linkType: hard @@ -21651,6 +21645,13 @@ __metadata: languageName: node 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": version: 1.0.2 resolution: "human-id@npm:1.0.2"