Initial Commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# Design: @suaveplan/currency-provider-kraken
|
||||
|
||||
> Non-normative. The normative requirements live in
|
||||
> `./specs/currency-provider-kraken/spec.md`. The cross-cutting
|
||||
> foundation-with-sibling-plugins architecture lives in
|
||||
> `openspec/archive/2026-05-17-slim-currency-providers/design.md` — read it
|
||||
> first; this document covers only the Kraken-specific deltas.
|
||||
|
||||
## Shape
|
||||
|
||||
```text
|
||||
@suaveplan/currency-provider-kraken
|
||||
├── src/symbols/symbols.ts # KRAKEN_SYMBOL_MAP + pair→symbol reverse
|
||||
├── src/provider/provider.ts # KrakenProvider class + DEFAULT_KRAKEN_BASE_URL
|
||||
└── src/index.ts # explicit named barrel
|
||||
```
|
||||
|
||||
Two feature modules. Each carries a sibling `.md`, a `.test.ts`, and a
|
||||
feature-folder `index.ts` (named-exports only — `export *` is banned per
|
||||
AGENTS.md §6a).
|
||||
|
||||
## Wire shape
|
||||
|
||||
The Kraken public ticker returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": ["EGeneral:Too many requests"],
|
||||
"result": {
|
||||
"XXBTZUSD": { "c": ["67000.00", "0.1"], "..." },
|
||||
"XETHZUSD": { "c": ["3500.00", "1.0"], "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `error` is **always** present (empty array on success). A non-empty array
|
||||
is the canonical upstream-failure signal and maps to
|
||||
`CurrencyProviderError(UPSTREAM_ERROR)`. The first entry is carried on
|
||||
`upstreamMessage` so consumers can dispatch on rate-limit / endpoint /
|
||||
pair-unknown messages.
|
||||
- `result` is keyed by Kraken's non-standard pair name (`XXBTZUSD`, not
|
||||
`BTCUSD`). The symbol map normalises both directions; the reverse map
|
||||
lets us go from the wire pair back to a Suaveplan symbol in O(1).
|
||||
- `c` is `[price, lotVolume]` — `c[0]` is the last-trade-closed price.
|
||||
|
||||
## Symbol mapping
|
||||
|
||||
Twelve symbols are mapped, matching the upstream coverage. The list is
|
||||
intentionally small (rather than dynamic) because:
|
||||
|
||||
- Kraken's full pair list rotates frequently (new listings, delistings) and
|
||||
pulling the live `AssetPairs` list per request would double the round trips.
|
||||
- The mapped set is the high-volume subset most consumers actually want.
|
||||
- Callers who need an additional symbol can supply a custom symbol-to-pair
|
||||
override via the constructor (`config.extraSymbols`) without forking the
|
||||
package. The override map is merged into `KRAKEN_SYMBOL_MAP` at
|
||||
construction time.
|
||||
|
||||
```ts
|
||||
const KRAKEN_SYMBOL_MAP: Readonly<Record<string, string>> = Object.freeze({
|
||||
BTC: "XXBTZUSD",
|
||||
ETH: "XETHZUSD",
|
||||
SOL: "SOLUSD",
|
||||
USDT: "USDTZUSD",
|
||||
DOT: "DOTUSD",
|
||||
ADA: "ADAUSD",
|
||||
LINK: "LINKUSD",
|
||||
AVAX: "AVAXUSD",
|
||||
ATOM: "ATOMUSD",
|
||||
LTC: "XLTCZUSD",
|
||||
DOGE: "XDGUSD",
|
||||
XRP: "XXRPZUSD",
|
||||
});
|
||||
```
|
||||
|
||||
## Rate model
|
||||
|
||||
Kraken returns the USD price of one unit of the crypto (e.g. `1 BTC = 67000
|
||||
USD`). The `CurrencyProvider` contract returns rates as "1 base = N target"
|
||||
where base defaults to `USD`. The rate for `USD → BTC` is therefore the
|
||||
reciprocal: `1 / 67000`. This matches the upstream behaviour verbatim and is
|
||||
what every downstream `CurrencyConverter` assumes.
|
||||
|
||||
If `c[0]` does not parse to a positive finite number, the entry is dropped
|
||||
(skipped) rather than raising — Kraken occasionally returns `"0.00"` for
|
||||
freshly-listed pairs that haven't yet traded. If the entire mapped set drops,
|
||||
the provider raises `UNKNOWN_CURRENCY` so the caller sees the failure rather
|
||||
than a silent empty rate map.
|
||||
|
||||
## Targets filtering
|
||||
|
||||
When `options.targets` is non-empty, the provider filters to those targets
|
||||
after building the full rate map. Any target that is not in
|
||||
`KRAKEN_SYMBOL_MAP` is a no-op (it simply doesn't appear in the result). If
|
||||
the filtered set is empty AND `options.targets` was non-empty, the provider
|
||||
raises `UNKNOWN_CURRENCY` — this is the "the caller asked for X / Y / Z and
|
||||
Kraken can't price any of them" signal, distinct from "Kraken is down"
|
||||
(`HTTP_ERROR`), "Kraken returned an error array" (`UPSTREAM_ERROR`), and
|
||||
"the response body is malformed" (`PARSE_ERROR`).
|
||||
|
||||
## Error path matrix
|
||||
|
||||
| Wire condition | Code | `providerName` | Other discriminants |
|
||||
|---|---|---|---|
|
||||
| Non-2xx response | `HTTP_ERROR` | `"kraken"` | `status` |
|
||||
| `data.error.length > 0` | `UPSTREAM_ERROR` | `"kraken"` | `upstreamMessage = data.error[0]` |
|
||||
| JSON parse failure | `PARSE_ERROR` | `"kraken"` | (cause: original error) |
|
||||
| `data.result` not an object | `PARSE_ERROR` | `"kraken"` | (cause: shape-check failure) |
|
||||
| All requested targets unmapped | `UNKNOWN_CURRENCY` | `"kraken"` | — |
|
||||
|
||||
`HTTP_ERROR` is raised by the shared `httpRequest()` helper (we don't
|
||||
re-implement it); the other three are raised inline after the response body
|
||||
is read.
|
||||
|
||||
## Test strategy
|
||||
|
||||
- `symbols.test.ts` — asserts the 12-symbol map is exported, immutable
|
||||
(`Object.isFrozen`), and that the reverse map is consistent.
|
||||
- `provider.test.ts` — uses a recording `FetchLike` closure (the same
|
||||
pattern the slim core's `http.test.ts` established). Covers:
|
||||
- happy path with a real-shape Kraken response (`error: []`, `result: {…}`)
|
||||
- `supportsCrypto` / `supportsFiat` metadata
|
||||
- rates are reciprocals of `c[0]`
|
||||
- `options.base` default of `"USD"`
|
||||
- `options.targets` filtering (subset)
|
||||
- `options.targets` filtering with one unmapped → still returns mapped ones
|
||||
- `options.targets` all unmapped → `UNKNOWN_CURRENCY`
|
||||
- `data.error` non-empty → `UPSTREAM_ERROR` with `upstreamMessage`
|
||||
- non-2xx response → `HTTP_ERROR` with `status` (via the shared helper)
|
||||
- JSON-parse failure → `PARSE_ERROR`
|
||||
- shape-check failure (missing `result`) → `PARSE_ERROR`
|
||||
- `c[0]` non-positive / non-finite → entry skipped, others retained
|
||||
- custom `baseUrl` is honoured
|
||||
- custom `fetch` (FetchLike) is used in place of `globalThis.fetch`
|
||||
- `options.signal` is forwarded into the `httpRequest` call (abort
|
||||
propagates)
|
||||
|
||||
All tests import runner primitives from `@suaveplan/testing/runner`.
|
||||
No `vi.mock`, no `vi.stubGlobal` — only injection through the constructor.
|
||||
|
||||
## Tier purity
|
||||
|
||||
Universal-tier. The Kraken endpoint is reachable from any runtime that has
|
||||
`fetch`: Node ≥ 18, Bun, modern browsers (subject to Kraken's CORS policy
|
||||
— at time of writing the public ticker endpoint serves
|
||||
`Access-Control-Allow-Origin: *`), Web Workers. No `node:`/`bun:`/DOM
|
||||
imports.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Add @suaveplan/currency-provider-kraken sibling package
|
||||
|
||||
**Package**: `@suaveplan/currency-provider-kraken`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new package (genesis-original sibling of `@suaveplan/currency-providers`)
|
||||
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/currency-provider-kraken/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
The slim refactor archived at
|
||||
`openspec/archive/2026-05-17-slim-currency-providers/` moved every concrete
|
||||
provider out of `@suaveplan/currency-providers` and committed to landing each
|
||||
one as its own `@suaveplan/currency-provider-<name>` sibling. Kraken is one of
|
||||
the nine siblings the slim core's `proposal.md ## Affected dependents` block
|
||||
explicitly enumerates — this change ships it.
|
||||
|
||||
Kraken is a free public-ticker provider (no API key) with a regulated-exchange
|
||||
real-time trading price. Wiring it through the slim core's `httpRequest` +
|
||||
`defineCurrencyProvider` factory gives consumers a granular install (only pay
|
||||
for Kraken when they want Kraken) and lets the provider iterate on its own
|
||||
version cadence — exactly the foundation-with-sibling-plugins shape established
|
||||
by `@suaveplan/network-plugin-*` and `@suaveplan/drive-plugin-*`.
|
||||
|
||||
## What changes
|
||||
|
||||
### ADDED
|
||||
|
||||
- New package `@suaveplan/currency-provider-kraken` at
|
||||
`packages/finance/currency-provider-kraken/` (universal-tier, `0.1.0`,
|
||||
`sideEffects: false`).
|
||||
- `KrakenProvider` class implementing the `CurrencyProvider` contract from
|
||||
`@suaveplan/currency`. Constructor accepts an optional `NoKeyConfig` from
|
||||
`@suaveplan/currency-providers` (`baseUrl` override + injectable `FetchLike`).
|
||||
- `KRAKEN_SYMBOL_MAP` — the canonical mapping from Suaveplan symbol
|
||||
(`BTC`, `ETH`, `SOL`, `USDT`, `DOT`, `ADA`, `LINK`, `AVAX`, `ATOM`, `LTC`,
|
||||
`DOGE`, `XRP`) to Kraken pair (`XXBTZUSD`, `XETHZUSD`, …) — exported so
|
||||
callers can probe / extend.
|
||||
- `DEFAULT_KRAKEN_BASE_URL = "https://api.kraken.com/0/public"` exported
|
||||
constant.
|
||||
- Uses the shared `httpRequest()` helper from
|
||||
`@suaveplan/currency-providers/http/http` for the GET — non-2xx responses
|
||||
raise `CurrencyProviderError(HTTP_ERROR)` with `providerName: "kraken"` and
|
||||
the HTTP status populated.
|
||||
- Maps the Kraken-specific `data.error: string[]` array to
|
||||
`CurrencyProviderError(UPSTREAM_ERROR)` with the first message carried on
|
||||
`upstreamMessage`.
|
||||
- Raises `CurrencyProviderError(PARSE_ERROR)` when the response body fails to
|
||||
JSON-parse, when the `result` map is missing / not an object, or when a
|
||||
ticker's `c[0]` price cannot be parsed to a positive finite number.
|
||||
- Raises `CurrencyProviderError(UNKNOWN_CURRENCY)` when every requested
|
||||
`options.targets` symbol resolves to an unknown / unmapped Kraken pair, so
|
||||
callers don't silently receive an empty rate set.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Upstream reference for parity (the legacy 9-provider bundle):
|
||||
|
||||
| Capability | Upstream source |
|
||||
|---|---|
|
||||
| `KrakenProvider` class | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L1-L91 |
|
||||
| Kraken symbol → pair table | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L11-L24 |
|
||||
| `c[0]` last-trade-closed price + reciprocal-to-USD rate calc | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L67-L77 |
|
||||
| Kraken `error: string[]` upstream-failure detection | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L58-L60 |
|
||||
| HTTP-error path | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L46-L51 (now delegated to `httpRequest`) |
|
||||
|
||||
Genesis-native deltas vs. upstream:
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| Plain `Error` HTTP failure | ✅ | ⬆️ | Now `CurrencyProviderError(HTTP_ERROR)` via shared `httpRequest`. |
|
||||
| Plain `Error` Kraken-error array | ✅ | ⬆️ | Now `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage`. |
|
||||
| Hardcoded `globalThis.fetch` | ✅ | ⬆️ | Injectable `FetchLike` via `NoKeyConfig.fetch`. |
|
||||
| Silent empty rate set on unknown target | ✅ | ⬆️ | Now `CurrencyProviderError(UNKNOWN_CURRENCY)` when every requested target is unmapped. |
|
||||
| `BTC`/`ETH`/`SOL`/`USDT`/`DOT`/`ADA`/`LINK`/`AVAX`/`ATOM`/`LTC`/`DOGE`/`XRP` symbol map | ✅ | ✅ | Preserved verbatim. |
|
||||
| Reciprocal-of-USD-price rate model | ✅ | ✅ | Preserved verbatim. |
|
||||
|
||||
No upstream enterprise audit exists for `currency-providers` (the slim core's
|
||||
design notes were the genesis-native source).
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/currency-provider-kraken` to
|
||||
enumerate. The package is brand-new so there are no current dependents:
|
||||
|
||||
- No current dependents (verified by
|
||||
`bunx turbo ls --filter=...@suaveplan/currency-provider-kraken`).
|
||||
|
||||
This package consumes `@suaveplan/currency-providers` (the slim core) for the
|
||||
`httpRequest` helper, the `CurrencyProviderError` + `CURRENCY_PROVIDER_CODES`
|
||||
registry, and the `NoKeyConfig` shape. It consumes `@suaveplan/currency` for
|
||||
the `CurrencyProvider` / `RateFetchOptions` / `RateFetchResult` contracts and
|
||||
`@suaveplan/types` for the `FetchLike` shape. None of those packages need
|
||||
re-testing — they are all unchanged.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Authenticated Kraken endpoints (private balance / order endpoints) — this
|
||||
provider wraps only the public ticker, matching the slim core's "free, no
|
||||
API key" classification.
|
||||
- WebSocket / streaming ticker — same deferral as the slim core's
|
||||
`## Out of scope`.
|
||||
- Historical-rate / OHLC endpoints — same deferral.
|
||||
- A `KrakenProvider` built via `defineCurrencyProvider` (object-literal
|
||||
idiom) — the class form is the canonical surface; the factory remains
|
||||
available for callers who prefer it.
|
||||
@@ -0,0 +1,185 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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"`
|
||||
@@ -0,0 +1,79 @@
|
||||
# Tasks — Add @suaveplan/currency-provider-kraken
|
||||
|
||||
## Spec-completeness preflight (AGENTS.md §12a)
|
||||
|
||||
- [x] Numbered checklist of every `### Requirement` and `#### Scenario`
|
||||
drafted in `specs/currency-provider-kraken/spec.md` (8 requirements;
|
||||
see the spec delta).
|
||||
|
||||
## Scaffold
|
||||
|
||||
- [x] Create `packages/finance/currency-provider-kraken/` with
|
||||
`package.json` (`0.1.0`, `sideEffects: false`, `type: module`)
|
||||
- [x] Add `tsconfig.json` + `tsconfig.lib.json` mirroring the slim core
|
||||
- [x] Add `vite.config.ts` with `preserveModules: true` and the four
|
||||
external packages
|
||||
- [x] Add `vitest.config.ts` (forks pool, istanbul provider, 100/100/100/100
|
||||
thresholds, includes `tests/**/*.test.ts`)
|
||||
- [x] Add `tests/setup/polyfills.ts` (empty shim)
|
||||
- [x] Add `README.md` (per CLAUDE.md §5 — H1, paragraph, install, quick
|
||||
start, modules)
|
||||
|
||||
## Implement
|
||||
|
||||
- [x] `src/symbols/symbols.ts` — `KRAKEN_SYMBOL_MAP` (12 frozen entries) +
|
||||
`KRAKEN_PAIR_TO_SYMBOL` reverse map + `mergeSymbolMap` helper
|
||||
- [x] `src/symbols/symbols.md` — sibling doc (≥200 words; Purpose / Features
|
||||
/ Basic / Advanced / API Reference / Implementation Notes)
|
||||
- [x] `src/symbols/symbols.test.ts` — frozen, complete, reverse consistent,
|
||||
merge override behaviour
|
||||
- [x] `src/symbols/index.ts` — explicit named barrel
|
||||
- [x] `src/provider/provider.ts` — `KrakenProvider` class +
|
||||
`KrakenProviderConfig` extending `NoKeyConfig` with optional
|
||||
`extraSymbols` map; `DEFAULT_KRAKEN_BASE_URL` constant
|
||||
- [x] `src/provider/provider.md` — sibling doc
|
||||
- [x] `src/provider/provider.test.ts` — full scenario coverage matching
|
||||
the `## Test strategy` in `design.md`
|
||||
- [x] `src/provider/index.ts` — explicit named barrel
|
||||
- [x] `src/index.ts` — explicit named barrel re-exporting both feature
|
||||
modules
|
||||
|
||||
## Dep-graph + invariants test
|
||||
|
||||
- [x] `tests/dep-graph.test.ts` — no `export *` in `src/index.ts` or any
|
||||
feature barrel; subpath-export count matches feature-module count
|
||||
|
||||
## Pre-archive gates (AGENTS.md §13)
|
||||
|
||||
- [x] Step 0 — spec reconciliation (every `### Requirement` +
|
||||
`**Verified by:**` resolves)
|
||||
- [x] Step 1 — `bunx turbo lint typecheck test --filter=@suaveplan/currency-provider-kraken`
|
||||
+ `bun run check` both green
|
||||
- [x] Step 2 — `bunx turbo test:coverage --filter=@suaveplan/currency-provider-kraken`
|
||||
reports 100/100/100/100
|
||||
- [x] Step 3 — `bunx turbo build --filter=@suaveplan/currency-provider-kraken`
|
||||
green
|
||||
- [x] Step 4 — banned-pattern grep returns zero
|
||||
- [x] Step 4a — barrel-shape grep returns zero
|
||||
- [x] Step 4b — test-runner-import grep returns zero
|
||||
- [x] Step 5 — missing-`.md` grep returns zero
|
||||
- [x] Step 6 — README present
|
||||
- [x] Step 7 — bare-version grep returns zero
|
||||
- [x] Step 8 — subpath-export count = 2 (matches feature module count)
|
||||
- [x] Step 9 — security-scan grep returns zero
|
||||
- [x] Step 10 — dependent sweep (no current dependents — fast-path)
|
||||
|
||||
## Root-doc refresh (AGENTS.md §13b)
|
||||
|
||||
- [x] `bun scripts/folder-reorg/generate-manifest.ts`
|
||||
- [x] `bun scripts/generate-missing.ts`
|
||||
- [x] Update `STATUS.md` row for `@suaveplan/currency-provider-kraken`
|
||||
- [x] Update `SUMMARY.md` row for `@suaveplan/currency-provider-kraken`
|
||||
- [x] `bunx markdownlint-cli2 --fix`
|
||||
|
||||
## Archive (AGENTS.md §14)
|
||||
|
||||
- [x] Fold `specs/currency-provider-kraken/spec.md` into
|
||||
`openspec/specs/currency-provider-kraken/spec.md`
|
||||
- [x] Move `openspec/changes/add-currency-provider-kraken/` →
|
||||
`openspec/archive/2026-05-17-add-currency-provider-kraken/`
|
||||
Reference in New Issue
Block a user