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, returnsAddressValidationwithvalid/normalized(EIP-55 checksummed for EVM) /chains/error. - ADDED:
validateAddressForChain(address, chainId)— chain-locked variant; narrowschainsto[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-inCHAINSconstant. - ADDED:
MemoryChainRegistryimplementing the asyncChainRegistryAdaptercontract; preloads built-in chains or accepts custom seed list; supportsgetChain/getAllChains/getChainByEvmId/registerChain. - ADDED:
ChainRegistry— async façade over an injectedChainRegistryAdapter, defaulting toMemoryChainRegistry; addsgetEvmChains()/getExplorerAddressUrl()/getExplorerTxUrl()and raisesCryptocurrencyError({ code: CRYPTOCURRENCY.UNKNOWN_CHAIN })on missing chains. - ADDED:
estimateGas(params)— EIP-1559 gas estimator returningGasEstimatewithgasLimit/gasPrice/maxFeePerGas/maxPriorityFeePerGas/totalCost(bigint throughout); honourslegacyGasPriceoverride. - ADDED:
calculateMaxFeePerGas(baseFee, priorityFee)— EIP-1559 formula2 * 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 returningFunctionSignature | undefinedorTxMetadata. - ADDED:
convertUnits(value, fromUnit, toUnit, currency)— generic ETH/BTC/SOL unit conversion (bigint arithmetic). - ADDED:
weiToEther/etherToWei/gweiToEther/satoshiToBitcoin/bitcoinToSatoshi/lamportToSol— convenience wrappers aroundconvertUnits. - ADDED:
getUnits(currency)— returnsCryptoUnit[]listing every denomination for ETH / BTC / SOL. - ADDED: Type exports —
ChainId,ChainInfo,ChainRegistryAdapter,CryptoUnit,AddressValidation,TxHashValidation,TokenInfo,GasEstimate,GasEstimateParams,TxMetadata,FunctionSignature. - ADDED:
CRYPTOCURRENCY_CODESregistry viadefineCodes("CRYPTOCURRENCY", [...])— 5 codes:UNKNOWN_CHAIN,UNKNOWN_CURRENCY,UNKNOWN_UNIT,INVALID_ADDRESS,INVALID_HASH. - ADDED:
CryptocurrencyError extends SuaveplanError<CryptocurrencyErrorCode>with optionalchainId/currency/unitdiscriminants. - ADDED: Optional
LoggerLikeinjection onChainRegistryconstructor — no-op default; logschain.registered/chain.lookup.missevents 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
decodeCalldatarewrite (enterprise audit §3 / §4). The naive selector lookup is preserved bit-for-bit; full ABI decoding (dynamic types, tuples) belongs in a follow-upadd-cryptocurrency-abichange 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'sGasEstimateshape 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
OnChainTokenRegistryaccepting 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-checkfor 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.mdtouching 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 validationsrc/chains/registry.test.ts— covers synchronous chain getters + explorer URL helperssrc/chains/chain-registry.test.ts— coversMemoryChainRegistry+ChainRegistryasync façade +UNKNOWN_CHAINerror pathsrc/chains/chain-data.test.ts— coversCHAINSconstant shape (counts, EVM ids, explorer URLs)src/gas/gas-estimator.test.ts— coversestimateGas/calculateMaxFeePerGas/calculateEffectiveGasPrice/formatGwei/estimateTxCostEthsrc/tokens/token-registry.test.ts— coversTokenRegistry+COMMON_TOKENS+formatTokenBalancesrc/tokens/token-data.test.ts— coversCOMMON_TOKENSshape invariantssrc/transaction/tx-decoder.test.ts— coversKNOWN_SELECTORS+lookupSelector/extractSelector/decodeCalldata/decodeTxHashsrc/units/converter.test.ts— coversconvertUnits+ ETH/BTC/SOL wrappers +getUnits+UNKNOWN_CURRENCY/UNKNOWN_UNITerrorssrc/error-codes/error-codes.test.ts— pins everyCRYPTOCURRENCY.*code to a real call site +CryptocurrencyErrorcause-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) optionalLoggerLikeinjection (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-hashis wired in.
Migration notes
None. Net-new package on genesis with zero consumers; the public contract this proposal locks in is the baseline.