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.
|
||||
Reference in New Issue
Block a user