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
hypAddress: stride1pvtesu3ve7qn7ctll2x495mrqf2ysp6fws68grvcu6f7n2ajghgsh2jdj6
tokenAddress: ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801
tokenCoinGeckoId: celestia
name: Celestia
symbol: TIA
decimals: 6

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

@ -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.

@ -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<ChainMetadata>,
) {
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<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);

@ -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",

@ -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<number>;
}
export type CoinGeckoInterface = Pick<CoinGecko, 'simple'>;
export type CoinGeckoSimpleInterface = CoinGecko['simple'];
export type CoinGeckoSimplePriceParams = Parameters<
CoinGeckoSimpleInterface['price']
>[0];
export type CoinGeckoResponse = ReturnType<CoinGeckoSimpleInterface['price']>;
export type CoinGeckoInterface = Pick<CoinGeckoClient, 'simplePrice'>;
export type CoinGeckoSimplePriceInterface = CoinGeckoClient['simplePrice'];
export type CoinGeckoSimplePriceParams =
Parameters<CoinGeckoSimplePriceInterface>[0];
export type CoinGeckoResponse = ReturnType<CoinGeckoSimplePriceInterface>;
type TokenPriceCacheEntry = {
price: number;
@ -85,10 +84,11 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
static withDefaultCoinGecko(
chainMetadata: ChainMap<ChainMetadata>,
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;

@ -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 {

@ -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"

Loading…
Cancel
Save