Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

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