diff --git a/.env.example b/.env.example index 39915e2..a8e8239 100644 --- a/.env.example +++ b/.env.example @@ -8,12 +8,18 @@ CONFIG_CHAIN=testnet RPC_URL_MAINNET=https://rpc.citreascan.com RPC_URL_TESTNET=https://rpc.testnet.citreascan.com -# CoinGecko: set EITHER a base URL pointing at a fronting pricing proxy (which -# injects the upstream key itself, recommended for in-cluster deployments) OR a -# direct Pro key. Setting only COINGECKO_BASE_URL is the cleanest setup; setting -# only COINGECKO_API_KEY routes through pro-api.coingecko.com directly. -# COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko -COINGECKO_API_KEY=[API-KEY] +# CoinGecko Configuration. +# +# COINGECKO_BASE_URL: required. The origin the api calls. Recommended is +# the in-cluster pricing-proxy (https://github.com/DFXswiss/pricing-proxy), +# which holds the upstream Pro key and serves a 60 s shared cache. Anything +# CoinGecko-compatible works (pro-api.coingecko.com, api.coingecko.com, …). +COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko +# +# COINGECKO_API_KEY: optional. If set, attached as `x-cg-pro-api-key` to +# every request. Leave unset when talking to the pricing-proxy (proxy injects +# its own key) or to the public host anonymously. +# COINGECKO_API_KEY= TELEGRAM_BOT_TOKEN=[API-KEY] TELEGRAM_GROUPS_JSON=telegram.groups.json diff --git a/README.md b/README.md index 247bd5c..0015c88 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,45 @@ CONFIG_CHAIN=mainnet RPC_URL_MAINNET=https://eth-mainnet.g.alchemy.com/v2/[API-KEY] RPC_URL_POLYGON=https://polygon-mainnet.g.alchemy.com/v2/[API-KEY] -COINGECKO_API_KEY=[API-KEY] +# See "CoinGecko" section below. +COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko +# COINGECKO_API_KEY= TELEGRAM_BOT_TOKEN=[API-KEY] TELEGRAM_GROUPS_JSON=telegram.groups.json TELEGRAM_IMAGES_DIR=./images ``` +## CoinGecko + +The api fetches token prices from a CoinGecko-compatible endpoint. +Configuration is two env vars: + +| Var | Required | Purpose | +|---|---|---| +| `COINGECKO_BASE_URL` | yes | Origin the api calls. | +| `COINGECKO_API_KEY` | no | Attached as the `x-cg-pro-api-key` header on every request when set. | + +The recommended deployment is the +[**pricing-proxy**](https://github.com/DFXswiss/pricing-proxy) — a small +caching reverse-proxy in front of CoinGecko Pro. It holds the upstream +key, serves a 60 s shared cache, validates upstream error envelopes, and +coalesces concurrent identical requests. When you use the proxy: + +```env +COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko +# COINGECKO_API_KEY left unset — the proxy injects its own key +``` + +Without the proxy, talk to CoinGecko directly: + +```env +COINGECKO_BASE_URL=https://pro-api.coingecko.com +COINGECKO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +The api refuses to start without `COINGECKO_BASE_URL`. + ## Running the app ```bash diff --git a/api.config.ts b/api.config.ts index c7c483b..2063b8e 100644 --- a/api.config.ts +++ b/api.config.ts @@ -9,11 +9,13 @@ dotenv.config(); const isMainnet = process.env.CONFIG_CHAIN === 'mainnet'; if (isMainnet && process.env.RPC_URL_MAINNET === undefined) throw new Error('RPC_URL_MAINNET not available'); if (!isMainnet && process.env.RPC_URL_TESTNET === undefined) throw new Error('RPC_URL_TESTNET not available'); -// Either a key for direct Pro access OR a base URL for a fronting pricing proxy -// must be set; otherwise the upstream CoinGecko calls are anonymous and fail -// under load. -if (!process.env.COINGECKO_API_KEY && !process.env.COINGECKO_BASE_URL) { - throw new Error('COINGECKO_API_KEY or COINGECKO_BASE_URL must be set'); +// COINGECKO_BASE_URL is the origin the api calls — typically the in-cluster +// pricing-proxy (https://github.com/DFXswiss/pricing-proxy), but any +// CoinGecko-compatible host works. COINGECKO_API_KEY is optional and is +// only attached as `x-cg-pro-api-key` on every request when set (proxy mode +// leaves it unset because the proxy injects its own key). +if (!process.env.COINGECKO_BASE_URL) { + throw new Error('COINGECKO_BASE_URL is not set'); } // Config type @@ -21,8 +23,8 @@ export type ConfigType = { app: string; indexer: string; indexerFallback: string; + coingeckoBaseUrl: string; coingeckoApiKey: string | undefined; - coingeckoBaseUrl: string | undefined; chain: Chain; network: { mainnet: string; @@ -47,8 +49,8 @@ export const CONFIG: ConfigType = { app: process.env.CONFIG_APP_URL, indexer: process.env.CONFIG_INDEXER_URL, indexerFallback: process.env.CONFIG_INDEXER_FALLBACK_URL, + coingeckoBaseUrl: process.env.COINGECKO_BASE_URL, coingeckoApiKey: process.env.COINGECKO_API_KEY || undefined, - coingeckoBaseUrl: process.env.COINGECKO_BASE_URL || undefined, chain: isMainnet ? mainnet : testnet, network: { mainnet: process.env.RPC_URL_MAINNET, @@ -83,10 +85,6 @@ const SENSITIVE_KEYS = new Set([ 'twitter.accessSecret', ]); -// `coingeckoBaseUrl` is intentionally NOT redacted — it is a non-secret pointer -// (typically the in-cluster pricing proxy origin) and useful at startup for -// confirming routing. - function redactConfig(config: T): T { return walkRedact(config, '') as T; } @@ -127,20 +125,18 @@ export const VIEM_CONFIG = createPublicClient({ // COINGECKO CLIENT // -// Resolution priority: -// 1. COINGECKO_BASE_URL set → trust the caller (typically a pricing proxy that -// injects the upstream key itself); send no auth. -// 2. COINGECKO_API_KEY set → Pro tier: pro-api.coingecko.com with the -// `x-cg-pro-api-key` header. The earlier query-string form is supported by -// CoinGecko but cache-poisons proxies that key on the URL. +// Calls go to whatever `COINGECKO_BASE_URL` points at. When the optional +// `COINGECKO_API_KEY` is set, it is attached as the `x-cg-pro-api-key` +// header — orthogonal to the base URL, never a fallback. The recommended +// deployment is the in-cluster pricing-proxy +// (https://github.com/DFXswiss/pricing-proxy), which injects its own key +// and leaves COINGECKO_API_KEY unset on every consumer. export const COINGECKO_CLIENT = (query: string) => { - if (CONFIG.coingeckoBaseUrl) { - return fetch(`${CONFIG.coingeckoBaseUrl}${query}`); + const headers: Record = { accept: 'application/json' }; + if (CONFIG.coingeckoApiKey) { + headers['x-cg-pro-api-key'] = CONFIG.coingeckoApiKey; } - const uri: string = `https://pro-api.coingecko.com${query}`; - return fetch(uri, { - headers: { 'x-cg-pro-api-key': CONFIG.coingeckoApiKey ?? '' }, - }); + return fetch(`${CONFIG.coingeckoBaseUrl}${query}`, { headers }); }; export const PROTOCOL_STABLECOIN_SYMBOL = 'JUSD';