deviant/test/unit/token/assetClass.test.ts
Local Dev 36a900ce6d refactor(taxonomy): regular-form class names + DEA renamed to Direct Emission Asset
User-facing labels and long names for six of the eight asset classes now
use the *regular* mint · supply · category form the taxonomy document
sketched, instead of the earlier canonical "Certificate/Tranche/Token"
framings that didn't fit the actual mechanics:

  MCA → MUA  (Managed Unlimited Asset,  was Managed Certificate Asset)
  MTA → MLA  (Managed Limited Asset,    was Managed Tranche Asset)
  PTU → PUT  (Permissionless Unlimited Token)
  PTL → PLT  (Permissionless Limited Token)
  MTU → MUT  (Managed Unlimited Token)
  MTL → MLT  (Managed Limited Token)

DEA keeps its 3-letter code (flagship configuration) but the long name
changes from "Deviant Equilibrium Asset" to "Direct Emission Asset" —
the class describes the mechanism; the "Deviant Equilibrium Asset" name
is reserved for a specific asset the project will mint (its ticker +
name), not the taxonomy class.

DEA / PLA / DRA unchanged (already regular; DRA is a separate family).

Internal `AssetClass` string-literal codes stay canonical (DEA/PLA/DRA/
MCA/MTA/PTU/PTL/MTU/MTL) so registries, serialised tokens, and the
classifier remain stable — only the display fields (`name`, `label`,
`description`) change. Two display tests updated to match.

Taxonomy HTML doc: quadrant, decomposition table, class-definition
cards, classification table, and trailing prose all rewritten to use
the regular names; a "prior canonical" column preserves the mapping.

597/597 unit tests pass. Core tsc clean, shell tsc clean. Deployed to
testchipnet.duckdns.org.
2026-09-02 00:02:53 +02:00

191 lines
8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
classifyAssetClass,
assetClassInfo,
allAssetClasses,
axesOf,
supplyModeFromReservoir,
deriveTierAClass,
deriveTokenClass,
ASSET_CLASS_INFO,
UNLIMITED_RESERVOIR_SENTINEL,
type AssetClass,
} from '../../../src/token/assetClass.js';
import type { TokenTierA } from '../../../src/token/types.js';
// --- the pure 3-axis lookup (V2.7 §2.4) -------------------------------------
test('classifyAssetClass maps all eight axis combinations', () => {
assert.equal(classifyAssetClass({ backing: 'mba', mint: 'permissionless', supply: 'unlimited' }), 'DEA');
assert.equal(classifyAssetClass({ backing: 'mba', mint: 'permissionless', supply: 'limited' }), 'PLA');
assert.equal(classifyAssetClass({ backing: 'mba', mint: 'managed', supply: 'unlimited' }), 'MCA');
assert.equal(classifyAssetClass({ backing: 'mba', mint: 'managed', supply: 'limited' }), 'MTA');
assert.equal(classifyAssetClass({ backing: 'token', mint: 'permissionless', supply: 'unlimited' }), 'PTU');
assert.equal(classifyAssetClass({ backing: 'token', mint: 'permissionless', supply: 'limited' }), 'PTL');
assert.equal(classifyAssetClass({ backing: 'token', mint: 'managed', supply: 'unlimited' }), 'MTU');
assert.equal(classifyAssetClass({ backing: 'token', mint: 'managed', supply: 'limited' }), 'MTL');
});
test('classifyAssetClass and axesOf are inverse for every 3-axis class', () => {
// DRA is template-derived (a 'dynamic' D-1 match), not a 3-axis-cube class:
// its axes (mba/permissionless/unlimited) deliberately collapse onto DEA's
// cell, so it is the one class that does not round-trip through the classifier.
for (const cls of allAssetClasses()) {
if (cls === 'DRA') continue;
assert.equal(classifyAssetClass(axesOf(cls)), cls, `round-trip failed for ${cls}`);
}
assert.equal(classifyAssetClass(axesOf('DRA')), 'DEA', 'DRA shares DEA axes');
});
// --- per-class metadata table -----------------------------------------------
test('ASSET_CLASS_INFO covers the eight 3-axis classes plus DRA', () => {
const codes = allAssetClasses();
// 8 three-axis-cube classes + DRA (the dynamic-rate, template-derived class).
assert.equal(codes.length, 9);
assert.equal(new Set(codes).size, 9);
assert.ok(codes.includes('DRA'), 'DRA must be registered');
for (const cls of codes) {
assert.equal(ASSET_CLASS_INFO[cls].code, cls);
}
});
test('families split MBA (fixed-rate) from Token (no rate)', () => {
const mba: AssetClass[] = ['DEA', 'PLA', 'MCA', 'MTA'];
const token: AssetClass[] = ['PTU', 'PTL', 'MTU', 'MTL'];
for (const c of mba) assert.equal(assetClassInfo(c).family, 'MBA');
for (const c of token) assert.equal(assetClassInfo(c).family, 'Token');
});
test('PNV is shown only for the MBA family', () => {
for (const c of allAssetClasses()) {
const info = assetClassInfo(c);
assert.equal(info.showsPnv, info.family === 'MBA');
}
});
test('ASM eligibility: DEA + DRA full, PLA until-depleted, all others none', () => {
assert.equal(assetClassInfo('DEA').asm, 'full');
assert.equal(assetClassInfo('DRA').asm, 'full'); // dynamic-rate, ASM-eligible both ways
assert.equal(assetClassInfo('PLA').asm, 'until-depleted');
for (const c of allAssetClasses()) {
if (c === 'DEA' || c === 'DRA' || c === 'PLA') continue;
assert.equal(assetClassInfo(c).asm, 'none', `${c} must not be ASM-eligible`);
}
});
test('labels use the regular mint·supply·category form (DEA keeps its flagship code)', () => {
// The `code` field stays canonical for storage compatibility, but the
// user-facing `label` reads out the class's three axes directly.
// MBA: bare regular code · Token: "Token · <regular code>".
const expected: Record<string, string> = {
DEA: 'DEA', // flagship — code and label both stay DEA
PLA: 'PLA', // already regular
DRA: 'DRA', // dynamic-rate variant (own family)
MCA: 'MUA', // Managed Unlimited Asset (was Certificate)
MTA: 'MLA', // Managed Limited Asset (was Tranche)
PTU: 'Token · PUT',
PTL: 'Token · PLT',
MTU: 'Token · MUT',
MTL: 'Token · MLT',
};
for (const c of allAssetClasses()) {
const info = assetClassInfo(c);
assert.equal(info.label, expected[c], `label for class ${c}`);
}
});
// --- supply derivation ------------------------------------------------------
test('supplyModeFromReservoir: the 10^18 sentinel and above is unlimited, below is limited', () => {
assert.equal(supplyModeFromReservoir(UNLIMITED_RESERVOIR_SENTINEL), 'unlimited');
assert.equal(supplyModeFromReservoir(UNLIMITED_RESERVOIR_SENTINEL + 1n), 'unlimited');
assert.equal(supplyModeFromReservoir(UNLIMITED_RESERVOIR_SENTINEL - 1n), 'limited');
assert.equal(supplyModeFromReservoir(1000n), 'limited');
assert.equal(supplyModeFromReservoir(0n), 'limited');
});
// --- Tier-A derivation ------------------------------------------------------
function tierA(over: Partial<TokenTierA> = {}): TokenTierA {
return {
id: 'aa'.repeat(32),
tier: 'A',
name: 'TokenA',
ticker: 'TKA',
rateSats: 1000n,
createdAt: 1,
lastSeen: 1,
userVerified: false,
pinned: false,
hidden: false,
categoryId: 'aa'.repeat(32),
founderFeeBps: 100,
founderLockingBytecodeHex: '76a914' + '11'.repeat(20) + '88ac',
founderAddress: 'bchtest:qrfounder',
contractAddress: 'bchtest:pcontract',
contractTokenAddress: 'bchtest:rcontract',
contractBytecodeHash: '99'.repeat(32),
initialReservoirSupply: '1000000000000000000', // 10^18 → unlimited
genesisTxId: '11'.repeat(32),
...over,
};
}
test('deriveTierAClass: permissionless + unlimited reservoir → DEA', () => {
const d = deriveTierAClass(tierA());
assert.equal(d.assetClass, 'DEA');
assert.equal(d.axes.backing, 'mba');
assert.equal(d.axes.mint, 'permissionless');
assert.equal(d.axes.supply, 'unlimited');
assert.equal(d.info.asm, 'full');
});
test('deriveTierAClass: permissionless + limited reservoir → PLA', () => {
const d = deriveTierAClass(tierA({ initialReservoirSupply: '500' }));
assert.equal(d.assetClass, 'PLA');
assert.equal(d.info.asm, 'until-depleted');
});
test('deriveTierAClass: managed-mint hint yields MCA / MTA', () => {
assert.equal(deriveTierAClass(tierA(), { mintRule: 'managed' }).assetClass, 'MCA');
assert.equal(
deriveTierAClass(tierA({ initialReservoirSupply: '500' }), { mintRule: 'managed' }).assetClass,
'MTA',
);
});
test('deriveTierAClass: corrupted reservoir string falls back to limited (PLA), never throws', () => {
const d = deriveTierAClass(tierA({ initialReservoirSupply: 'not-a-number' }));
assert.equal(d.assetClass, 'PLA');
});
// --- Token-family derivation (PTU / PTL / MTU / MTL) -------------------------
test('deriveTokenClass maps descriptive mint × supply to the four Token classes', () => {
const t = (mintRule: 'permissionless' | 'managed', supplyMode: 'unlimited' | 'limited') =>
deriveTokenClass(tierA({ backing: 'token', mintRule, supplyMode }))?.assetClass;
assert.equal(t('permissionless', 'unlimited'), 'PTU');
assert.equal(t('permissionless', 'limited'), 'PTL');
assert.equal(t('managed', 'unlimited'), 'MTU');
assert.equal(t('managed', 'limited'), 'MTL');
});
test('deriveTokenClass returns null when an axis is unknown (render generic "Token")', () => {
assert.equal(deriveTokenClass(tierA({ backing: 'token', supplyMode: 'limited' })), null);
assert.equal(deriveTokenClass(tierA({ backing: 'token', mintRule: 'managed' })), null);
});
test('deriveTokenClass returns null for an MBA-backed token (wrong family)', () => {
assert.equal(deriveTokenClass(tierA({ mintRule: 'managed', supplyMode: 'limited' })), null);
assert.equal(deriveTokenClass(tierA({ backing: 'mba', mintRule: 'managed', supplyMode: 'limited' })), null);
});
test('Token-family classes are never ASM-eligible and never show PNV', () => {
for (const cls of ['PTU', 'PTL', 'MTU', 'MTL'] as const) {
const info = assetClassInfo(cls);
assert.equal(info.asm, 'none');
assert.equal(info.showsPnv, false);
}
});