mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
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:
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* ChainProfile registry (Pillar 3 foundation) — one source for chain params across Smirk's five
|
||||
* PoW chains, and the fix for decimals silently defaulting to 12 for anything but WOW/XMR.
|
||||
*/
|
||||
const ChainProfile = require('../src/chain/chainProfile');
|
||||
const { inferCurrencyDecimals } = require('../src/game/helpers/gameModeUtils');
|
||||
|
||||
describe('ChainProfile', () => {
|
||||
test('decimals are correct for every Smirk chain', () => {
|
||||
expect(ChainProfile.decimalsFor('WOW')).toBe(11);
|
||||
expect(ChainProfile.decimalsFor('XMR')).toBe(12);
|
||||
expect(ChainProfile.decimalsFor('BTC')).toBe(8);
|
||||
expect(ChainProfile.decimalsFor('LTC')).toBe(8);
|
||||
expect(ChainProfile.decimalsFor('GRIN')).toBe(9);
|
||||
});
|
||||
|
||||
test('mean block time + adapter family per chain', () => {
|
||||
expect(ChainProfile.meanBlockTimeMsFor('GRIN')).toBe(60000); // ~1 min, fast floor
|
||||
expect(ChainProfile.meanBlockTimeMsFor('BTC')).toBe(600000); // ~10 min, slow end
|
||||
expect(ChainProfile.familyFor('WOW')).toBe('monero');
|
||||
expect(ChainProfile.familyFor('BTC')).toBe('utxo');
|
||||
expect(ChainProfile.familyFor('LTC')).toBe('utxo');
|
||||
expect(ChainProfile.familyFor('GRIN')).toBe('mimblewimble');
|
||||
});
|
||||
|
||||
test('atomic divisor is a BigInt of the right scale', () => {
|
||||
expect(ChainProfile.atomicDivisor('WOW')).toBe(100000000000n); // 1e11
|
||||
expect(ChainProfile.atomicDivisor('BTC')).toBe(100000000n); // 1e8
|
||||
expect(ChainProfile.atomicDivisor('GRIN')).toBe(1000000000n); // 1e9
|
||||
});
|
||||
|
||||
test('case-insensitive; unknown falls back safely', () => {
|
||||
expect(ChainProfile.getProfile('grin').decimals).toBe(9);
|
||||
expect(ChainProfile.isSupported('DOGE')).toBe(false);
|
||||
const fb = ChainProfile.getProfile('DOGE');
|
||||
expect(fb.decimals).toBe(12);
|
||||
expect(fb.family).toBe('monero');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inferCurrencyDecimals now delegates to ChainProfile', () => {
|
||||
test('WOW/XMR unchanged; BTC/GRIN fixed (were silently 12)', () => {
|
||||
expect(inferCurrencyDecimals('WOW')).toBe(11);
|
||||
expect(inferCurrencyDecimals('XMR')).toBe(12);
|
||||
expect(inferCurrencyDecimals('BTC')).toBe(8);
|
||||
expect(inferCurrencyDecimals('GRIN')).toBe(9);
|
||||
expect(inferCurrencyDecimals('')).toBe(12);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user