# Capability Spec Delta: currency-provider-cryptocompare (currency-provider-cryptocompare) **Package**: `@suaveplan/currency-provider-cryptocompare` **Status**: ARCHIVED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: CryptoCompareProvider — public pricemulti rate fetch The package SHALL export `CryptoCompareProvider` implementing `CurrencyProvider` from `@suaveplan/currency` with `name === "cryptocompare"`, `supportsCrypto === true`, `supportsFiat === false`. `fetchRates(options?)` SHALL call `GET /pricemulti?fsyms=&tsyms=` via the shared `httpRequest` helper from `@suaveplan/currency-providers`, defaulting `base` to `"USD"` and `targets` to the 15-symbol built-in list when `options.targets` is omitted or empty. Returned `rates` SHALL be the inverted spot price (`1 / price`) for every symbol whose quote in the requested `base` is a finite positive number; non-finite and non-positive quotes SHALL be skipped. #### Scenario: defaults base to USD and inverts pricemulti payload - **WHEN** `fetchRates()` is called against `{ BTC: { USD: 67000 }, ETH: { USD: 3500 }, FAKE: { USD: 0 } }` - **THEN** `result.base === "USD"`, `result.rates.BTC` ≈ `1 / 67000`, `result.rates.ETH` ≈ `1 / 3500`, and `result.rates.FAKE` is absent. #### Scenario: honours custom baseUrl and explicit targets - **WHEN** `fetchRates({ base: "EUR", targets: ["BTC", "ETH"] })` is called with `baseUrl: "https://staging.example.com/data"` - **THEN** the request URL is exactly `https://staging.example.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR`. #### Scenario: empty targets array falls back to built-in defaults - **WHEN** `fetchRates({ base: "USD", targets: [] })` is called - **THEN** the request URL `fsyms` parameter starts with `BTC,ETH,SOL,USDT`. **Verified by:** `src/cryptocompare/cryptocompare.test.ts::"reports stable metadata identifying the provider"` AND `src/cryptocompare/cryptocompare.test.ts::"inverts pricemulti payload, skips zero prices, and defaults base to USD"` AND `src/cryptocompare/cryptocompare.test.ts::"honours custom baseUrl and explicit targets list"` AND `src/cryptocompare/cryptocompare.test.ts::"falls back to defaults when targets array is empty"` --- ### Requirement: Optional API key attached via Apikey authorization header The package SHALL accept an **optional** `apiKey` field on the constructor config. When `apiKey` is provided and non-whitespace, every outbound request SHALL carry an `authorization: Apikey ` header per CryptoCompare's documented Apikey auth scheme. When `apiKey` is omitted, `undefined`, or whitespace-only, the request SHALL be issued with no authorization header (keyless free-tier mode). #### Scenario: keyless construction issues no authorization header - **WHEN** `new CryptoCompareProvider({ fetch }).fetchRates(...)` runs - **THEN** the injected fetch sees `init?.headers === undefined`. #### Scenario: keyed construction issues authorization: Apikey - **WHEN** `new CryptoCompareProvider({ fetch, apiKey: "secret-key" }).fetchRates(...)` runs - **THEN** the injected fetch sees `init.headers.authorization === "Apikey secret-key"`. #### Scenario: whitespace-only apiKey is treated as absent - **WHEN** `new CryptoCompareProvider({ fetch, apiKey: " " }).fetchRates(...)` runs - **THEN** the injected fetch sees `init?.headers === undefined`. **Verified by:** `src/cryptocompare/cryptocompare.test.ts::"sends authorization header only when an apiKey is provided"` AND `src/cryptocompare/cryptocompare.test.ts::"treats whitespace-only apiKey as absent and omits the header"` --- ### Requirement: Typed-error mapping for HTTP, parse, upstream, and unknown-currency failures The package SHALL surface every failure as `CurrencyProviderError` from `@suaveplan/currency-providers` with `providerName === "cryptocompare"`. Non-2xx responses SHALL raise `HTTP_ERROR` (via the shared `httpRequest` helper, populating `status`). Invalid JSON bodies and non-object parsed payloads SHALL raise `PARSE_ERROR`. Payloads carrying `Response: "Error"` SHALL raise `UPSTREAM_ERROR` with `upstreamMessage` set to the upstream `Message` field, or to `"unknown upstream error"` when `Message` is absent. `fetchRates()` SHALL raise `UNKNOWN_CURRENCY` when no requested symbol resolves to a finite positive quote in the configured `base`. #### Scenario: non-2xx response raises HTTP_ERROR - **WHEN** the injected fetch returns HTTP 502 - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.HTTP_ERROR"` and `status === 502`. #### Scenario: invalid JSON body raises PARSE_ERROR - **WHEN** the body is `"<>"` with `content-type: application/json` - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.PARSE_ERROR"`. #### Scenario: non-object payload (e.g. JSON null) raises PARSE_ERROR - **WHEN** the body is `"null"` parsed as JSON - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.PARSE_ERROR"`. #### Scenario: upstream error payload raises UPSTREAM_ERROR - **WHEN** the payload is `{ Response: "Error", Message: "tsym XYZ is not available" }` - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.UPSTREAM_ERROR"` and `upstreamMessage` contains `"tsym XYZ"`. #### Scenario: upstream error payload without Message uses default text - **WHEN** the payload is `{ Response: "Error" }` - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.UPSTREAM_ERROR"` and `upstreamMessage === "unknown upstream error"`. #### Scenario: empty result map raises UNKNOWN_CURRENCY - **WHEN** `fetchRates({ base: "USD", targets: ["BTC"] })` is called and the payload is `{}` - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`. #### Scenario: every quote non-finite or non-positive raises UNKNOWN_CURRENCY - **WHEN** the payload contains only `0`, `NaN`, and negative quotes - **THEN** the call rejects with `code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`. **Verified by:** `src/cryptocompare/cryptocompare.test.ts::"raises HTTP_ERROR on non-2xx response"` AND `src/cryptocompare/cryptocompare.test.ts::"raises PARSE_ERROR when the response body is not valid JSON"` AND `src/cryptocompare/cryptocompare.test.ts::"raises PARSE_ERROR when the parsed payload is not an object"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UPSTREAM_ERROR when payload carries Response='Error' with Message"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UPSTREAM_ERROR with default message when Message field is absent"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UNKNOWN_CURRENCY when no symbols resolve to a quote"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UNKNOWN_CURRENCY when every quote is non-finite or non-positive"` --- ### Requirement: Caller AbortSignal forwarded into shared httpRequest The package SHALL forward `RateFetchOptions.signal` (when provided) through the shared `httpRequest` helper so caller-initiated aborts propagate to the underlying `FetchLike` call. #### Scenario: caller signal reaches the injected fetch - **WHEN** `fetchRates({ signal: controller.signal, ... })` is called with a recording `FetchLike` - **THEN** the recorded `init.signal` is exactly `controller.signal`. **Verified by:** `src/cryptocompare/cryptocompare.test.ts::"forwards a caller AbortSignal through httpRequest"`