diff --git a/.env.example b/.env.example index 672ae40..eda9c6e 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,18 @@ 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] +# 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/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b733717 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + branches: [develop, main] + push: + branches: [develop, main] + +jobs: + build: + name: Build & Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build + run: yarn build + + - name: Lint + run: yarn lint + continue-on-error: true diff --git a/README.md b/README.md index 7a7c221..1381fc7 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 b7aa24d..d6f2a92 100644 --- a/api.config.ts +++ b/api.config.ts @@ -9,14 +9,22 @@ dotenv.config(); // Verify environment if (process.env.RPC_URL_MAINNET === undefined) throw new Error('RPC_URL_MAINNET not available'); if (process.env.RPC_URL_POLYGON === undefined) throw new Error('RPC_URL_POLYGON not available'); -if (process.env.COINGECKO_API_KEY === undefined) throw new Error('COINGECKO_API_KEY not available'); +// 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 export type ConfigType = { app: string; indexer: string; indexerFallback: string; - coingeckoApiKey: string; + coingeckoBaseUrl: string; + coingeckoApiKey: string | undefined; chain: Chain; network: { mainnet: string; @@ -42,7 +50,8 @@ export const CONFIG: ConfigType = { app: process.env.CONFIG_APP_URL || 'https://app.deuro.com', indexer: process.env.CONFIG_INDEXER_URL || 'https://ponder.deuro.com/', indexerFallback: process.env.CONFIG_INDEXER_FALLBACK_URL || 'https://dev.ponder.deuro.com/', - coingeckoApiKey: process.env.COINGECKO_API_KEY, + coingeckoBaseUrl: process.env.COINGECKO_BASE_URL, + coingeckoApiKey: process.env.COINGECKO_API_KEY || undefined, chain: process.env.CONFIG_CHAIN === 'polygon' ? polygon : mainnet, // @dev: default mainnet network: { mainnet: process.env.RPC_URL_MAINNET, @@ -112,10 +121,19 @@ export const VIEM_CONFIG = createPublicClient({ }); // COINGECKO CLIENT +// +// 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) => { - const hasParams = query.includes('?'); - const uri: string = `https://pro-api.coingecko.com${query}`; - return fetch(`${uri}${hasParams ? '&' : '?'}x_cg_pro_api_key=${CONFIG.coingeckoApiKey}`); + const headers: Record = { accept: 'application/json' }; + if (CONFIG.coingeckoApiKey) { + headers['x-cg-pro-api-key'] = CONFIG.coingeckoApiKey; + } + return fetch(`${CONFIG.coingeckoBaseUrl}${query}`, { headers }); }; // Contract addresses for the active chain