# 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 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; getAllChains(): Promise; getChainByEvmId(evmChainId: number): Promise; registerChain(chain: ChainInfo): Promise; } ``` 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.