feat(chain): ChainProfile registry for Smirk's 5 chains + centralize decimals

New src/chain/chainProfile.js — one data-driven source for chain params (decimals, mean block time, adapter family, URI scheme) across WOW/XMR/BTC/LTC/GRIN, all PoW/high-variance. inferCurrencyDecimals now delegates to it: WOW=11/XMR=12 unchanged, and BTC=8/LTC=8/GRIN=9 are fixed (were silently defaulting to 12). This is the seam the daemon/wallet/address adapters and the block-time difficulty-scaling plug into. +6 tests, suite 50/350 green.

P2 foundation on branch feat/credits-catalog-foundation.
This commit is contained in:
jwinterm
2026-07-10 16:38:25 -04:00
parent 6c7a677057
commit f9d49f34ee
3 changed files with 130 additions and 7 deletions
+76
View File
@@ -0,0 +1,76 @@
/**
* ChainProfile registry — the 10× foundation stone for multi-chain (Pillar 3).
*
* A single, data-driven source for everything chain-specific that ISN'T an RPC call: money units
* (decimals / atomic divisor), the block-timing model (mean block time — which difficulty must
* scale to), the adapter family (which wallet/daemon adapter implements it), and the payment-URI
* scheme. The adapters (daemon/wallet clients, address validators) plug in per family and read
* their params from here.
*
* The set is exactly what the Smirk wallet supports — btc, ltc, xmr, wow, grin (see
* smirk-monorepo packages/core/src/types.ts AssetType) — all proof-of-work / high-variance, so the
* "a random block ends your run" mechanic is fair on every one. Adding a chain = one entry here
* plus a family adapter, never a fork.
*/
const DEFAULT_DECIMALS = 12;
// family: 'monero' (subaddress + transfer_split), 'utxo' (BTC/LTC-style), 'mimblewimble' (Grin).
const PROFILES = Object.freeze({
WOW: Object.freeze({ id: 'WOW', symbol: 'WOW', decimals: 11, meanBlockTimeMs: 120000, family: 'monero', uriScheme: 'wownero' }),
XMR: Object.freeze({ id: 'XMR', symbol: 'XMR', decimals: 12, meanBlockTimeMs: 120000, family: 'monero', uriScheme: 'monero' }),
BTC: Object.freeze({ id: 'BTC', symbol: 'BTC', decimals: 8, meanBlockTimeMs: 600000, family: 'utxo', uriScheme: 'bitcoin' }),
LTC: Object.freeze({ id: 'LTC', symbol: 'LTC', decimals: 8, meanBlockTimeMs: 150000, family: 'utxo', uriScheme: 'litecoin' }),
GRIN: Object.freeze({ id: 'GRIN', symbol: 'GRIN', decimals: 9, meanBlockTimeMs: 60000, family: 'mimblewimble', uriScheme: 'grin' })
});
function normalizeId(cryptoType) {
return String(cryptoType || '').trim().toUpperCase();
}
/**
* Resolve a chain profile. Unknown/unset falls back to a WOW-shaped default (11 decimals, ~2 min)
* so callers never crash on a mis-set CRYPTO_TYPE. Returns a frozen profile object.
*/
function getProfile(cryptoType) {
const id = normalizeId(cryptoType);
if (PROFILES[id]) return PROFILES[id];
return Object.freeze({ id: id || 'WOW', symbol: id || 'WOW', decimals: DEFAULT_DECIMALS, meanBlockTimeMs: 120000, family: 'monero', uriScheme: 'monero' });
}
function decimalsFor(cryptoType) {
return getProfile(cryptoType).decimals;
}
function meanBlockTimeMsFor(cryptoType) {
return getProfile(cryptoType).meanBlockTimeMs;
}
function familyFor(cryptoType) {
return getProfile(cryptoType).family;
}
function uriSchemeFor(cryptoType) {
return getProfile(cryptoType).uriScheme;
}
/** Atomic divisor as a BigInt (10 ** decimals) — safe for ETH-18-scale amounts (none here, but correct). */
function atomicDivisor(cryptoType) {
return 10n ** BigInt(decimalsFor(cryptoType));
}
function isSupported(cryptoType) {
return !!PROFILES[normalizeId(cryptoType)];
}
module.exports = {
DEFAULT_DECIMALS,
PROFILES,
getProfile,
decimalsFor,
meanBlockTimeMsFor,
familyFor,
uriSchemeFor,
atomicDivisor,
isSupported
};
+5 -7
View File
@@ -1,3 +1,5 @@
const ChainProfile = require('../../chain/chainProfile');
const DEFAULT_DECIMALS = 12;
const parseAtomicEnvValue = (val, fallback) => {
@@ -17,13 +19,9 @@ const parseAtomicEnvValue = (val, fallback) => {
return Math.trunc(num);
};
const inferCurrencyDecimals = (symbol) => {
if (!symbol) return DEFAULT_DECIMALS;
const normalized = symbol.toUpperCase();
if (normalized === 'WOW') return 11;
if (normalized === 'XMR') return 12;
return DEFAULT_DECIMALS;
};
// Delegates to the ChainProfile registry — one source for all chains (WOW 11, XMR 12, BTC/LTC 8,
// GRIN 9; unknown -> 12). Previously WOW/XMR were hardcoded and everything else silently got 12.
const inferCurrencyDecimals = (symbol) => ChainProfile.decimalsFor(symbol);
const getDecimalDivisor = (decimals = DEFAULT_DECIMALS) => {
const normalized = Number.isFinite(decimals) ? decimals : DEFAULT_DECIMALS;