first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
# `CoinMarketCapProvider` — crypto-rates sibling provider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CoinMarketCapProvider` is the
`@suaveplan/currency-providers` sibling that fetches cryptocurrency
spot prices from the CoinMarketCap professional REST API
(`pro-api.coinmarketcap.com`). It implements `CurrencyProvider` from
`@suaveplan/currency` so it slots directly into `CurrencyConverter`
either by direct instantiation (`new CurrencyConverter({ provider: new
CoinMarketCapProvider(...) })`) or by registration with
`CurrencyProviderRegistry`. The class exists so consumers that need
CoinMarketCap as a primary or fallback rate source install exactly one
provider package instead of pulling in the eight other sibling
providers they do not wire in.
The class is crypto-only — `supportsCrypto = true`, `supportsFiat =
false` — because the upstream `/cryptocurrency/quotes/latest` endpoint
is a crypto quotes feed; the request still accepts any fiat conversion
currency via the `convert` query parameter, so a USD-base, EUR-base, or
GBP-base call is equally valid. Consumers that need fiat rates should
register a fiat sibling alongside (`currency-provider-ecb`,
`currency-provider-fixer`, `currency-provider-open-exchange-rates`).
## Features
- Implements `CurrencyProvider` with stable identity (`name =
"coinmarketcap"`, `supportsCrypto = true`, `supportsFiat = false`).
- Constructor accepts `ApiKeyConfig` (`apiKey`, optional `baseUrl`,
optional injectable `FetchLike`); empty / whitespace `apiKey`
immediately raises `CurrencyProviderError(MISSING_CONFIG)`.
- `X-CMC_PRO_API_KEY` + `Accept: application/json` headers carried on
every request through the shared `httpRequest()` helper, so abort
signals, timeouts, and HTTP error mapping are uniform with every
other sibling provider.
- Default target list covers the top 10 cryptocurrencies by market cap
(`BTC,ETH,SOL,USDT,USDC,BNB,XRP,ADA,DOGE,DOT`); callers can override
via `options.targets` to query any supported symbol set.
- Default `base` is `USD`; any fiat conversion currency supported by
CoinMarketCap is accepted via `options.base`.
- Prices are inverted from CoinMarketCap's `1 crypto = X fiat` form to
the converter's expected `1 fiat = (1 / X) crypto` form before being
written into `RateFetchResult.rates`.
- Typed error mapping — `HTTP_ERROR` (non-2xx), `PARSE_ERROR` (invalid
JSON / missing `data` field / non-object data), `UPSTREAM_ERROR`
(`status.error_code` populated), `UNKNOWN_CURRENCY` (no requested
symbol resolved), `MISSING_CONFIG` (empty `apiKey`) — every error
carries `providerName: "coinmarketcap"`.
- Universal — no `node:` / `bun:` / DOM imports; runs unchanged
under Node 18+, Bun, modern browsers, and Web Workers via the
injectable `FetchLike` seam.
## Usage: Basic
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { CoinMarketCapProvider } from "@suaveplan/currency-provider-coinmarketcap";
const provider = new CoinMarketCapProvider({
apiKey: process.env.CMC_API_KEY ?? "",
});
const converter = new CurrencyConverter({ provider });
const rate = await converter.getRate("BTC", "USD");
```
## Usage: Advanced
Inject a `FetchLike` for testing, point at the CoinMarketCap sandbox,
restrict the target list, and forward an `AbortSignal` for cancellation:
```ts
import type { FetchLike } from "@suaveplan/types";
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
import { CoinMarketCapProvider } from "@suaveplan/currency-provider-coinmarketcap";
const recordingFetch: FetchLike = async (url, init) => {
console.log("→", url);
return await fetch(url, init);
};
const provider = new CoinMarketCapProvider({
apiKey: process.env.CMC_SANDBOX_KEY ?? "",
baseUrl: "https://sandbox-api.coinmarketcap.com/v1",
fetch: recordingFetch,
});
const registry = new CurrencyProviderRegistry();
registry.register(provider);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 2_000);
try {
const result = await registry.get("coinmarketcap").fetchRates({
base: "EUR",
targets: ["BTC", "ETH", "SOL"],
signal: controller.signal,
});
console.log(result.rates);
} finally {
clearTimeout(timer);
}
```
## API Reference
### `class CoinMarketCapProvider implements CurrencyProvider`
- `readonly name: "coinmarketcap"`
- `readonly supportsCrypto: true`
- `readonly supportsFiat: false`
- `constructor(config: ApiKeyConfig)` — `config.apiKey` is required;
whitespace-only and empty strings raise
`CurrencyProviderError(MISSING_CONFIG)`. `config.baseUrl` defaults to
`https://pro-api.coinmarketcap.com/v1`. `config.fetch` defaults to
`globalThis.fetch`.
- `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>` —
issues `GET <baseUrl>/cryptocurrency/quotes/latest?symbol=<csv>&convert=<base>`
through `@suaveplan/currency-providers`'s `httpRequest()`. Returns
`{ base, rates, timestamp }` with rates inverted into base-per-unit
form. Forwards `options.signal` to the underlying fetch.
### Error codes (all from `CURRENCY_PROVIDER_CODES`)
- `MISSING_CONFIG` — empty / whitespace `apiKey` at construction.
- `HTTP_ERROR` — non-2xx HTTP response (raised by `httpRequest()`).
- `PARSE_ERROR` — invalid JSON, non-object payload, or missing `data`.
- `UPSTREAM_ERROR` — payload `status.error_code` populated and non-zero.
- `UNKNOWN_CURRENCY` — none of the requested symbols resolved.
## Implementation Notes
- The default target list (`BTC,ETH,SOL,USDT,USDC,BNB,XRP,ADA,DOGE,DOT`)
mirrors the upstream `../suaveplan/...coinmarketcap.ts` source —
these are the top ten cryptocurrencies by market cap and are the
same set every consumer of the upstream package received by default.
- Price inversion (`1 / price`) is applied per-symbol so a symbol with
a missing, non-numeric, or non-positive `price` is silently dropped
from the result. If the upstream returns `{ data: {} }` or every
symbol fails the numeric guard, the whole call raises
`UNKNOWN_CURRENCY` — the converter then falls back to its next
registered provider rather than caching a zero-entry result.
- `status.error_code` is treated as "present and non-zero" — many
CoinMarketCap success responses carry `status.error_code: 0`, which
is not a real error. The dedicated `assertNoUpstreamError` guard
preserves that distinction.
- The `parsePayload` step is intentionally lenient about the order of
validation: when both `status.error_code` and a missing `data` field
are present, the upstream error wins so consumers see the actionable
`UPSTREAM_ERROR` (and `upstreamMessage`) rather than a generic
`PARSE_ERROR`.
- All HTTP plumbing — abort-signal composition, timeout derivation,
non-2xx mapping to `HTTP_ERROR` — is delegated to
`@suaveplan/currency-providers`'s `httpRequest()`. The provider
never calls the injected `FetchLike` directly, so timeout / abort
behaviour is identical to every other sibling.