Files
2026-05-27 23:29:53 +01:00

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 CoinMarketCapProvider is constructed with a valid apiKey
  • THEN provider.name === "coinmarketcap", provider.supportsCrypto === true, provider.supportsFiat === false, and provider.supportsVirtual is 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 CurrencyProviderError is thrown synchronously with code === "CURRENCY_PROVIDER.MISSING_CONFIG" and providerName === "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 GET to https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol=BTC,ETH&convert=EUR with X-CMC_PRO_API_KEY and Accept: application/json headers.

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,DOT and convert=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 fetchRates rejects.

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 with CurrencyProviderError whose code === "CURRENCY_PROVIDER.HTTP_ERROR", status === 503, and providerName === "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 with CurrencyProviderError, code === "CURRENCY_PROVIDER.PARSE_ERROR".

Scenario: Missing data field raises PARSE_ERROR

  • WHEN the upstream returns { "status": { "error_code": null } }
  • THEN fetchRates() rejects with CurrencyProviderError, 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 with CurrencyProviderError, code === "CURRENCY_PROVIDER.UPSTREAM_ERROR", and upstreamMessage === "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 with CurrencyProviderError, 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 } } } } } for fetchRates({ base: "USD", targets: ["BTC"] })
  • THEN the result has base === "USD", rates.BTC === 1 / 50000, and a numeric timestamp.

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 fetch field and globalThis.fetch is 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).