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.
191 lines
8 KiB
TypeScript
191 lines
8 KiB
TypeScript
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);
|
||
}
|
||
});
|