113 lines
4.9 KiB
Markdown
113 lines
4.9 KiB
Markdown
# Design — @suaveplan/currency-providers
|
|
|
|
Non-normative design notes. The spec delta in
|
|
`./specs/currency-providers/spec.md` is the contract; this file explains
|
|
*why* the contract is shaped this way.
|
|
|
|
## Folder layout
|
|
|
|
```text
|
|
packages/finance/currency-providers/
|
|
src/
|
|
types/
|
|
configs.ts # ApiKeyConfig / NoKeyConfig / HttpFetchOptions
|
|
configs.md
|
|
error-codes/
|
|
error-codes.ts # CURRENCY_PROVIDER_CODES + CurrencyProviderError
|
|
error-codes.test.ts
|
|
error-codes.md
|
|
providers/
|
|
ecb/{ecb.ts, ecb.test.ts, ecb.md, index.ts}
|
|
fixer/{fixer.ts, fixer.test.ts, fixer.md, index.ts}
|
|
open-exchange-rates/{open-exchange-rates.ts, .test.ts, .md, index.ts}
|
|
binance/{binance.ts, binance.test.ts, binance.md, index.ts}
|
|
coingecko/{coingecko.ts, coingecko.test.ts, coingecko.md, index.ts}
|
|
coinmarketcap/{coinmarketcap.ts, .test.ts, .md, index.ts}
|
|
cryptocompare/{cryptocompare.ts, .test.ts, .md, index.ts}
|
|
kraken/{kraken.ts, kraken.test.ts, kraken.md, index.ts}
|
|
virtual/{virtual.ts, virtual.test.ts, virtual.md, index.ts}
|
|
index.ts # explicit named re-exports (no `export *`)
|
|
```
|
|
|
|
The `providers/<name>/<name>.ts` shape satisfies §10 (no `-provider` suffix
|
|
inside `providers/`) and the §11 path-naming rule (no parent-folder repetition).
|
|
|
|
## Public surface
|
|
|
|
Each provider class implements `CurrencyProvider` from `@suaveplan/currency`:
|
|
|
|
```ts
|
|
class EcbProvider implements CurrencyProvider {
|
|
readonly name = "ecb";
|
|
readonly supportsCrypto = false;
|
|
readonly supportsFiat = true;
|
|
constructor(config?: NoKeyConfig & { fetch?: FetchLike });
|
|
fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>;
|
|
}
|
|
```
|
|
|
|
`apiKey`-keyed providers (`Fixer`, `OXR`, `CoinMarketCap`) require
|
|
`ApiKeyConfig`; the rest accept `NoKeyConfig` (both optional `baseUrl`).
|
|
Every provider takes an optional `fetch: FetchLike` — defaults to
|
|
`globalThis.fetch`. This is the key genesis change from upstream: tests
|
|
inject a real fake `FetchLike` instead of `vi.stubGlobal('fetch', ...)`, so
|
|
the test suite is hermetic and CLAUDE.md rule 2 ("NEVER use mocks") is
|
|
honoured.
|
|
|
|
`VirtualCurrencyProvider` is the only provider with mutating methods
|
|
(`addCurrency`, `updateRate`, `removeCurrency`, `getRate`,
|
|
`getCurrencyCodes`) because virtual currencies are operator-controlled, not
|
|
fetched. Construction registers every currency with `@suaveplan/money`'s
|
|
`registerCurrency` so downstream `Money` arithmetic resolves them.
|
|
|
|
## Error model
|
|
|
|
Every HTTP / parse / config failure raises
|
|
`CurrencyProviderError extends SuaveplanError<CurrencyProviderErrorCode>`:
|
|
|
|
| Code | When |
|
|
|---|---|
|
|
| `CURRENCY_PROVIDER.HTTP_ERROR` | HTTP response non-2xx |
|
|
| `CURRENCY_PROVIDER.PARSE_ERROR` | Upstream payload missing required field |
|
|
| `CURRENCY_PROVIDER.UPSTREAM_ERROR` | Upstream returned a structured error (Fixer `success:false`, Kraken `error: []` non-empty) |
|
|
| `CURRENCY_PROVIDER.UNSUPPORTED_BASE` | ECB asked for a base it doesn't quote |
|
|
| `CURRENCY_PROVIDER.MISSING_CONFIG` | `apiKey` is required but missing |
|
|
| `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` | `VirtualCurrencyProvider` asked to update/get a code it doesn't have |
|
|
|
|
Discriminants (`providerName`, `status`, `upstreamMessage`) are stored on
|
|
the error so the fallback chain in `CurrencyConverter` can decide
|
|
skip-vs-fail based on the typed information.
|
|
|
|
## Test strategy
|
|
|
|
- Real `FetchLike` substitution: each test injects a small closure that
|
|
returns a synthesised `Response` (built with the WHATWG `Response`
|
|
constructor). Zero `vi.mock`, zero `vi.stubGlobal`.
|
|
- Every test carries `{ timeout: 5000 }` — no real network. WHATWG `Response`
|
|
is available in Node 18+, Bun, and browsers, so no polyfills needed.
|
|
- Coverage gate: 100/100/100/100 (Istanbul). Vitest config mirrors the
|
|
shipped `@suaveplan/currency` config.
|
|
- All tests import runner primitives from `@suaveplan/testing/runner`.
|
|
|
|
## Universal-tier purity
|
|
|
|
No imports from `node:*`, `bun:*`, `fs`, `path`, `stream`, `net`, `http`, or
|
|
`crypto`. The package uses only `globalThis.fetch`, `URLSearchParams`,
|
|
`Response`, `Number.parseFloat`, `Date.now`, `Map`, `Object.entries`, and
|
|
regex. All available in every supported runtime (Node 18+, Bun, modern
|
|
browsers, Web Workers).
|
|
|
|
`package.json` declares `"sideEffects": false` and omits the `browser`
|
|
field and any `engines.node` / `engines.bun` pin.
|
|
|
|
## Deferred enhancements
|
|
|
|
See `proposal.md` `## Out of scope` for the full list. Each requires a
|
|
dependency that isn't yet wired through the upstream `CurrencyProvider`
|
|
interface — landing them in this change would either need (a) a coordinated
|
|
breaking change to `@suaveplan/currency` (rate-limit metadata, historical
|
|
endpoints), (b) a new dep that isn't migrated to genesis yet (rate-limiter,
|
|
cryptocurrency oracle), or (c) a new public surface (streaming adapters,
|
|
aggregator) that warrants its own spec delta. None are blockers for any
|
|
current downstream consumer.
|