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

18 KiB

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 Errors; 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.