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

8.3 KiB

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<RateFetchResult>.

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"