Initial Commit
This commit is contained in:
233
openspec/archive/2026-05-17-add-cryptocurrency/design.md
Normal file
233
openspec/archive/2026-05-17-add-cryptocurrency/design.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Design — @suaveplan/cryptocurrency
|
||||
|
||||
> Non-normative. Architecture, layout, and trade-off notes. Normative requirements live in `specs/cryptocurrency/spec.md`; the implementation contract lives in `proposal.md` § Parity Checklist.
|
||||
|
||||
## 1. Layout
|
||||
|
||||
```text
|
||||
packages/finance/cryptocurrency/
|
||||
src/
|
||||
address/
|
||||
validator.ts # validateAddress, validateAddressForChain, validateTxHash, internal EIP-55 checksum
|
||||
validator.md
|
||||
validator.test.ts
|
||||
index.ts
|
||||
chains/
|
||||
chain-data.ts # CHAINS: Record<ChainId, ChainInfo> with 15 entries
|
||||
chain-data.md
|
||||
chain-data.test.ts
|
||||
chain-registry.ts # MemoryChainRegistry + ChainRegistry async façade + LoggerLike injection
|
||||
chain-registry.md
|
||||
chain-registry.test.ts
|
||||
registry.ts # synchronous getChain / getAllChainIds / getEvmChains / getChainByEvmId / getExplorerAddressUrl / getExplorerTxUrl
|
||||
registry.md
|
||||
registry.test.ts
|
||||
index.ts
|
||||
error-codes/
|
||||
error-codes.ts # CRYPTOCURRENCY_CODES + CryptocurrencyError extends SuaveplanError
|
||||
error-codes.md
|
||||
error-codes.test.ts
|
||||
index.ts
|
||||
gas/
|
||||
gas-estimator.ts # estimateGas, calculateMaxFeePerGas, calculateEffectiveGasPrice, formatGwei, estimateTxCostEth
|
||||
gas-estimator.md
|
||||
gas-estimator.test.ts
|
||||
index.ts
|
||||
tokens/
|
||||
token-data.ts # COMMON_TOKENS: TokenInfo[]
|
||||
token-data.md
|
||||
token-data.test.ts
|
||||
token-registry.ts # TokenRegistry + formatTokenBalance
|
||||
token-registry.md
|
||||
token-registry.test.ts
|
||||
index.ts
|
||||
transaction/
|
||||
tx-decoder.ts # KNOWN_SELECTORS + lookupSelector / extractSelector / decodeCalldata / decodeTxHash
|
||||
tx-decoder.md
|
||||
tx-decoder.test.ts
|
||||
index.ts
|
||||
types/
|
||||
crypto.types.ts # ChainId, ChainInfo, ChainRegistryAdapter, CryptoUnit, AddressValidation, TxHashValidation, TokenInfo, GasEstimate, GasEstimateParams, TxMetadata, FunctionSignature
|
||||
crypto.types.md
|
||||
units/
|
||||
converter.ts # convertUnits + ETH/BTC/SOL wrappers + getUnits
|
||||
converter.md
|
||||
converter.test.ts
|
||||
index.ts
|
||||
index.ts # explicit named root barrel
|
||||
tests/setup/polyfills.ts # empty; universal-tier needs no polyfills
|
||||
README.md
|
||||
package.json
|
||||
vite.config.ts
|
||||
vitest.config.ts
|
||||
tsconfig.json
|
||||
tsconfig.lib.json
|
||||
```
|
||||
|
||||
Naming conforms to rules §6a (no `export *` in feature barrels), §10 (no parent/child name redundancy — folder is `address/`, file is `validator.ts`, not `address-validator.ts`), and §11 (no inline object shapes in interfaces). Files stay well under the 300-line hard cap.
|
||||
|
||||
## 2. Public contract surface
|
||||
|
||||
The root barrel re-exports every public symbol explicitly (rule §6a). The full surface matches the upstream `src/index.ts` plus the new error registry:
|
||||
|
||||
```ts
|
||||
// validation
|
||||
export { validateAddress, validateAddressForChain, validateTxHash } from "./address/validator.js";
|
||||
|
||||
// chain data
|
||||
export { CHAINS } from "./chains/chain-data.js";
|
||||
|
||||
// chain registry
|
||||
export { ChainRegistry, MemoryChainRegistry } from "./chains/chain-registry.js";
|
||||
|
||||
// chain getters
|
||||
export {
|
||||
getAllChainIds,
|
||||
getChain,
|
||||
getChainByEvmId,
|
||||
getEvmChains,
|
||||
getExplorerAddressUrl,
|
||||
getExplorerTxUrl,
|
||||
} from "./chains/registry.js";
|
||||
|
||||
// gas
|
||||
export {
|
||||
calculateEffectiveGasPrice,
|
||||
calculateMaxFeePerGas,
|
||||
estimateGas,
|
||||
estimateTxCostEth,
|
||||
formatGwei,
|
||||
} from "./gas/gas-estimator.js";
|
||||
|
||||
// tokens
|
||||
export { COMMON_TOKENS } from "./tokens/token-data.js";
|
||||
export { formatTokenBalance, TokenRegistry } from "./tokens/token-registry.js";
|
||||
|
||||
// transaction
|
||||
export {
|
||||
decodeCalldata,
|
||||
decodeTxHash,
|
||||
extractSelector,
|
||||
KNOWN_SELECTORS,
|
||||
lookupSelector,
|
||||
} from "./transaction/tx-decoder.js";
|
||||
|
||||
// units
|
||||
export {
|
||||
bitcoinToSatoshi,
|
||||
convertUnits,
|
||||
etherToWei,
|
||||
getUnits,
|
||||
gweiToEther,
|
||||
lamportToSol,
|
||||
satoshiToBitcoin,
|
||||
weiToEther,
|
||||
} from "./units/converter.js";
|
||||
|
||||
// types
|
||||
export type {
|
||||
AddressValidation,
|
||||
ChainId,
|
||||
ChainInfo,
|
||||
ChainRegistryAdapter,
|
||||
CryptoUnit,
|
||||
FunctionSignature,
|
||||
GasEstimate,
|
||||
GasEstimateParams,
|
||||
TokenInfo,
|
||||
TxHashValidation,
|
||||
TxMetadata,
|
||||
} from "./types/crypto.types.js";
|
||||
|
||||
// errors
|
||||
export {
|
||||
CRYPTOCURRENCY_CODES,
|
||||
CryptocurrencyError,
|
||||
type CryptocurrencyErrorCode,
|
||||
type CryptocurrencyErrorOptions,
|
||||
} from "./error-codes/error-codes.js";
|
||||
```
|
||||
|
||||
## 3. Error registry
|
||||
|
||||
Plain `throw new Error("Unknown chain: …")` boundaries in upstream become typed `CryptocurrencyError` raises carrying a stable `CRYPTOCURRENCY.*` code. The registry has exactly five codes — one per distinct failure mode in the migrated surface:
|
||||
|
||||
| Code | Raised by | Discriminant fields |
|
||||
|---|---|---|
|
||||
| `CRYPTOCURRENCY.UNKNOWN_CHAIN` | `ChainRegistry.getExplorerAddressUrl` / `getExplorerTxUrl` when the adapter returns no chain for the given id | `chainId` |
|
||||
| `CRYPTOCURRENCY.UNKNOWN_CURRENCY` | `convertUnits` / `getUnits` when the currency tag is not ETH / BTC / SOL | `currency` |
|
||||
| `CRYPTOCURRENCY.UNKNOWN_UNIT` | `convertUnits` when `fromUnit` or `toUnit` is not registered for the currency | `currency`, `unit` |
|
||||
| `CRYPTOCURRENCY.INVALID_ADDRESS` | Reserved for callers — `validateAddress` returns a structured `AddressValidation` rather than throwing, but the code is exposed so callers can wrap the validation result with a typed error if they prefer | `chainId` |
|
||||
| `CRYPTOCURRENCY.INVALID_HASH` | Reserved for callers — same rationale as `INVALID_ADDRESS`, for `validateTxHash` consumers | `chainId` |
|
||||
|
||||
Two codes (`INVALID_ADDRESS`, `INVALID_HASH`) are exposed but not thrown by the package's own code paths — the validators are pure-data returns by design (`{ valid: false, error: "..." }`). The codes exist so application-tier consumers that want to map validation failures to a typed throw can do so without inventing their own code. They are exercised in `error-codes.test.ts` by direct `CryptocurrencyError` construction; the conformance gate is satisfied because the registry's job is to enumerate the namespace consumers can dispatch on, not to force every code through a throw site inside this package. The `error-codes.test.ts` also pins the discriminant shape so the public types stay stable.
|
||||
|
||||
## 4. Type design
|
||||
|
||||
All public types stay in `types/crypto.types.ts`, preserving upstream's structure. The discriminated `ChainInfo.addressType` union (`"base58" | "bech32" | "hex" | "bech32m" | "ss58"`) is preserved verbatim. Optional `evmChainId` distinguishes EVM from non-EVM chains. No inline object shapes inside any interface body (rule §11) — the upstream layout already complies.
|
||||
|
||||
`ChainRegistryAdapter` is an async interface (`Promise`-returning) so consumers can plug in DB-backed or HTTP-backed implementations later without changing the contract:
|
||||
|
||||
```ts
|
||||
export interface ChainRegistryAdapter {
|
||||
getChain(id: ChainId): Promise<ChainInfo | undefined>;
|
||||
getAllChains(): Promise<ChainInfo[]>;
|
||||
getChainByEvmId(evmChainId: number): Promise<ChainInfo | undefined>;
|
||||
registerChain(chain: ChainInfo): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
The synchronous `registry.ts` helpers (`getChain`, `getExplorerAddressUrl`, …) are the fast-path for the static `CHAINS` constant. The async `ChainRegistry` is the extension point for callers wanting custom data sources.
|
||||
|
||||
## 5. EIP-55 algorithm — preserved-as-is
|
||||
|
||||
Upstream's "checksum" is a positional `even → upper, odd → lower` toy algorithm, not the keccak256-based EIP-55 algorithm. The audit (L57-58) flags this as a gap. We **preserve upstream behaviour bit-for-bit** in this migration so any consumer relying on the current output keeps working; swapping to keccak256 requires `@suaveplan/crypto-hash` and is a behaviour change. That swap belongs in a follow-up change (`fix-cryptocurrency-eip55`) gated on the hash dep landing.
|
||||
|
||||
The `validator.md` doc and the `validateAddress` JSDoc explicitly call out the simplified algorithm so callers are not misled.
|
||||
|
||||
## 6. LoggerLike injection
|
||||
|
||||
`ChainRegistry` accepts an optional `LoggerLike` in its constructor. Default is a no-op logger (four empty methods). When wired, the registry logs:
|
||||
|
||||
- `chain.registered` — info — `{ chainId, evmChainId }`
|
||||
- `chain.lookup.miss` — debug — `{ chainId }` (only when `getExplorerAddressUrl` / `getExplorerTxUrl` would raise)
|
||||
|
||||
No other module needs logging — the pure helpers in `registry.ts`, `gas-estimator.ts`, `converter.ts`, and `tx-decoder.ts` have no operational events worth surfacing. This keeps the package leaf-like.
|
||||
|
||||
## 7. Tier purity
|
||||
|
||||
Universal tier — no `node:*`, no `bun:*`, no `fs`/`path`/`stream`/`net`/`http`/`crypto` bare specifiers. The package uses only standard JavaScript built-ins (`BigInt`, `Map`, `RegExp`, `Object.keys`, `Array.from`, `String.prototype.padStart`). `performance.now()` is not used. The package runs unchanged under Node ≥ 18, Bun, modern browsers, and Web Workers.
|
||||
|
||||
`package.json` declares `"sideEffects": false`, no `"browser"` field, no `engines.*` pin.
|
||||
|
||||
## 8. Test runner imports
|
||||
|
||||
All test files import runner primitives from `@suaveplan/testing/runner` (rule §11). Upstream imported from `@suaveplan/testing` root — this is rewritten in the migration commit, not deferred.
|
||||
|
||||
## 9. File budget
|
||||
|
||||
Every source file fits comfortably under 300 lines:
|
||||
|
||||
| File | Upstream LOC | Migrated LOC (est.) |
|
||||
|---|---|---|
|
||||
| `address/validator.ts` | 187 | ~195 |
|
||||
| `chains/chain-data.ts` | 148 | 148 |
|
||||
| `chains/chain-registry.ts` | 97 | ~115 |
|
||||
| `chains/registry.ts` | 37 | ~38 |
|
||||
| `error-codes/error-codes.ts` | — | ~70 |
|
||||
| `gas/gas-estimator.ts` | 99 | 99 |
|
||||
| `tokens/token-data.ts` | 61 | 61 |
|
||||
| `tokens/token-registry.ts` | 98 | 98 |
|
||||
| `transaction/tx-decoder.ts` | 147 | 147 |
|
||||
| `types/crypto.types.ts` | 148 | 148 |
|
||||
| `units/converter.ts` | 169 | ~175 |
|
||||
|
||||
Test files all stay under the 500-line cap.
|
||||
|
||||
## 10. Trade-offs & decisions
|
||||
|
||||
- **Preserve naive EIP-55 algorithm vs. swap to keccak256.** Preserve. The migration's job is to make the upstream surface available on genesis with the error model swapped; behavioural deltas land in follow-up changes. (Audit L57-58 documents the long-term direction.)
|
||||
- **Add `@suaveplan/error` + `@suaveplan/types` as runtime deps vs. hand-roll a local error class.** Add the deps. The audit's "zero runtime deps" preference (L12) was satisfied upstream by hand-rolled `Error`s; genesis policy (CLAUDE.md rule §63) mandates the central `SuaveplanError` shape, and both packages are zero-runtime themselves (`@suaveplan/types`) or near-zero (`@suaveplan/error`). The duplication cost of forking the error shape locally outweighs the dep cost.
|
||||
- **`LoggerLike` on `ChainRegistry` only vs. on every module.** Only the async façade. The pure helpers have no operational events worth surfacing; adding logging there would be inert ceremony.
|
||||
- **Five error codes vs. one per validator failure mode.** Five. `INVALID_ADDRESS` / `INVALID_HASH` are exposed (without internal throws) so application-tier consumers wanting typed dispatch can wrap the validators' structured returns. The validators themselves keep their pure-data return contract for compatibility.
|
||||
- **Subpath exports: 7 (upstream) vs. 11 (migrated).** 11. Tree-shaking is the goal; exposing `chain-data`, `token-data`, `types`, `error-codes` rounds out the legitimate consumption surfaces upstream omitted.
|
||||
Reference in New Issue
Block a user