# Capability Spec Delta: currency-provider-kraken (add-currency-provider-kraken) **Package**: `@suaveplan/currency-provider-kraken` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: KrakenProvider implements the CurrencyProvider contract The package SHALL export a `KrakenProvider` class implementing the `CurrencyProvider` contract from `@suaveplan/currency`. Instances MUST expose `name = "kraken"`, `supportsCrypto = true`, `supportsFiat = false`, and a `fetchRates(options?)` method returning a `Promise`. #### Scenario: identity metadata is fixed - **WHEN** a new `KrakenProvider()` is constructed - **THEN** `provider.name === "kraken"`, `provider.supportsCrypto === true`, and `provider.supportsFiat === false` **Verified by:** `src/provider/provider.test.ts::"exposes the canonical kraken identity"` --- ### Requirement: Constructor accepts NoKeyConfig with optional extras The constructor SHALL accept an optional `KrakenProviderConfig` extending the slim core's `NoKeyConfig` (optional `baseUrl`, optional injectable `FetchLike`). It MUST additionally accept an optional `extraSymbols` record that is merged on top of `KRAKEN_SYMBOL_MAP` so callers can extend coverage without forking the package. The constructor SHALL NOT require an API key. #### Scenario: defaults are used when no config is supplied - **WHEN** `new KrakenProvider()` is constructed and `fetchRates()` is called - **THEN** the request URL begins with `DEFAULT_KRAKEN_BASE_URL` and the injected `globalThis.fetch` is used #### Scenario: custom baseUrl and fetch are honoured - **WHEN** `new KrakenProvider({ baseUrl: "https://custom.kraken", fetch: recordingFetch })` is constructed and `fetchRates()` is called - **THEN** `recordingFetch` is invoked exactly once with a URL beginning with `"https://custom.kraken"` #### Scenario: extraSymbols extends the symbol map - **WHEN** `new KrakenProvider({ extraSymbols: { MATIC: "MATICUSD" } })` is constructed and the Kraken response carries `MATICUSD` - **THEN** the returned `rates` map contains a `MATIC` entry **Verified by:** `src/provider/provider.test.ts::"defaults to globalThis.fetch and DEFAULT_KRAKEN_BASE_URL"` AND `src/provider/provider.test.ts::"honours custom baseUrl and fetch"` AND `src/provider/provider.test.ts::"extends the symbol map via extraSymbols"` --- ### Requirement: Uses the shared httpRequest helper The provider SHALL issue its request through `httpRequest()` from `@suaveplan/currency-providers/http/http` so that abort-signal composition, timeout derivation, and non-2xx → `HTTP_ERROR` mapping are uniform across every sibling provider. It SHALL forward `options.signal` to the helper. #### Scenario: non-2xx response raises HTTP_ERROR - **WHEN** the injected fetch responds with status 503 - **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose `code === CURRENCY_PROVIDER_CODES.HTTP_ERROR`, `providerName === "kraken"`, and `status === 503` #### Scenario: caller signal aborts the request - **WHEN** `options.signal` is an already-aborted `AbortSignal` - **THEN** `fetchRates({ signal })` rejects with an abort error (the injected fetch sees an aborted signal) **Verified by:** `src/provider/provider.test.ts::"raises HTTP_ERROR on non-2xx response"` AND `src/provider/provider.test.ts::"forwards options.signal to the underlying fetch"` --- ### Requirement: Maps Kraken error array to UPSTREAM_ERROR When the Kraken response body's `error` field is a non-empty string array, the provider SHALL raise `CurrencyProviderError(UPSTREAM_ERROR)` with `providerName: "kraken"` and `upstreamMessage` set to the first element of the array. The error message string SHALL include the first upstream message so existing `Error.message` consumers continue to match it. #### Scenario: non-empty error array maps to UPSTREAM_ERROR - **WHEN** the response body is `{ error: ["EGeneral:Too many requests"], result: {} }` - **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose `code === CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR`, `upstreamMessage === "EGeneral:Too many requests"`, and `message` contains `"Too many requests"` **Verified by:** `src/provider/provider.test.ts::"maps a non-empty error array to UPSTREAM_ERROR"` --- ### Requirement: Maps unparsable bodies to PARSE_ERROR When the response body is not valid JSON, or when the parsed body's `result` field is missing / not an object, the provider SHALL raise `CurrencyProviderError(PARSE_ERROR)` with `providerName: "kraken"`. #### Scenario: malformed JSON raises PARSE_ERROR - **WHEN** the response body is `"not-json"` - **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose `code === CURRENCY_PROVIDER_CODES.PARSE_ERROR` #### Scenario: missing result field raises PARSE_ERROR - **WHEN** the response body is `{ "error": [] }` (no `result`) - **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose `code === CURRENCY_PROVIDER_CODES.PARSE_ERROR` **Verified by:** `src/provider/provider.test.ts::"raises PARSE_ERROR on malformed JSON"` AND `src/provider/provider.test.ts::"raises PARSE_ERROR when result is missing"` --- ### Requirement: Maps ticker prices to reciprocal-of-USD rates For every entry in `data.result` whose Kraken pair matches a mapped symbol, the provider SHALL set `rates[symbol] = 1 / Number.parseFloat(ticker.c[0])` when that float is positive and finite. Entries whose price does not parse to a positive finite number SHALL be skipped (not raised) — Kraken occasionally returns `"0.00"` for freshly-listed pairs. #### Scenario: positive prices are reciprocated - **WHEN** the response carries `XXBTZUSD: { c: ["67000.00", "0.1"] }` and `XETHZUSD: { c: ["3500.00", "1.0"] }` - **THEN** the returned rates satisfy `rates.BTC ≈ 1/67000` and `rates.ETH ≈ 1/3500` #### Scenario: zero / non-finite prices are skipped, not raised - **WHEN** the response carries `XXBTZUSD: { c: ["0.00", "0"] }` alongside a valid `XETHZUSD` entry - **THEN** the returned rates contain `ETH` but not `BTC`, and no error is raised **Verified by:** `src/provider/provider.test.ts::"maps positive prices to reciprocal-of-USD rates"` AND `src/provider/provider.test.ts::"skips zero or non-finite prices without raising"` --- ### Requirement: Filters by options.targets and raises on all-unknown targets When `options.targets` is supplied, the provider SHALL return only rates for the requested target symbols. If after filtering the resulting rate map is empty AND `options.targets` was non-empty, the provider SHALL raise `CurrencyProviderError(UNKNOWN_CURRENCY)` with `providerName: "kraken"` so callers see the failure rather than a silent empty rate set. #### Scenario: subset filtering returns only requested targets - **WHEN** the response carries BTC + ETH + SOL and `fetchRates({ targets: ["BTC"] })` is called - **THEN** the returned `rates` has exactly one key — `"BTC"` #### Scenario: all-unknown targets raise UNKNOWN_CURRENCY - **WHEN** `fetchRates({ targets: ["FAKE1", "FAKE2"] })` is called against a healthy ticker response - **THEN** `fetchRates` rejects with a `CurrencyProviderError` whose `code === CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY` and `providerName === "kraken"` **Verified by:** `src/provider/provider.test.ts::"filters by options.targets"` AND `src/provider/provider.test.ts::"raises UNKNOWN_CURRENCY when every requested target is unmapped"` --- ### Requirement: Exposes the symbol-mapping surface The package SHALL export `KRAKEN_SYMBOL_MAP` (the canonical symbol → Kraken pair record), `KRAKEN_PAIR_TO_SYMBOL` (the reverse map, pair → symbol), and `DEFAULT_KRAKEN_BASE_URL`. The forward map SHALL be `Object.freeze`-d. #### Scenario: symbol map is exported, frozen, and complete - **WHEN** the package is imported - **THEN** `KRAKEN_SYMBOL_MAP` is exported, `Object.isFrozen(KRAKEN_SYMBOL_MAP)` is `true`, and it contains entries for `BTC`, `ETH`, `SOL`, `USDT`, `DOT`, `ADA`, `LINK`, `AVAX`, `ATOM`, `LTC`, `DOGE`, `XRP` #### Scenario: reverse map is consistent with the forward map - **WHEN** any entry `KRAKEN_SYMBOL_MAP[symbol] === pair` is examined - **THEN** `KRAKEN_PAIR_TO_SYMBOL[pair] === symbol` **Verified by:** `src/symbols/symbols.test.ts::"exports a frozen 12-entry symbol map"` AND `src/symbols/symbols.test.ts::"reverse map is consistent with the forward map"`