Files
suaveplan/openspec/archive/2026-05-17-add-cryptocurrency/proposal.md
2026-05-27 23:29:53 +01:00

160 lines
18 KiB
Markdown

# Add @suaveplan/cryptocurrency — wallet validation, unit conversion, chain registry, transaction parsing
**Package**: `@suaveplan/cryptocurrency`
**Status**: PROPOSED
**Type**: migration (upstream `../suaveplan/packages/finance/cryptocurrency`)
- **Design**: ./design.md
- **Spec delta**: ./specs/cryptocurrency/spec.md
## Why
`STATUS.md` line 174 lists `@suaveplan/cryptocurrency` as Pending. The upstream package is a small, dependency-light universal-tier utility library (8 implementation files, ~720 LOC of `src/`, zero `@suaveplan/*` runtime deps) covering wallet address validation across 15 chains, EIP-55 checksumming, unit conversion across ETH/BTC/SOL denominations, a static + adapter-backed chain registry, EIP-1559 gas helpers, a token registry, and a transaction calldata/selector decoder. No genesis package covers this surface, and downstream finance / wallet / payment packages will want to depend on it.
The migration preserves the entire public surface bit-for-bit. The only behavioural deltas are (a) replacing plain `throw new Error(...)` boundaries with `SuaveplanError` + a stable `CRYPTOCURRENCY.*` code registry (per CLAUDE.md rule §63), (b) adding the `defaultMessageResolver`-friendly error contract, and (c) gaining `LoggerLike` injection on `ChainRegistry` (no-op default) for parity with sibling packages. The enterprise audit (`../suaveplan/enterprise/cryptocurrency.md` §3-§4) lists ambitious future additions (ABI-aware calldata decoder, EIP-4844 blob-gas, testnets, on-chain token metadata adapter, ENS resolver) — these are explicitly out of scope for this change and belong in follow-up proposals layered on top of the migrated baseline.
## What changes
- ADDED: `validateAddress(address)` — multi-chain address validation with chain auto-detection, returns `AddressValidation` with `valid` / `normalized` (EIP-55 checksummed for EVM) / `chains` / `error`.
- ADDED: `validateAddressForChain(address, chainId)` — chain-locked variant; narrows `chains` to `[chainId]` on success.
- ADDED: `validateTxHash(hash)` — recognises EVM (0x + 64 hex), UTXO (64 hex), and Solana (base58 87-88 char) hash shapes.
- ADDED: `CHAINS` — `Record<ChainId, ChainInfo>` with 15 entries (bitcoin, ethereum, solana, polygon, arbitrum, optimism, avalanche, bsc, base, tron, litecoin, dogecoin, cardano, polkadot, cosmos).
- ADDED: `getChain(id)`, `getAllChainIds()`, `getEvmChains()`, `getChainByEvmId(evmChainId)`, `getExplorerAddressUrl(chainId, address)`, `getExplorerTxUrl(chainId, txHash)` — pure synchronous helpers over the built-in `CHAINS` constant.
- ADDED: `MemoryChainRegistry` implementing the async `ChainRegistryAdapter` contract; preloads built-in chains or accepts custom seed list; supports `getChain`/`getAllChains`/`getChainByEvmId`/`registerChain`.
- ADDED: `ChainRegistry` — async façade over an injected `ChainRegistryAdapter`, defaulting to `MemoryChainRegistry`; adds `getEvmChains()` / `getExplorerAddressUrl()` / `getExplorerTxUrl()` and raises `CryptocurrencyError({ code: CRYPTOCURRENCY.UNKNOWN_CHAIN })` on missing chains.
- ADDED: `estimateGas(params)` — EIP-1559 gas estimator returning `GasEstimate` with `gasLimit` / `gasPrice` / `maxFeePerGas` / `maxPriorityFeePerGas` / `totalCost` (bigint throughout); honours `legacyGasPrice` override.
- ADDED: `calculateMaxFeePerGas(baseFee, priorityFee)` — EIP-1559 formula `2 * base + priority`.
- ADDED: `calculateEffectiveGasPrice(baseFee, maxFee, priorityFee)` — `min(maxFee, baseFee + priorityFee)`.
- ADDED: `formatGwei(weiValue)` — bigint → human-readable gwei string with sign + trailing-zero trim.
- ADDED: `estimateTxCostEth(gasLimit, gasPriceWei)` — bigint → ETH decimal string.
- ADDED: `COMMON_TOKENS` — 8 well-known ERC-20s on Ethereum mainnet (USDT, USDC, DAI, WBTC, WETH, LINK, UNI, MATIC).
- ADDED: `TokenRegistry` — `addToken`/`getToken`/`getTokensByChain`/`searchBySymbol`/`searchByName`/`getAllTokens`/`size`; address keys are case-insensitive.
- ADDED: `formatTokenBalance(rawBalance, decimals)` — bigint balance formatter with sign + trailing-zero trim.
- ADDED: `KNOWN_SELECTORS` — 8 well-known ERC-20 / ERC-721 function signatures (transfer, approve, transferFrom, balanceOf, allowance, totalSupply, safeTransferFrom, setApprovalForAll).
- ADDED: `lookupSelector(selector)`, `extractSelector(calldata)`, `decodeCalldata(calldata)`, `decodeTxHash(hash)` — calldata / tx-hash parsing helpers returning `FunctionSignature | undefined` or `TxMetadata`.
- ADDED: `convertUnits(value, fromUnit, toUnit, currency)` — generic ETH/BTC/SOL unit conversion (bigint arithmetic).
- ADDED: `weiToEther` / `etherToWei` / `gweiToEther` / `satoshiToBitcoin` / `bitcoinToSatoshi` / `lamportToSol` — convenience wrappers around `convertUnits`.
- ADDED: `getUnits(currency)` — returns `CryptoUnit[]` listing every denomination for ETH / BTC / SOL.
- ADDED: Type exports — `ChainId`, `ChainInfo`, `ChainRegistryAdapter`, `CryptoUnit`, `AddressValidation`, `TxHashValidation`, `TokenInfo`, `GasEstimate`, `GasEstimateParams`, `TxMetadata`, `FunctionSignature`.
- ADDED: `CRYPTOCURRENCY_CODES` registry via `defineCodes("CRYPTOCURRENCY", [...])` — 5 codes: `UNKNOWN_CHAIN`, `UNKNOWN_CURRENCY`, `UNKNOWN_UNIT`, `INVALID_ADDRESS`, `INVALID_HASH`.
- ADDED: `CryptocurrencyError extends SuaveplanError<CryptocurrencyErrorCode>` with optional `chainId` / `currency` / `unit` discriminants.
- ADDED: Optional `LoggerLike` injection on `ChainRegistry` constructor — no-op default; logs `chain.registered` / `chain.lookup.miss` events when wired.
- ADDED: Subpath exports `./address/validator`, `./units/converter`, `./chains/registry`, `./chains/chain-registry`, `./chains/chain-data`, `./tokens/token-registry`, `./tokens/token-data`, `./gas/gas-estimator`, `./transaction/tx-decoder`, `./types/crypto.types`, `./error-codes/error-codes` — tree-shakeable.
## Out of scope
- **ABI-aware `decodeCalldata` rewrite** (enterprise audit §3 / §4). The naive selector lookup is preserved bit-for-bit; full ABI decoding (dynamic types, tuples) belongs in a follow-up `add-cryptocurrency-abi` change because it materially expands the type surface and pulls in non-trivial codec code. Reason: orthogonal to the address/unit/chain core this migration ships.
- **Online selector resolver adapter** (enterprise audit §3). Network-bound; lands with the ABI decoder follow-up so the resolver interface can be co-designed.
- **EIP-4844 blob-gas helpers** (enterprise audit §3). Will be added in `add-cryptocurrency-blob-gas`; the gas estimator's `GasEstimate` shape is left forward-compatible.
- **Testnet chain entries + zkRollups / Aptos / Sui / Sei / extended Cosmos zones** (enterprise audit §4). The 15-chain mainnet set is preserved; testnets land in `add-cryptocurrency-testnets`.
- **On-chain `OnChainTokenRegistry` accepting an RPC client** (enterprise audit §4). Requires an RPC-client abstraction not yet defined in genesis; defer until that interface exists.
- **ENS / SNS / UD resolver adapter interface** (enterprise audit §4). Net-new surface; ships in `add-cryptocurrency-names`.
- **UTXO / PSBT helpers** (enterprise audit §4). Different problem domain; ships in `add-cryptocurrency-utxo`.
- **Property-based tests via `fast-check`** for round-trip conversions. The unit converter is fully covered by the upstream characterisation suite; property tests are a future enhancement, not a parity gate.
- **Benchmarks (`src/__benchmarks__/`)** for EIP-55 / bech32 hot paths. Useful but not blocking for a pure utility migration with no IO; defer to enterprise wave.
## Source mapping
Upstream source: `../suaveplan/packages/finance/cryptocurrency/src/` (8 implementation files + 7 test files, ~720 LOC). Every public symbol in upstream `src/index.ts` migrates as-is; layout normalises per the no-prefix and feature-folder rules (no folder/file renames are required — upstream already uses feature-folder layout without redundant prefixes).
Enterprise audit reference: `../suaveplan/enterprise/cryptocurrency.md`.
| Requirement | Source lines | Notes |
|---|---|---|
| Pure utility, zero runtime deps, tree-shakeable, `sideEffects: false` | L12 + L49 | verbatim — zero-dep design is the audit's headline constraint |
| Address validation across 15 chains | L12 + L18-19 | verbatim public API: `validateAddress`, `validateAddressForChain` |
| Auto-detect chain from address format | L12 | verbatim |
| EIP-55 checksumming for EVM addresses | L12 | verbatim (simplified positional algorithm preserved; full keccak256 deferred to enterprise wave per L57-58) |
| Tx-hash validation across EVM / UTXO / Solana | L12 + L19 | verbatim: `validateTxHash` |
| Chain registry with 15 entries, EVM ids, explorer templates, native symbols | L12 + L19-20 | verbatim: `CHAINS`, `ChainInfo`, `ChainRegistry`, `MemoryChainRegistry`, getters |
| Async `ChainRegistryAdapter` interface | L12 + L25 | verbatim |
| EIP-1559 gas estimation helpers | L12 + L20-21 | verbatim public API: `estimateGas`, `calculateMaxFeePerGas`, `calculateEffectiveGasPrice`, `estimateTxCostEth`, `formatGwei` |
| Unit conversion ETH/BTC/SOL + generic `convertUnits` | L12 + L23-24 | verbatim |
| `getUnits(currency)` registry of denominations | L23 | verbatim |
| Token registry with `COMMON_TOKENS` + `formatTokenBalance` | L12 + L21-22 | verbatim |
| Transaction calldata / function selector decoder + `KNOWN_SELECTORS` | L12 + L22-23 | verbatim — naive matcher preserved; ABI decoding scoped out |
| All public type exports | L24-25 | verbatim |
| Subpath exports per public module | L49 | verbatim — preserves upstream's 7 subpaths, adds `./chains/chain-data`, `./tokens/token-data`, `./types/crypto.types`, `./error-codes/error-codes` to round out tree-shaking |
| 100% test coverage | L63 | upstream is ~97.73%; genesis policy is hard 100% (rule §7) |
| 100% docs coverage; sibling `.md` per public symbol | L64 + L97 | verbatim |
| `@suaveplan/error`-aligned error hierarchy | L59 + L88 | verbatim — replaces plain `throw new Error(...)` boundaries with `CryptocurrencyError` + `CRYPTOCURRENCY.*` codes |
| Universal-tier purity (no node/bun deps) | L63 + L65 (`workspace:*` + catalog) + L12 ("zero runtime deps") | verbatim — except `tslib` (per genesis convention) and `@suaveplan/error` + `@suaveplan/types` (genesis error-shape requirement; documented exception below) |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% coverage threshold | Genesis-wide policy (rule §7), not package-specific; upstream tolerates ~97.73% |
| Optional `LoggerLike` on `ChainRegistry` | Genesis convention across migrated packages; no-op default keeps zero runtime cost when unwired |
| `@suaveplan/error` + `@suaveplan/types` runtime deps | Required to align with the central error registry + injectable contracts. The audit's "zero runtime deps" preference (L12, L67-68) was satisfied upstream by hand-rolled plain `Error`s; genesis policy (CLAUDE.md rule §63) mandates the central error shape, so two small typed-only deps are accepted. Both packages are zero-runtime themselves (`@suaveplan/types`) or near-zero (`@suaveplan/error`). |
| Subpath exports for `./chains/chain-data`, `./tokens/token-data`, `./types/crypto.types`, `./error-codes/error-codes` | Tree-shaking completeness; the upstream package omitted these but the data constants and types are valid consumption surfaces |
### Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `validateAddress`, `validateAddressForChain`, `validateTxHash` | ✅ | ✅ | Behaviour bit-for-bit preserved |
| EIP-55 checksum (positional simplified) | ✅ | ✅ | Preserved; full keccak256 deferred to enterprise wave |
| 15-chain `CHAINS` constant | ✅ | ✅ | Identical chain entries |
| `getChain` / `getAllChainIds` / `getEvmChains` / `getChainByEvmId` / `getExplorerAddressUrl` / `getExplorerTxUrl` | ✅ | ✅ | Identical |
| `MemoryChainRegistry` + `ChainRegistry` async adapter façade | ✅ | ⬆️ Enhanced | `throw new Error("Unknown chain: …")` → `CryptocurrencyError({ code: CRYPTOCURRENCY.UNKNOWN_CHAIN, chainId })`; constructor gains optional `LoggerLike` |
| `estimateGas` / `calculateMaxFeePerGas` / `calculateEffectiveGasPrice` / `formatGwei` / `estimateTxCostEth` | ✅ | ✅ | Identical bigint math |
| `COMMON_TOKENS` + `TokenRegistry` + `formatTokenBalance` | ✅ | ✅ | Identical |
| `KNOWN_SELECTORS` + `lookupSelector` / `extractSelector` / `decodeCalldata` / `decodeTxHash` | ✅ | ✅ | Identical (ABI-aware decode scoped out) |
| `convertUnits` + ETH/BTC/SOL convenience wrappers + `getUnits` | ✅ | ⬆️ Enhanced | `throw new Error("Unknown …")` → `CryptocurrencyError` with `UNKNOWN_CURRENCY` / `UNKNOWN_UNIT` codes |
| Subpath exports per public module | ✅ | ⬆️ Enhanced | Upstream exposes 7 subpaths; genesis exposes 11 (adds `chain-data`, `token-data`, `types`, `error-codes`) |
| Plain `throw new Error(...)` at boundaries | ✅ | ⬆️ Replaced | All public errors now `CryptocurrencyError extends SuaveplanError` with stable `CRYPTOCURRENCY.*` codes |
| `LoggerLike` injection on `ChainRegistry` | 🚫 upstream | ⬆️ Added | Genesis convention; no-op default |
| `tslib` runtime dep | ✅ | ✅ | Preserved per genesis convention |
| `@suaveplan/testing` dev dep | ✅ (upstream uses `@suaveplan/testing` root) | ⬆️ Replaced | Uses `@suaveplan/testing/runner` per rule §7 / §11; direct `vitest` / `bun:test` imports banned |
| ABI-aware calldata decoder | 🚫 upstream | 🚫 deferred | Enterprise audit §3 wants this; scoped to `add-cryptocurrency-abi` follow-up so the codec API can be co-designed with the resolver interface |
| Online selector resolver | 🚫 upstream | 🚫 deferred | Network-bound; bundle with ABI decoder follow-up |
| EIP-4844 blob-gas | 🚫 upstream | 🚫 deferred | Net-new helpers; `add-cryptocurrency-blob-gas` follow-up |
| Testnet chains / zkRollups / extended Cosmos / non-EVM L1s | 🚫 upstream | 🚫 deferred | `add-cryptocurrency-testnets` follow-up |
| OnChainTokenRegistry (RPC-backed) | 🚫 upstream | 🚫 deferred | Requires RPC-client abstraction not yet in genesis |
| ENS / SNS / UD resolver interface | 🚫 upstream | 🚫 deferred | `add-cryptocurrency-names` follow-up |
| UTXO / PSBT helpers | 🚫 upstream | 🚫 deferred | Different problem domain; `add-cryptocurrency-utxo` follow-up |
| Benchmarks under `src/__benchmarks__/` | 🚫 upstream | 🚫 deferred | Defer to enterprise wave; pure-CPU helpers without IO are noise-bound |
### Reference docs consulted
- ✅ `../suaveplan/packages/finance/cryptocurrency/src/` — all 8 implementation files read end-to-end
- ✅ `../suaveplan/packages/finance/cryptocurrency/src/**/*.test.ts` — all 7 test files read for characterisation
- ✅ `../suaveplan/enterprise/cryptocurrency.md` — full enterprise audit
- ✅ `../suaveplan/packages/finance/cryptocurrency/package.json` — public exports / dep graph
- ✅ `../suaveplan/packages/finance/cryptocurrency/README.md` — public API examples
- 🚫 `../suaveplan/openspec/specs/finance/spec.md` — no formal upstream spec exists for this capability; the parity contract is the source itself + the enterprise audit
- 🚫 `../suaveplan/openspec/changes/*/proposal.md` touching cryptocurrency — none found
## Verification approach
Every requirement in the spec delta is pinned to one or more concrete test files that will exist after this change ships:
- `src/address/validator.test.ts` — covers address validation, chain auto-detection, EIP-55 checksumming, tx-hash validation
- `src/chains/registry.test.ts` — covers synchronous chain getters + explorer URL helpers
- `src/chains/chain-registry.test.ts` — covers `MemoryChainRegistry` + `ChainRegistry` async façade + `UNKNOWN_CHAIN` error path
- `src/chains/chain-data.test.ts` — covers `CHAINS` constant shape (counts, EVM ids, explorer URLs)
- `src/gas/gas-estimator.test.ts` — covers `estimateGas` / `calculateMaxFeePerGas` / `calculateEffectiveGasPrice` / `formatGwei` / `estimateTxCostEth`
- `src/tokens/token-registry.test.ts` — covers `TokenRegistry` + `COMMON_TOKENS` + `formatTokenBalance`
- `src/tokens/token-data.test.ts` — covers `COMMON_TOKENS` shape invariants
- `src/transaction/tx-decoder.test.ts` — covers `KNOWN_SELECTORS` + `lookupSelector` / `extractSelector` / `decodeCalldata` / `decodeTxHash`
- `src/units/converter.test.ts` — covers `convertUnits` + ETH/BTC/SOL wrappers + `getUnits` + `UNKNOWN_CURRENCY` / `UNKNOWN_UNIT` errors
- `src/error-codes/error-codes.test.ts` — pins every `CRYPTOCURRENCY.*` code to a real call site + `CryptocurrencyError` `cause`-preservation contract
## Migration impact
### Affected dependents
No dependents today — `@suaveplan/cryptocurrency` is being added net-new on genesis. Future consumers (`payment`, `wallet`, `currency-providers`, `react-payment`) will land in subsequent migrations against the public contract this proposal locks in.
Run `bunx turbo ls --filter=...@suaveplan/cryptocurrency` post-migration to confirm zero dependents at archive time.
### Risk
- **Low.** Pure utility library; no IO, no global state, no concurrency. Every helper is a pure function or a small class wrapping a `Map`. The only deviations from upstream are (a) error-type substitution (drop-in: same throw points, richer instance) and (b) optional `LoggerLike` injection (additive). Both are covered by new tests.
- The simplified positional EIP-55 algorithm preserved from upstream is **not** RFC-correct (a real EIP-55 implementation requires keccak256). The audit (L57-58) flags this; the upstream behaviour is preserved so consumers depending on its checksum output keep working. A future change can swap to keccak256 once `@suaveplan/crypto-hash` is wired in.
### Migration notes
None. Net-new package on genesis with zero consumers; the public contract this proposal locks in is the baseline.