9.5 KiB
Capability Spec Delta: currency-provider-coinmarketcap (add-currency-provider-coinmarketcap)
Package: @suaveplan/currency-provider-coinmarketcap
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions.
ADDED Requirements
Requirement: CoinMarketCapProvider — class implementing CurrencyProvider
The package SHALL export CoinMarketCapProvider, a class implementing
CurrencyProvider from @suaveplan/currency with stable identity fields
name = "coinmarketcap", supportsCrypto = true, and supportsFiat = false.
The constructor SHALL accept an ApiKeyConfig (from
@suaveplan/currency-providers) carrying a non-empty apiKey, an optional
baseUrl (default https://pro-api.coinmarketcap.com/v1), and an optional
injectable FetchLike (default globalThis.fetch).
Scenario: Identity fields are populated
- WHEN a
CoinMarketCapProvideris constructed with a validapiKey - THEN
provider.name === "coinmarketcap",provider.supportsCrypto === true,provider.supportsFiat === false, andprovider.supportsVirtualis undefined.
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"exposes stable identity fields"
Requirement: Constructor rejects missing or empty API key
The package SHALL raise CurrencyProviderError(MISSING_CONFIG) from the
constructor when config.apiKey is the empty string or contains only
whitespace. The error SHALL carry providerName: "coinmarketcap".
Scenario: Empty apiKey is rejected at construction time
- WHEN
new CoinMarketCapProvider({ apiKey: "" })is called - THEN a
CurrencyProviderErroris thrown synchronously withcode === "CURRENCY_PROVIDER.MISSING_CONFIG"andproviderName === "coinmarketcap".
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"raises MISSING_CONFIG when apiKey is empty" AND src/coinmarketcap/coinmarketcap.test.ts::"raises MISSING_CONFIG when apiKey is whitespace"
Requirement: fetchRates issues an authenticated quotes/latest request
fetchRates(options?) SHALL call
GET <baseUrl>/cryptocurrency/quotes/latest?symbol=<targets-csv>&convert=<base>
through the shared httpRequest() helper from
@suaveplan/currency-providers. The request SHALL carry the headers
X-CMC_PRO_API_KEY: <apiKey> and Accept: application/json. When
options.targets is omitted, the request SHALL query a default symbol set
covering the top crypto-by-market-cap (BTC,ETH,SOL,USDT,USDC,BNB,XRP,ADA,DOGE,DOT).
When options.base is omitted, the request SHALL convert into USD.
options.signal SHALL be forwarded to httpRequest() so caller cancellation
propagates to the underlying fetch.
Scenario: Builds the canonical URL and headers
- WHEN
fetchRates({ base: "EUR", targets: ["BTC", "ETH"] })is called - THEN the injected fetch receives a
GETtohttps://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol=BTC,ETH&convert=EURwithX-CMC_PRO_API_KEYandAccept: application/jsonheaders.
Scenario: Defaults targets to the top-10 symbol set and base to USD
- WHEN
fetchRates()is called with no options - THEN the request URL contains
symbol=BTC,ETH,SOL,USDT,USDC,BNB,XRP,ADA,DOGE,DOTandconvert=USD.
Scenario: Forwards caller AbortSignal
- WHEN the caller aborts the signal passed via
options.signal - THEN the underlying fetch rejects with the abort reason and
fetchRatesrejects.
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"issues an authenticated GET to /cryptocurrency/quotes/latest" AND src/coinmarketcap/coinmarketcap.test.ts::"defaults to USD + top-10 symbols when no options are given" AND src/coinmarketcap/coinmarketcap.test.ts::"forwards the caller AbortSignal to the injected fetch"
Requirement: Non-2xx HTTP responses surface as HTTP_ERROR
The package SHALL surface non-2xx responses as
CurrencyProviderError(HTTP_ERROR) (raised by the shared httpRequest()
helper, with status populated and providerName: "coinmarketcap").
Scenario: 503 maps to HTTP_ERROR with status
- WHEN the upstream returns
503 Service Unavailable - THEN
fetchRates()rejects withCurrencyProviderErrorwhosecode === "CURRENCY_PROVIDER.HTTP_ERROR",status === 503, andproviderName === "coinmarketcap".
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"surfaces non-2xx responses as HTTP_ERROR"
Requirement: Malformed JSON or missing data map raises PARSE_ERROR
The package SHALL raise CurrencyProviderError(PARSE_ERROR) when the
upstream payload is not valid JSON, when the parsed value is not an object,
or when the data field is missing / not an object. The error SHALL carry
providerName: "coinmarketcap".
Scenario: Invalid JSON body raises PARSE_ERROR
- WHEN the upstream returns a 200 with body
"not-json" - THEN
fetchRates()rejects withCurrencyProviderError,code === "CURRENCY_PROVIDER.PARSE_ERROR".
Scenario: Missing data field raises PARSE_ERROR
- WHEN the upstream returns
{ "status": { "error_code": null } } - THEN
fetchRates()rejects withCurrencyProviderError,code === "CURRENCY_PROVIDER.PARSE_ERROR".
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"raises PARSE_ERROR when the body is not valid JSON" AND src/coinmarketcap/coinmarketcap.test.ts::"raises PARSE_ERROR when the data field is missing"
Requirement: Upstream-reported errors surface as UPSTREAM_ERROR
When the upstream payload carries a non-null, non-zero status.error_code,
the package SHALL raise CurrencyProviderError(UPSTREAM_ERROR) carrying the
upstream status.error_message (when present) on the upstreamMessage
discriminant and providerName: "coinmarketcap".
Scenario: Upstream error_code populated
- WHEN the upstream returns
{ "status": { "error_code": 1006, "error_message": "Insufficient credits" } } - THEN
fetchRates()rejects withCurrencyProviderError,code === "CURRENCY_PROVIDER.UPSTREAM_ERROR", andupstreamMessage === "Insufficient credits".
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"raises UPSTREAM_ERROR when status.error_code is populated"
Requirement: No resolved symbols raises UNKNOWN_CURRENCY
When the upstream payload contains a valid data map but none of the
requested target symbols resolve to a quote in the configured base
currency, the package SHALL raise
CurrencyProviderError(UNKNOWN_CURRENCY) carrying
providerName: "coinmarketcap".
Scenario: Data map empty for requested symbols
- WHEN the upstream returns
{ "data": {} }for the requested symbols - THEN
fetchRates()rejects withCurrencyProviderError,code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY".
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"raises UNKNOWN_CURRENCY when no requested symbol resolved"
Requirement: Resolved rates are inverted to base-per-unit form
CoinMarketCap returns 1 crypto = X fiat; the package SHALL invert each
returned price into 1 fiat = (1 / price) crypto and build the
RateFetchResult.rates map keyed by symbol. The result's base SHALL
match the requested base; timestamp SHALL be set to Date.now() at
parse time.
Scenario: Returned price is inverted
- WHEN the upstream returns
{ "data": { "BTC": { "quote": { "USD": { "price": 50000 } } } } }forfetchRates({ base: "USD", targets: ["BTC"] }) - THEN the result has
base === "USD",rates.BTC === 1 / 50000, and a numerictimestamp.
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"inverts price into base-per-unit rate"
Requirement: Custom baseUrl and injected fetch are honoured
The package SHALL honour the optional baseUrl and fetch fields of
ApiKeyConfig. When baseUrl is set, the request URL SHALL use it as
prefix. When fetch is set, that closure SHALL be used in place of
globalThis.fetch. When fetch is omitted, globalThis.fetch SHALL be
the fallback.
Scenario: Custom baseUrl prefixes the request
- WHEN the provider is constructed with
baseUrl: "https://sandbox-api.coinmarketcap.com/v1" - THEN the request URL starts with
https://sandbox-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest.
Scenario: Omitted fetch falls back to globalThis.fetch
- WHEN the provider is constructed without a
fetchfield andglobalThis.fetchis temporarily replaced with a recording stub - THEN the recording stub receives the call.
Verified by: src/coinmarketcap/coinmarketcap.test.ts::"honours a custom baseUrl" AND src/coinmarketcap/coinmarketcap.test.ts::"falls back to globalThis.fetch when fetch is not injected"
Requirement: Universal-tier purity and packaging
The package SHALL be universal-tier with sideEffects: false, no
node: / bun: / DOM imports in production source, explicit named
barrels (no export * in non-root index.ts), test files importing
runner primitives only from @suaveplan/testing/runner, and 100/100/100/100
istanbul coverage thresholds. Public subpath exports SHALL include .
and ./coinmarketcap/coinmarketcap.
Scenario: Subpath exports tree-shake the provider class
- WHEN a consumer imports
from "@suaveplan/currency-provider-coinmarketcap/coinmarketcap/coinmarketcap" - THEN the import resolves to the provider module and the rest of the package tree-shakes away.
Verified by: package.json::exports AND the §13 pre-ship gate (bunx turbo lint typecheck test --filter=@suaveplan/currency-provider-coinmarketcap + bunx turbo test:coverage).