Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# @suaveplan/currency-provider-kraken - Universal Kraken public-ticker rate provider for @suaveplan/currency. Wraps Kraken's free no-API-key /0/public/Ticker endpoint into a CurrencyProvider with the slim core's shared httpRequest helper, typed CurrencyProviderError (HTTP_ERROR / PARSE_ERROR / UPSTREAM_ERROR / UNKNOWN_CURRENCY), and the 12-symbol BTC/ETH/SOL/USDT/DOT/ADA/LINK/AVAX/ATOM/LTC/DOGE/XRP coverage matched against Kraken's non-standard pair names.
Universal Kraken public-ticker rate provider for `@suaveplan/currency`.
Wraps Kraken's free, no-API-key `/0/public/Ticker` endpoint into a
`CurrencyProvider` so a `CurrencyConverter` can price the major crypto
pairs against USD without callers ever seeing Kraken's non-standard pair
names (`XXBTZUSD`, `XETHZUSD`, `XDGUSD`, …).
Concrete sibling of the slim `@suaveplan/currency-providers` core — uses
the shared `httpRequest()` helper, raises the shared `CurrencyProviderError`
codes (`HTTP_ERROR`, `PARSE_ERROR`, `UPSTREAM_ERROR`, `UNKNOWN_CURRENCY`),
and accepts the shared `NoKeyConfig` (optional `baseUrl`, optional injectable
`FetchLike`) plus an optional `extraSymbols` map for caller-supplied
extensions.
<!-- begin:auto-docs -->
## Documentation tree
- [provider](./src/provider/provider.md) — `KrakenProvider` is the universal `CurrencyProvider` implementation for Kraken's free `/0/public/Ticker` endpoint.…
- [provider](./src/provider/provider.md) — `KrakenProvider` is the universal `CurrencyProvider` implementation for Kraken's free `/0/public/Ticker` endpoint.…
- [symbols](./src/symbols/symbols.md) — The Kraken public ticker addresses pairs by non-standard names — `XXBTZUSD` for BTC/USD, `XETHZUSD` for ETH/USD, `XDGUS…
- [symbols](./src/symbols/symbols.md) — The Kraken public ticker addresses pairs by non-standard names — `XXBTZUSD` for BTC/USD, `XETHZUSD` for ETH/USD, `XDGUS…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/currency-provider-kraken @suaveplan/currency @suaveplan/currency-providers
```
## Quick start
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { KrakenProvider } from "@suaveplan/currency-provider-kraken";
const provider = new KrakenProvider();
const converter = new CurrencyConverter({ provider });
const btcPerUsd = await converter.getRate("USD", "BTC"); // e.g. 0.0000149
const ethPerUsd = await converter.getRate("USD", "ETH"); // e.g. 0.0002857
```
Or register through the slim core's registry:
```ts
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
import { KrakenProvider } from "@suaveplan/currency-provider-kraken";
const registry = new CurrencyProviderRegistry();
registry.register(new KrakenProvider());
const provider = registry.get("kraken");
```
## Features
- No API key — Kraken's public endpoint is free (per-IP rate-limited)
- 12 high-volume symbols out of the box: BTC, ETH, SOL, USDT, DOT, ADA,
LINK, AVAX, ATOM, LTC, DOGE, XRP
- Optional `extraSymbols` for newly-listed pairs without forking
- Reciprocal-of-USD rate model — `rates[X] = 1 / lastTradeClosedPriceUsd`
- Single-round-trip batch fetch (Kraken supports a comma-separated `pair=`
query)
- Uses the slim core's `httpRequest()` so HTTP errors, timeouts, and caller
`AbortSignal` propagation are uniform across providers
- Maps wire-level failure modes to typed `CurrencyProviderError`:
`HTTP_ERROR`, `UPSTREAM_ERROR`, `PARSE_ERROR`, `UNKNOWN_CURRENCY`
- Universal — no `node:`/`bun:`/DOM imports; runs unchanged under
Node ≥ 18, Bun, modern browsers (subject to Kraken's CORS policy), Web
Workers
## Modules
- [`src/provider/provider.md`](./src/provider/provider.md) —
`KrakenProvider` class, `KrakenProviderConfig`, `DEFAULT_KRAKEN_BASE_URL`
- [`src/symbols/symbols.md`](./src/symbols/symbols.md) —
`KRAKEN_SYMBOL_MAP`, `KRAKEN_PAIR_TO_SYMBOL`, `mergeSymbolMap`
## Status
`0.1.0` (pre-graduation). native sibling of the slim
`@suaveplan/currency-providers` core landed by
the design document. See
the design proposal for
the migration parity table and source mapping.

View File

@@ -0,0 +1,74 @@
{
"name": "@suaveplan/currency-provider-kraken",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Universal Kraken public-ticker rate provider for @suaveplan/currency. Wraps Kraken's free no-API-key /0/public/Ticker endpoint into a CurrencyProvider with the slim core's shared httpRequest helper, typed CurrencyProviderError (HTTP_ERROR / PARSE_ERROR / UPSTREAM_ERROR / UNKNOWN_CURRENCY), and the 12-symbol BTC/ETH/SOL/USDT/DOT/ADA/LINK/AVAX/ATOM/LTC/DOGE/XRP coverage matched against Kraken's non-standard pair names.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./symbols/symbols": {
"import": {
"types": "./dist/symbols/symbols.d.ts",
"default": "./dist/symbols/symbols.js"
}
},
"./provider/provider": {
"import": {
"types": "./dist/provider/provider.d.ts",
"default": "./dist/provider/provider.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/currency": "workspace:^",
"@suaveplan/currency-providers": "workspace:^",
"@suaveplan/error": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
]
}

View File

@@ -0,0 +1,11 @@
export {
DEFAULT_KRAKEN_BASE_URL,
KrakenProvider,
type KrakenProviderConfig,
} from "./provider/provider.js";
export {
KRAKEN_PAIR_TO_SYMBOL,
KRAKEN_SYMBOL_MAP,
type MergedSymbolMaps,
mergeSymbolMap,
} from "./symbols/symbols.js";

View File

@@ -0,0 +1,5 @@
export type { KrakenProviderConfig } from "./provider.js";
export {
DEFAULT_KRAKEN_BASE_URL,
KrakenProvider,
} from "./provider.js";

View File

@@ -0,0 +1,130 @@
# `provider` — KrakenProvider class
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`KrakenProvider` is the universal `CurrencyProvider` implementation
for Kraken's free `/0/public/Ticker` endpoint. It fetches real-time
last-trade-closed prices for a fixed set of high-volume crypto pairs and
returns them as reciprocal-of-USD rates, so a downstream `CurrencyConverter`
can translate `USD → BTC` (and friends) without callers ever seeing
Kraken's non-standard pair names.
The class is the canonical sibling-package surface defined by the slim
`@suaveplan/currency-providers` core: it wraps a single upstream endpoint,
maps every documented failure mode to a typed `CurrencyProviderError`, and
exposes nothing that consumers can't already obtain from the shared core
helpers.
## Features
- No API key required — Kraken's public endpoint is free and rate-limited
per IP
- 12 high-volume symbols out of the box (BTC, ETH, SOL, USDT, DOT, ADA,
LINK, AVAX, ATOM, LTC, DOGE, XRP); extensible via constructor
`extraSymbols`
- Reciprocal-of-USD rate model — `1 USD = (1 / krakenLastTradePrice)` of the
target crypto, matching the shape every `CurrencyConverter` consumer expects
- Uses the slim core's `httpRequest()` helper so HTTP failures, timeouts,
and caller abort-signal propagation are handled uniformly
- Maps the four wire-level failure modes to typed `CurrencyProviderError`:
`HTTP_ERROR` (non-2xx), `UPSTREAM_ERROR` (Kraken `error[]` array),
`PARSE_ERROR` (malformed JSON / shape mismatch), `UNKNOWN_CURRENCY` (all
requested targets unmapped)
- Universal — no `node:`/`bun:`/DOM imports; runs unchanged under
Node ≥ 18, Bun, modern browsers (subject to Kraken's CORS), Web Workers
- Optional injectable `FetchLike` (defaults to `globalThis.fetch`) for
recording tests, proxying, or staging URLs
## Usage: Basic
```ts
import { KrakenProvider } from "@suaveplan/currency-provider-kraken/provider/provider";
const provider = new KrakenProvider();
const result = await provider.fetchRates({ base: "USD" });
console.log(result.rates.BTC); // e.g. 0.0000149 (≈ 1 / 67000)
console.log(result.rates.ETH); // e.g. 0.0002857 (≈ 1 / 3500)
```
## Usage: Advanced
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
import { KrakenProvider } from "@suaveplan/currency-provider-kraken/provider/provider";
// Use the slim core's registry — wire Kraken plus any other sibling
// providers your app supports. The Kraken instance extends coverage with
// a newly-listed pair via `extraSymbols`, and reuses a custom fetch
// closure that adds a proxy header.
const registry = new CurrencyProviderRegistry();
registry.register(
new KrakenProvider({
baseUrl: "https://kraken.proxy.example.com/0/public",
fetch: async (url, init) =>
fetch(url, {
...init,
headers: {
...(init?.headers ?? {}),
"x-proxy-token": process.env.PROXY_TOKEN ?? "",
},
}),
extraSymbols: { MATIC: "MATICUSD" },
}),
);
const converter = new CurrencyConverter({
provider: registry.get("kraken"),
});
const btcRate = await converter.getRate("USD", "BTC");
const maticRate = await converter.getRate("USD", "MATIC");
```
## API Reference
| Export | Kind | Description |
|---|---|---|
| `DEFAULT_KRAKEN_BASE_URL` | `string` | `"https://api.kraken.com/0/public"` — overridable via `config.baseUrl`. |
| `KrakenProviderConfig` | `interface` extends `NoKeyConfig` | Optional `baseUrl`, optional `fetch` (`FetchLike`), optional `extraSymbols` (`Record<string, string>`). |
| `KrakenProvider` | `class implements CurrencyProvider` | Fixed `name = "kraken"`, `supportsCrypto = true`, `supportsFiat = false`. Constructor takes an optional `KrakenProviderConfig`. |
| `KrakenProvider.fetchRates(options?)` | `(RateFetchOptions?) => Promise<RateFetchResult>` | Issues one `GET /Ticker?pair=<csv>` via the injected fetch. Returns `{ base, rates, timestamp }`. Throws `CurrencyProviderError` on every documented failure path. |
### Error codes raised
| Code | When |
|---|---|
| `CURRENCY_PROVIDER.HTTP_ERROR` | Non-2xx response (raised by the shared `httpRequest`). Carries `status`. |
| `CURRENCY_PROVIDER.UPSTREAM_ERROR` | Kraken response carries a non-empty `error[]` array. Carries `upstreamMessage`. |
| `CURRENCY_PROVIDER.PARSE_ERROR` | Body fails JSON-parse, OR the parsed body does not match the `{ error: string[], result: object }` shape. Carries `cause` for the JSON-parse case. |
| `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` | Caller supplied `options.targets` but none of the requested symbols resolved to a mapped Kraken pair. |
## Implementation Notes
- The pair list sent in the `pair=` query is the value list of the merged
symbol map — Kraken supports a comma-separated batch so the whole supported
set is fetched in a single round trip. This is more efficient than one
request per symbol AND matches what Kraken's documentation recommends.
- The rate model is `1 / Number.parseFloat(ticker.c[0])` where `c[0]` is
Kraken's "last trade closed" price. Entries whose price does not parse
to a positive finite number are skipped (not raised) — Kraken occasionally
returns `"0.00"` for freshly-listed pairs. If the entire mapped set drops,
`options.targets` filtering will subsequently raise `UNKNOWN_CURRENCY`
on the caller's behalf.
- `options.signal` is forwarded into `httpRequest()` so the caller's
`AbortController` composes with any future timeout the helper applies.
- The provider does **not** retry, cache, or rate-limit. Those concerns
belong in wrapper providers (the slim core's `## Out of scope` defers
them) — keeping this class focused on Kraken's wire protocol keeps the
failure surface auditable.
- All HTTP error mapping flows through the shared `httpRequest()` helper
exported from `@suaveplan/currency-providers/http/http`, so the response
is never read on a non-2xx path — Kraken's body on a 5xx is typically
HTML and would otherwise raise a confusing `PARSE_ERROR`.
- Tier purity: no `node:` imports, no DOM imports, no `setTimeout`. The
helper uses `AbortSignal.timeout` for timeouts (universal in Node 17.3+,
Bun, modern browsers).

View File

@@ -0,0 +1,420 @@
import {
CURRENCY_PROVIDER_CODES,
CurrencyProviderError,
} from "@suaveplan/currency-providers/error-codes/error-codes";
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { FetchLike } from "@suaveplan/types";
import { DEFAULT_KRAKEN_BASE_URL, KrakenProvider } from "./provider.js";
interface FetchCall {
url: string;
init: RequestInit | undefined;
}
interface Recorder {
fetch: FetchLike;
calls: FetchCall[];
}
function recordingFetch(
handler: (call: FetchCall) => Promise<Response> | Response
): Recorder {
const calls: FetchCall[] = [];
const fetchImpl: FetchLike = async (
url: string | URL | Request,
init?: RequestInit
): Promise<Response> => {
const call: FetchCall = { url: String(url), init };
calls.push(call);
return await handler(call);
};
return { fetch: fetchImpl, calls };
}
function ok(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}
const SAMPLE_BODY = {
error: [],
result: {
XXBTZUSD: { c: ["67000.00", "0.1"] },
XETHZUSD: { c: ["3500.00", "1.0"] },
SOLUSD: { c: ["150.00", "5.0"] },
},
};
describe("KrakenProvider — identity & defaults", () => {
it("exposes the canonical kraken identity", { timeout: 5000 }, () => {
const provider = new KrakenProvider();
expect(provider.name).toBe("kraken");
expect(provider.supportsCrypto).toBe(true);
expect(provider.supportsFiat).toBe(false);
});
it("defaults to globalThis.fetch and DEFAULT_KRAKEN_BASE_URL", {
timeout: 5000,
}, async () => {
// Replace `globalThis.fetch` with a recording closure scoped to this test.
const original = globalThis.fetch;
const calls: string[] = [];
globalThis.fetch = (async (
url: string | URL | Request,
_init?: RequestInit
) => {
calls.push(String(url));
return ok(SAMPLE_BODY);
}) as typeof fetch;
try {
const provider = new KrakenProvider();
const result = await provider.fetchRates();
expect(result.base).toBe("USD");
expect(calls.length).toBe(1);
expect(calls[0]?.startsWith(DEFAULT_KRAKEN_BASE_URL)).toBe(true);
} finally {
globalThis.fetch = original;
}
});
it("honours custom baseUrl and fetch", { timeout: 5000 }, async () => {
const { fetch, calls } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({
fetch,
baseUrl: "https://custom.kraken.example",
});
await provider.fetchRates();
expect(calls.length).toBe(1);
expect(calls[0]?.url.startsWith("https://custom.kraken.example")).toBe(
true
);
expect(calls[0]?.url).toContain("/Ticker?pair=");
});
it("defaults options.base to USD", { timeout: 5000 }, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates();
expect(result.base).toBe("USD");
});
it("forwards options.base verbatim", { timeout: 5000 }, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates({ base: "USDT" });
expect(result.base).toBe("USDT");
});
});
describe("KrakenProvider — rate mapping", () => {
it("maps positive prices to reciprocal-of-USD rates", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates();
expect(result.rates.BTC).toBeCloseTo(1 / 67000, 10);
expect(result.rates.ETH).toBeCloseTo(1 / 3500, 10);
expect(result.rates.SOL).toBeCloseTo(1 / 150, 10);
expect(typeof result.timestamp).toBe("number");
});
it("skips zero or non-finite prices without raising", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() =>
ok({
error: [],
result: {
XXBTZUSD: { c: ["0.00", "0"] },
XETHZUSD: { c: ["3500.00", "1.0"] },
SOLUSD: { c: ["not-a-number", "5.0"] },
},
})
);
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates();
expect(result.rates.ETH).toBeCloseTo(1 / 3500, 10);
expect(result.rates.BTC).toBeUndefined();
expect(result.rates.SOL).toBeUndefined();
});
it("skips ticker entries missing c[0]", { timeout: 5000 }, async () => {
const { fetch } = recordingFetch(() =>
ok({
error: [],
result: {
XXBTZUSD: { c: [] as unknown as [string, string] },
XETHZUSD: { c: ["3500.00", "1.0"] },
},
})
);
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates();
expect(result.rates.ETH).toBeCloseTo(1 / 3500, 10);
expect(result.rates.BTC).toBeUndefined();
});
it("skips unknown Kraken pairs", { timeout: 5000 }, async () => {
const { fetch } = recordingFetch(() =>
ok({
error: [],
result: {
XXBTZUSD: { c: ["67000.00", "0.1"] },
UNKNOWNPAIR: { c: ["1.00", "1.0"] },
},
})
);
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates();
expect(result.rates.BTC).toBeCloseTo(1 / 67000, 10);
expect(Object.keys(result.rates)).toEqual(["BTC"]);
});
});
describe("KrakenProvider — targets filtering", () => {
it("filters by options.targets", { timeout: 5000 }, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates({ targets: ["BTC"] });
expect(Object.keys(result.rates)).toEqual(["BTC"]);
expect(result.rates.BTC).toBeCloseTo(1 / 67000, 10);
});
it("returns mapped subset when some targets are unmapped", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates({
targets: ["BTC", "DOGECOIN-FAKE"],
});
expect(Object.keys(result.rates)).toEqual(["BTC"]);
});
it("raises UNKNOWN_CURRENCY when every requested target is unmapped", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
try {
await provider.fetchRates({ targets: ["FAKE1", "FAKE2"] });
expect.fail("fetchRates should have rejected");
} catch (error) {
expect(error).toBeInstanceOf(CurrencyProviderError);
const err = error as CurrencyProviderError;
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY);
expect(err.providerName).toBe("kraken");
}
});
it("returns an empty targets list as a no-op (no UNKNOWN_CURRENCY)", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok(SAMPLE_BODY));
const provider = new KrakenProvider({ fetch });
const result = await provider.fetchRates({ targets: [] });
expect(Object.keys(result.rates)).toEqual([]);
});
it("extends the symbol map via extraSymbols", {
timeout: 5000,
}, async () => {
const { fetch, calls } = recordingFetch(() =>
ok({
error: [],
result: {
MATICUSD: { c: ["0.50", "10.0"] },
},
})
);
const provider = new KrakenProvider({
fetch,
extraSymbols: { MATIC: "MATICUSD" },
});
const result = await provider.fetchRates({ targets: ["MATIC"] });
expect(result.rates.MATIC).toBeCloseTo(1 / 0.5, 10);
// MATICUSD made it into the pair= query.
expect(calls[0]?.url).toContain("MATICUSD");
});
});
describe("KrakenProvider — failure modes", () => {
it("raises HTTP_ERROR on non-2xx response", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(
() => new Response("", { status: 503, statusText: "Down" })
);
const provider = new KrakenProvider({ fetch });
try {
await provider.fetchRates();
expect.fail("fetchRates should have rejected");
} catch (error) {
expect(error).toBeInstanceOf(CurrencyProviderError);
const err = error as CurrencyProviderError;
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.HTTP_ERROR);
expect(err.status).toBe(503);
expect(err.providerName).toBe("kraken");
}
});
it("maps a non-empty error array to UPSTREAM_ERROR", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() =>
ok({
error: ["EGeneral:Too many requests"],
result: {},
})
);
const provider = new KrakenProvider({ fetch });
try {
await provider.fetchRates();
expect.fail("fetchRates should have rejected");
} catch (error) {
expect(error).toBeInstanceOf(CurrencyProviderError);
const err = error as CurrencyProviderError;
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR);
expect(err.upstreamMessage).toBe("EGeneral:Too many requests");
expect(err.message).toContain("Too many requests");
expect(err.providerName).toBe("kraken");
}
});
it("uses a placeholder when error array is non-empty but the first entry is missing", {
timeout: 5000,
}, async () => {
// Synthesise an array with an explicit `undefined`-hole to exercise
// the `?? "unknown upstream error"` fallback. JSON.stringify drops
// holes to `null`, then JSON.parse rehydrates them as `null`, which
// is `!== undefined` — so we use a fetch that returns the parsed
// object directly by wrapping the body in a Response whose .json()
// resolves to the array we want.
const fetchImpl: FetchLike = async () =>
new Response('{"error":[null],"result":{}}', {
status: 200,
headers: { "content-type": "application/json" },
});
const provider = new KrakenProvider({ fetch: fetchImpl });
try {
await provider.fetchRates();
expect.fail("fetchRates should have rejected");
} catch (error) {
expect(error).toBeInstanceOf(CurrencyProviderError);
const err = error as CurrencyProviderError;
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR);
// The first element was JSON `null`, so the fallback "unknown
// upstream error" string is used.
expect(err.upstreamMessage).toBe("unknown upstream error");
}
});
it("raises PARSE_ERROR on malformed JSON", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(
() =>
new Response("not-json", {
status: 200,
headers: { "content-type": "application/json" },
})
);
const provider = new KrakenProvider({ fetch });
try {
await provider.fetchRates();
expect.fail("fetchRates should have rejected");
} catch (error) {
expect(error).toBeInstanceOf(CurrencyProviderError);
const err = error as CurrencyProviderError;
expect(err.code).toBe(CURRENCY_PROVIDER_CODES.PARSE_ERROR);
expect(err.providerName).toBe("kraken");
expect(err.cause).toBeDefined();
}
});
it("raises PARSE_ERROR when the body is JSON null", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok(null));
const provider = new KrakenProvider({ fetch });
await expect(provider.fetchRates()).rejects.toMatchObject({
code: CURRENCY_PROVIDER_CODES.PARSE_ERROR,
});
});
it("raises PARSE_ERROR when error is not an array", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() =>
ok({ error: "boom", result: {} })
);
const provider = new KrakenProvider({ fetch });
await expect(provider.fetchRates()).rejects.toMatchObject({
code: CURRENCY_PROVIDER_CODES.PARSE_ERROR,
});
});
it("raises PARSE_ERROR when result is missing", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok({ error: [] }));
const provider = new KrakenProvider({ fetch });
await expect(provider.fetchRates()).rejects.toMatchObject({
code: CURRENCY_PROVIDER_CODES.PARSE_ERROR,
});
});
it("raises PARSE_ERROR when result is null", {
timeout: 5000,
}, async () => {
const { fetch } = recordingFetch(() => ok({ error: [], result: null }));
const provider = new KrakenProvider({ fetch });
await expect(provider.fetchRates()).rejects.toMatchObject({
code: CURRENCY_PROVIDER_CODES.PARSE_ERROR,
});
});
});
describe("KrakenProvider — abort propagation", () => {
it("forwards options.signal to the underlying fetch", {
timeout: 5000,
}, async () => {
const seenSignals: (AbortSignal | undefined)[] = [];
const fetchImpl: FetchLike = async (
_url: string | URL | Request,
init?: RequestInit
): Promise<Response> => {
seenSignals.push(init?.signal ?? undefined);
return ok(SAMPLE_BODY);
};
const controller = new AbortController();
const provider = new KrakenProvider({ fetch: fetchImpl });
await provider.fetchRates({ signal: controller.signal });
expect(seenSignals.length).toBe(1);
expect(seenSignals[0]).toBeDefined();
});
it("propagates a caller-aborted signal", { timeout: 5000 }, async () => {
const fetchImpl: FetchLike = async (
_url: string | URL | Request,
init?: RequestInit
): Promise<Response> => {
return new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => {
reject(
init?.signal?.reason ??
new DOMException("aborted", "AbortError")
);
});
});
};
const controller = new AbortController();
const provider = new KrakenProvider({ fetch: fetchImpl });
const promise = provider.fetchRates({ signal: controller.signal });
controller.abort(new Error("caller cancelled"));
await expect(promise).rejects.toThrow(/caller cancelled/);
});
});

View File

@@ -0,0 +1,181 @@
import type {
CurrencyProvider,
RateFetchOptions,
RateFetchResult,
} from "@suaveplan/currency";
import {
CURRENCY_PROVIDER_CODES,
CurrencyProviderError,
} from "@suaveplan/currency-providers/error-codes/error-codes";
import { httpRequest } from "@suaveplan/currency-providers/http/http";
import type { NoKeyConfig } from "@suaveplan/currency-providers/types/configs";
import type { FetchLike } from "@suaveplan/types";
import { mergeSymbolMap } from "../symbols/symbols.js";
/**
* Public Kraken ticker base URL. Override via
* {@link KrakenProviderConfig.baseUrl} when wiring to a staging mirror or
* recording fixture.
*/
export const DEFAULT_KRAKEN_BASE_URL = "https://api.kraken.com/0/public";
/** Stable provider identifier carried on every raised error. */
const PROVIDER_NAME = "kraken";
/**
* Configuration accepted by {@link KrakenProvider}. Extends the slim core's
* {@link NoKeyConfig} (optional `baseUrl`, optional injectable `FetchLike`)
* with an optional `extraSymbols` record so callers can extend symbol
* coverage at construction time without forking the package.
*/
export interface KrakenProviderConfig extends NoKeyConfig {
/**
* Additional Suaveplan-symbol → Kraken-pair entries merged on top of
* the canonical `KRAKEN_SYMBOL_MAP`. Useful for newly-listed pairs or
* private-label overrides.
*/
readonly extraSymbols?: Readonly<Record<string, string>>;
}
/** Shape of a single Kraken ticker entry — only `c` is used downstream. */
interface KrakenTicker {
readonly c: readonly [string, string];
}
/** Shape of the Kraken `/0/public/Ticker` JSON response. */
interface KrakenResponse {
readonly error: readonly string[];
readonly result: Readonly<Record<string, KrakenTicker>>;
}
/**
* Kraken public-ticker rate provider.
*
* Wraps `GET /0/public/Ticker?pair=<csv>` — Kraken's free, no-API-key
* real-time price endpoint — into the `CurrencyProvider` contract. Returns
* reciprocal-of-USD rates (`1 / lastTradeClosedPrice`) keyed by the
* Suaveplan-standard symbol. Maps Kraken's wire-level failure modes to the
* shared `CurrencyProviderError` codes:
*
* - non-2xx response → `HTTP_ERROR` (raised by the shared `httpRequest`)
* - non-empty `error[]` array → `UPSTREAM_ERROR` (with `upstreamMessage`)
* - unparsable / shape-violating body → `PARSE_ERROR`
* - all requested targets unmapped → `UNKNOWN_CURRENCY`
*/
export class KrakenProvider implements CurrencyProvider {
public readonly name = PROVIDER_NAME;
public readonly supportsCrypto = true;
public readonly supportsFiat = false;
private readonly baseUrl: string;
private readonly fetchImpl: FetchLike;
private readonly forwardMap: Readonly<Record<string, string>>;
private readonly reverseMap: Readonly<Record<string, string>>;
public constructor(config: KrakenProviderConfig = {}) {
this.baseUrl = config.baseUrl ?? DEFAULT_KRAKEN_BASE_URL;
this.fetchImpl = config.fetch ?? globalThis.fetch;
const merged = mergeSymbolMap(config.extraSymbols);
this.forwardMap = merged.forward;
this.reverseMap = merged.reverse;
}
public async fetchRates(
options?: RateFetchOptions
): Promise<RateFetchResult> {
const base = options?.base ?? "USD";
const pairs = Object.values(this.forwardMap).join(",");
const url = `${this.baseUrl}/Ticker?pair=${pairs}`;
const response = await httpRequest({
url,
fetch: this.fetchImpl,
providerName: PROVIDER_NAME,
...(options?.signal !== undefined
? { signal: options.signal }
: {}),
});
const data = await parseKrakenBody(response);
assertNoUpstreamError(data);
const rates = buildRates(data.result, this.reverseMap);
const filtered = filterByTargets(rates, options?.targets);
return { base, rates: filtered, timestamp: Date.now() };
}
}
async function parseKrakenBody(response: Response): Promise<KrakenResponse> {
let raw: unknown;
try {
raw = await response.json();
} catch (cause) {
throw new CurrencyProviderError(
CURRENCY_PROVIDER_CODES.PARSE_ERROR,
`${PROVIDER_NAME} response was not valid JSON`,
{ providerName: PROVIDER_NAME, cause }
);
}
if (
raw === null ||
typeof raw !== "object" ||
!Array.isArray((raw as { error?: unknown }).error) ||
typeof (raw as { result?: unknown }).result !== "object" ||
(raw as { result?: unknown }).result === null
) {
throw new CurrencyProviderError(
CURRENCY_PROVIDER_CODES.PARSE_ERROR,
`${PROVIDER_NAME} response did not match the expected { error, result } shape`,
{ providerName: PROVIDER_NAME }
);
}
return raw as KrakenResponse;
}
function assertNoUpstreamError(data: KrakenResponse): void {
if (data.error.length === 0) return;
const first = data.error[0] ?? "unknown upstream error";
throw new CurrencyProviderError(
CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR,
`${PROVIDER_NAME} upstream error: ${first}`,
{ providerName: PROVIDER_NAME, upstreamMessage: first }
);
}
function buildRates(
result: Readonly<Record<string, KrakenTicker>>,
reverseMap: Readonly<Record<string, string>>
): Record<string, number> {
const rates: Record<string, number> = {};
for (const [pair, ticker] of Object.entries(result)) {
const symbol = reverseMap[pair];
if (symbol === undefined) continue;
const raw = ticker.c?.[0];
if (typeof raw !== "string") continue;
const price = Number.parseFloat(raw);
if (!Number.isFinite(price) || price <= 0) continue;
rates[symbol] = 1 / price;
}
return rates;
}
function filterByTargets(
rates: Record<string, number>,
targets: readonly string[] | undefined
): Record<string, number> {
if (targets === undefined) return rates;
const filtered: Record<string, number> = {};
for (const t of targets) {
const value = rates[t];
if (value !== undefined) filtered[t] = value;
}
if (targets.length > 0 && Object.keys(filtered).length === 0) {
throw new CurrencyProviderError(
CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY,
`${PROVIDER_NAME} could not price any of the requested targets: ${targets.join(", ")}`,
{ providerName: PROVIDER_NAME }
);
}
return filtered;
}

View File

@@ -0,0 +1,6 @@
export type { MergedSymbolMaps } from "./symbols.js";
export {
KRAKEN_PAIR_TO_SYMBOL,
KRAKEN_SYMBOL_MAP,
mergeSymbolMap,
} from "./symbols.js";

View File

@@ -0,0 +1,90 @@
# `symbols` — Kraken symbol-mapping table
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The Kraken public ticker addresses pairs by non-standard names — `XXBTZUSD`
for BTC/USD, `XETHZUSD` for ETH/USD, `XDGUSD` for DOGE/USD — using the
asset-prefix convention (`X` for native crypto assets, `Z` for fiat) inherited
from its early-2014 instrument schema. Every consumer of the `KrakenProvider`
wants to address rates by the Suaveplan-standard symbol (`BTC`, `ETH`,
`DOGE`), so the provider needs a stable bidirectional lookup table that
normalises both directions in O(1).
This module owns that table. It exports the frozen forward map
(`KRAKEN_SYMBOL_MAP`), the derived frozen reverse map
(`KRAKEN_PAIR_TO_SYMBOL`), and a `mergeSymbolMap` helper for callers who
need to extend coverage without forking the package.
## Features
- 12 canonical Suaveplan symbols mapped to their Kraken pair names: BTC,
ETH, SOL, USDT, DOT, ADA, LINK, AVAX, ATOM, LTC, DOGE, XRP
- Forward + reverse maps both `Object.freeze`-d for identity stability and
cheap memoised lookups in the hot path
- `mergeSymbolMap(extras)` for caller-supplied extensions — returns a fresh
forward + reverse pair without mutating the canonical maps
- Zero runtime dependencies, universal
## Usage: Basic
```ts
import {
KRAKEN_PAIR_TO_SYMBOL,
KRAKEN_SYMBOL_MAP,
} from "@suaveplan/currency-provider-kraken/symbols/symbols";
console.log(KRAKEN_SYMBOL_MAP.BTC); // "XXBTZUSD"
console.log(KRAKEN_PAIR_TO_SYMBOL.XXBTZUSD); // "BTC"
```
## Usage: Advanced
```ts
import { mergeSymbolMap } from "@suaveplan/currency-provider-kraken/symbols/symbols";
// Extend with a pair Kraken added after this release shipped.
const { forward, reverse } = mergeSymbolMap({
MATIC: "MATICUSD",
APT: "APTUSD",
});
console.log(forward.MATIC); // "MATICUSD"
console.log(reverse.MATICUSD); // "MATIC"
// Original maps are unchanged — `mergeSymbolMap` never mutates them.
console.log("MATIC" in KRAKEN_SYMBOL_MAP); // false
```
## API Reference
| Export | Kind | Description |
|---|---|---|
| `KRAKEN_SYMBOL_MAP` | `Readonly<Record<string, string>>` | Frozen forward map: 12 Suaveplan symbols → Kraken pair name. |
| `KRAKEN_PAIR_TO_SYMBOL` | `Readonly<Record<string, string>>` | Frozen reverse map derived at module load from the forward map. |
| `MergedSymbolMaps` | `interface` | Shape returned by `mergeSymbolMap` — `{ forward, reverse }`, both `Readonly<Record<string, string>>`. |
| `mergeSymbolMap(extras?)` | `(Readonly<Record<string, string>> \| undefined) => MergedSymbolMaps` | Returns the canonical maps when `extras` is empty/undefined; otherwise returns fresh forward + reverse maps with the extras merged on top. |
## Implementation Notes
- The Kraken instrument schema mixes legacy `X<base>Z<quote>` pairs (`XXBTZUSD`)
with flat `<base>USD` pairs (`SOLUSD`, `DOTUSD`) added after the schema
redesign. The map captures both forms so callers don't have to.
- The forward map is intentionally small. Kraken lists hundreds of pairs;
the 12 included here are the high-volume subset that match the upstream
9-provider bundle's coverage. Callers needing more symbols use the
`extraSymbols` constructor field on `KrakenProvider` (which calls
`mergeSymbolMap` internally) or call `mergeSymbolMap` directly when they
want the maps for their own logic.
- `mergeSymbolMap` is a hot-path-aware fast path: when `extras` is empty or
undefined, it returns the canonical maps by reference without allocating.
- Both canonical maps are frozen, so `Object.assign(KRAKEN_SYMBOL_MAP, …)`
silently fails in non-strict mode and throws in strict mode — making
accidental mutation impossible.
- Determinism: iteration order of `Object.entries(KRAKEN_SYMBOL_MAP)` is
property-insertion order per the ES2015 spec, so derived structures
(including the reverse map and merged outputs) have deterministic ordering
for golden-file tests.

View File

@@ -0,0 +1,105 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
KRAKEN_PAIR_TO_SYMBOL,
KRAKEN_SYMBOL_MAP,
mergeSymbolMap,
} from "./symbols.js";
const REQUIRED_SYMBOLS = [
"BTC",
"ETH",
"SOL",
"USDT",
"DOT",
"ADA",
"LINK",
"AVAX",
"ATOM",
"LTC",
"DOGE",
"XRP",
] as const;
describe("KRAKEN_SYMBOL_MAP", () => {
it("exports a frozen 12-entry symbol map", { timeout: 5000 }, () => {
expect(Object.isFrozen(KRAKEN_SYMBOL_MAP)).toBe(true);
const keys = Object.keys(KRAKEN_SYMBOL_MAP);
expect(keys.length).toBe(REQUIRED_SYMBOLS.length);
for (const sym of REQUIRED_SYMBOLS) {
expect(KRAKEN_SYMBOL_MAP[sym]).toBeDefined();
expect(typeof KRAKEN_SYMBOL_MAP[sym]).toBe("string");
}
});
it("maps the well-known pairs to their canonical Kraken names", {
timeout: 5000,
}, () => {
expect(KRAKEN_SYMBOL_MAP.BTC).toBe("XXBTZUSD");
expect(KRAKEN_SYMBOL_MAP.ETH).toBe("XETHZUSD");
expect(KRAKEN_SYMBOL_MAP.DOGE).toBe("XDGUSD");
expect(KRAKEN_SYMBOL_MAP.SOL).toBe("SOLUSD");
});
it("reverse map is consistent with the forward map", {
timeout: 5000,
}, () => {
expect(Object.isFrozen(KRAKEN_PAIR_TO_SYMBOL)).toBe(true);
for (const [symbol, pair] of Object.entries(KRAKEN_SYMBOL_MAP)) {
expect(KRAKEN_PAIR_TO_SYMBOL[pair]).toBe(symbol);
}
// And every reverse-map entry exists in the forward map.
for (const [pair, symbol] of Object.entries(KRAKEN_PAIR_TO_SYMBOL)) {
expect(KRAKEN_SYMBOL_MAP[symbol]).toBe(pair);
}
});
});
describe("mergeSymbolMap", () => {
it("returns canonical maps by reference when no extras are supplied", {
timeout: 5000,
}, () => {
const { forward, reverse } = mergeSymbolMap();
expect(forward).toBe(KRAKEN_SYMBOL_MAP);
expect(reverse).toBe(KRAKEN_PAIR_TO_SYMBOL);
});
it("returns canonical maps by reference when extras is an empty object", {
timeout: 5000,
}, () => {
const { forward, reverse } = mergeSymbolMap({});
expect(forward).toBe(KRAKEN_SYMBOL_MAP);
expect(reverse).toBe(KRAKEN_PAIR_TO_SYMBOL);
});
it("merges extras into a fresh forward map without mutating the canonical map", {
timeout: 5000,
}, () => {
const { forward, reverse } = mergeSymbolMap({
MATIC: "MATICUSD",
APT: "APTUSD",
});
expect(forward).not.toBe(KRAKEN_SYMBOL_MAP);
expect(forward.MATIC).toBe("MATICUSD");
expect(forward.APT).toBe("APTUSD");
expect(forward.BTC).toBe("XXBTZUSD");
expect(reverse.MATICUSD).toBe("MATIC");
expect(reverse.APTUSD).toBe("APT");
expect(reverse.XXBTZUSD).toBe("BTC");
// Canonical maps untouched.
expect("MATIC" in KRAKEN_SYMBOL_MAP).toBe(false);
expect("MATICUSD" in KRAKEN_PAIR_TO_SYMBOL).toBe(false);
});
it("lets extras override a canonical mapping", { timeout: 5000 }, () => {
const { forward, reverse } = mergeSymbolMap({ BTC: "XBTUSDT" });
expect(forward.BTC).toBe("XBTUSDT");
expect(reverse.XBTUSDT).toBe("BTC");
// The legacy XXBTZUSD pair no longer reverse-maps to BTC because BTC
// has been redirected — but the legacy entry still points back to
// BTC since both forward entries had value BTC ... actually,
// because BTC is overridden, the reverse map only has XBTUSDT for BTC.
expect(reverse.XXBTZUSD).toBeUndefined();
});
});

View File

@@ -0,0 +1,71 @@
/**
* Canonical mapping from Suaveplan-standard cryptocurrency symbol to
* Kraken's non-standard ticker pair name. Kraken uses asset-prefixed names
* (`XXBT` for BTC, `XETH` for ETH, `XDG` for DOGE) plus a `Z` separator
* for the quote (`ZUSD` for USD) on legacy pairs, and a flat `<base>USD`
* for newer listings. This table normalises both forms so callers always
* see the standard symbol.
*
* The map is `Object.freeze`-d so callers can rely on identity stability
* for memoised lookups.
*/
export 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",
});
/**
* Reverse of {@link KRAKEN_SYMBOL_MAP} — Kraken pair name → Suaveplan
* symbol. Used when iterating a Kraken `result` payload to recover the
* standard symbol in O(1). Derived at module load from
* `KRAKEN_SYMBOL_MAP`; frozen so identity remains stable.
*/
export const KRAKEN_PAIR_TO_SYMBOL: Readonly<Record<string, string>> =
Object.freeze(
Object.fromEntries(
Object.entries(KRAKEN_SYMBOL_MAP).map(([symbol, pair]) => [
pair,
symbol,
])
)
);
/**
* Merge a caller-supplied `extraSymbols` record on top of
* {@link KRAKEN_SYMBOL_MAP}, producing a new forward map + matching
* reverse map. Used by `KrakenProvider` when the caller wires the
* constructor's optional `extraSymbols` field.
*
* The returned maps are NOT frozen — the provider keeps them private and
* never exposes them to callers, so freezing would just cost cycles. If
* a future caller needs to expose them, freeze at the call site.
*/
export interface MergedSymbolMaps {
readonly forward: Readonly<Record<string, string>>;
readonly reverse: Readonly<Record<string, string>>;
}
export function mergeSymbolMap(
extra?: Readonly<Record<string, string>>
): MergedSymbolMaps {
if (extra === undefined || Object.keys(extra).length === 0) {
return { forward: KRAKEN_SYMBOL_MAP, reverse: KRAKEN_PAIR_TO_SYMBOL };
}
const forward: Record<string, string> = { ...KRAKEN_SYMBOL_MAP, ...extra };
const reverse: Record<string, string> = {};
for (const [symbol, pair] of Object.entries(forward)) {
reverse[pair] = symbol;
}
return { forward, reverse };
}

View File

@@ -0,0 +1,62 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const PKG_ROOT = join(__dirname, "..");
function findFiles(
dir: string,
predicate: (path: string) => boolean
): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) {
if (entry === "node_modules" || entry === "dist") continue;
out.push(...findFiles(full, predicate));
} else if (predicate(full)) {
out.push(full);
}
}
return out;
}
describe("dep-graph invariants", () => {
it("src/index.ts has no export * statements", { timeout: 5000 }, () => {
const src = readFileSync(join(PKG_ROOT, "src", "index.ts"), "utf8");
expect(/^\s*export\s+(type\s+)?\*\s+from/m.test(src)).toBe(false);
});
it("feature barrels have no export * statements", { timeout: 5000 }, () => {
const barrels = findFiles(join(PKG_ROOT, "src"), (p) =>
p.endsWith(`${"/"}index.ts`)
);
const offenders: string[] = [];
for (const path of barrels) {
if (path === join(PKG_ROOT, "src", "index.ts")) continue;
const text = readFileSync(path, "utf8");
if (/^\s*export\s+(type\s+)?\*\s+from/m.test(text)) {
offenders.push(path);
}
}
expect(offenders).toEqual([]);
});
it("package.json declares one subpath export per feature module", {
timeout: 5000,
}, () => {
const pkg = JSON.parse(
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
) as { exports: Record<string, unknown> };
const subpaths = Object.keys(pkg.exports).filter(
(k) => k.startsWith("./") && k !== "./package.json"
);
const required = ["./symbols/symbols", "./provider/provider"];
for (const r of required) {
expect(subpaths).toContain(r);
}
// Root `.` + exactly two feature subpaths.
expect(subpaths.length).toBe(required.length);
});
});

View File

@@ -0,0 +1,7 @@
// Polyfills for test environment — none required. This package is
// universal and uses only `globalThis.fetch` (overridable via FetchLike
// injection), `JSON.parse`, `Number.parseFloat`, `Number.isFinite`, `Map`,
// and `Object.freeze` — all available in every supported runtime (Node 18+,
// Bun, modern browsers, Web Workers). This file exists to give vitest a
// stable setup file path mirroring the sibling-package layout.
export { };

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,39 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/currency-provider-kraken",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/currency",
"@suaveplan/currency-providers",
"@suaveplan/currency-providers/error-codes/error-codes",
"@suaveplan/currency-providers/http/http",
"@suaveplan/currency-providers/types/configs",
"@suaveplan/error",
"@suaveplan/types",
"tslib",
],
},
},
});

View File

@@ -0,0 +1,27 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
setupFiles: ["./tests/setup/polyfills.ts"],
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/index.ts",
"src/**/*.bench.ts",
"src/__benchmarks__/**",
"tests/**",
],
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 },
},
testTimeout: 30000,
},
});